From a7ea2be6b7f304ce064765f6699687ce1b3e4d5a Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 11:06:49 +0100 Subject: [PATCH 01/28] perf(onboarding): parallelize vendor/risk extraction, fire-and-forget policies MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The onboarding task blocked for ~4 minutes waiting for all 30+ policy LLM calls to finish before marking completion. Sales demos suffered because the prospect stared at a spinner the entire time. Key changes: - Policy fan-out switched from batchTriggerAndWait to batchTrigger so the main task completes in ~35-70s instead of ~4-5min. Per-policy progress is already tracked via child metadata. - Policy fan-out moved before the linkage gate since policies don't depend on linkage — they start generating ~30-60s earlier. - Vendor and risk extraction (both independent LLM calls) now run in parallel via Promise.all instead of serially. - Owner lookup parallelized with frameworkInstances query. - Two task.updateMany calls (assignee + frequency) merged into one. - triggerVendorResearch serial for-await loop replaced with Promise.allSettled for parallel trigger.dev RPCs. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../onboard-organization-helpers.ts | 30 +-- .../tasks/onboarding/onboard-organization.ts | 188 ++++++++---------- 2 files changed, 96 insertions(+), 122 deletions(-) diff --git a/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts b/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts index c333bdba01..6f8f5774f2 100644 --- a/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts +++ b/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts @@ -935,29 +935,27 @@ async function triggerVendorRiskAssessmentsViaApi(params: { * Triggers research tasks for created vendors */ export async function triggerVendorResearch(vendors: any[]): Promise { - for (const vendor of vendors) { + const researchable = vendors.filter((vendor) => { const website = (vendor.website ?? '').toString().trim(); if (!website) { logger.info(`Skipping research for vendor ${vendor.name} (no website)`); - continue; + return false; } - - // Ensure it's a valid absolute URL; don't let one bad vendor break the whole onboarding. try { // eslint-disable-next-line no-new new URL(website); + return true; } catch { logger.warn(`Skipping research for vendor ${vendor.name} (invalid website URL)`, { website, }); - continue; + return false; } + }); - try { - // `scoreContext` chains the research run into score-vendor-risk - // when GlobalVendors finishes saving, so the per-org Vendor row - // gets a posture-grounded score instead of the conservative - // (possible × moderate) default the extraction pass set. + const results = await Promise.allSettled( + researchable.map(async (vendor) => { + const website = (vendor.website ?? '').toString().trim(); const handle = await tasks.trigger('research-vendor', { website, scoreContext: @@ -966,12 +964,16 @@ export async function triggerVendorResearch(vendors: any[]): Promise { : undefined, }); logger.info(`Triggered research for vendor ${vendor.name} with handle ${handle.id}`); - } catch (error) { + }), + ); + + for (const [i, result] of results.entries()) { + if (result.status === 'rejected') { + const vendor = researchable[i]; logger.error('Failed to trigger vendor research task', { vendorId: vendor.id, vendorName: vendor.name, - website, - error: error instanceof Error ? error.message : String(error), + error: result.reason instanceof Error ? result.reason.message : String(result.reason), }); } } @@ -1310,7 +1312,7 @@ export async function triggerPolicyUpdates( metadata.set(`policy_${policy.id}_status`, 'queued'); }); - await updatePolicy.batchTriggerAndWait( + await updatePolicy.batchTrigger( policies.map((policy) => ({ payload: { organizationId, diff --git a/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts b/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts index caa3bd1813..3c8650ce81 100644 --- a/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts +++ b/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts @@ -61,11 +61,18 @@ export const onboardOrganization = task({ metadata.set('policiesInfo', []); } - const frameworkInstances = await db.frameworkInstance.findMany({ - where: { - organizationId: payload.organizationId, - }, - }); + const [frameworkInstances, owner] = await Promise.all([ + db.frameworkInstance.findMany({ + where: { organizationId: payload.organizationId }, + }), + db.member.findFirst({ + where: { + organizationId: payload.organizationId, + role: { contains: 'owner' }, + deactivated: false, + }, + }), + ]); const frameworks = await db.frameworkEditorFramework.findMany({ where: { @@ -77,107 +84,81 @@ export const onboardOrganization = task({ }, }); - // Get owner - const owner = await db.member.findFirst({ - where: { - organizationId: payload.organizationId, - role: { - contains: 'owner', - }, - deactivated: false, - }, - }); - if (!owner) { logger.error(`Owner not found for organization ${payload.organizationId}`); throw new Error(`Owner not found for organization ${payload.organizationId}`); } - // Update owner to also be an employee - await db.member.update({ - where: { - id: owner.id, - }, - data: { - role: 'owner,employee', - }, - }); - - // Assign owner to all tasks - await db.task.updateMany({ - where: { - organizationId: payload.organizationId, - }, - data: { - assigneeId: owner.id, - }, - }); - - // Update tasks to be quarterly - await db.task.updateMany({ - where: { - organizationId: payload.organizationId, - }, - data: { - frequency: 'quarterly', - }, - }); - - // Extract vendors first so we can show them immediately - const vendorData = await extractVendorsFromContext(questionsAndAnswers); - - // Track vendors immediately as "pending" before creation - if (vendorData.length > 0) { - metadata.set('vendorsTotal', vendorData.length); - metadata.set('vendorsCompleted', 0); - metadata.set('vendorsRemaining', vendorData.length); - // Use temporary IDs based on index until we have real IDs - metadata.set( - 'vendorsInfo', - vendorData.map((v, index) => ({ id: `temp_${index}`, name: v.vendor_name })), - ); - // Mark all as pending initially - vendorData.forEach((_, index) => { - metadata.set(`vendor_temp_${index}_status`, 'pending'); - }); - } - - // Create vendors (pass extracted data to avoid re-extraction) - // Tracking is handled inside createVendors -> createVendorsFromData - const vendors = await createVendors(questionsAndAnswers, payload.organizationId, vendorData); - - // Update tracking with real vendor IDs (tracking during creation uses temp IDs) - if (vendors.length > 0) { - metadata.set( - 'vendorsInfo', - vendors.map((v) => ({ id: v.id, name: v.name })), - ); - // Mark all created vendors as "assessing" since they need mitigation - vendors.forEach((vendor) => { - metadata.set(`vendor_${vendor.id}_status`, 'assessing'); - }); - } - - // Mark vendors step as complete in metadata (real-time) - metadata.set('vendors', true); - metadata.set('currentStep', 'Creating Risks...'); - - // Create risks (tracking is handled inside createRisks) - const risks = await createRisks( - questionsAndAnswers, - payload.organizationId, - organization.name, - ); + await Promise.all([ + db.member.update({ + where: { id: owner.id }, + data: { role: 'owner,employee' }, + }), + db.task.updateMany({ + where: { organizationId: payload.organizationId }, + data: { assigneeId: owner.id, frequency: 'quarterly' }, + }), + ]); - // Mark all created risks as "assessing" since they need mitigation - if (risks.length > 0) { - risks.forEach((risk) => { - metadata.set(`risk_${risk.id}_status`, 'assessing'); - }); - } + // Extract vendors + risks in parallel (both are independent LLM calls) + metadata.set('currentStep', 'Researching Vendors and Risks...'); + + const [vendors, risks] = await Promise.all([ + (async () => { + const vendorData = await extractVendorsFromContext(questionsAndAnswers); + if (vendorData.length > 0) { + metadata.set('vendorsTotal', vendorData.length); + metadata.set('vendorsCompleted', 0); + metadata.set('vendorsRemaining', vendorData.length); + metadata.set( + 'vendorsInfo', + vendorData.map((v, index) => ({ id: `temp_${index}`, name: v.vendor_name })), + ); + vendorData.forEach((_, index) => { + metadata.set(`vendor_temp_${index}_status`, 'pending'); + }); + } + const created = await createVendors( + questionsAndAnswers, + payload.organizationId, + vendorData, + ); + if (created.length > 0) { + metadata.set( + 'vendorsInfo', + created.map((v) => ({ id: v.id, name: v.name })), + ); + created.forEach((vendor) => { + metadata.set(`vendor_${vendor.id}_status`, 'assessing'); + }); + } + metadata.set('vendors', true); + return created; + })(), + (async () => { + const created = await createRisks( + questionsAndAnswers, + payload.organizationId, + organization.name, + ); + if (created.length > 0) { + created.forEach((risk) => { + metadata.set(`risk_${risk.id}_status`, 'assessing'); + }); + } + metadata.set('risk', true); + return created; + })(), + ]); - // Mark risks step as complete in metadata (real-time) - metadata.set('risk', true); + // Start policy fan-out first — policies depend on framework + Q&A + // context only, NOT on linkage. Fire-and-forget so the main task can + // proceed to linkage + mitigations while policies drain in parallel. + // Per-policy progress is tracked via child metadata (policy_${id}_status). + const policyCount = policyList.length; + metadata.set('currentStep', `Tailoring Policies... (0/${policyCount})`); + await updateOrganizationPolicies(payload.organizationId, questionsAndAnswers, frameworks); + metadata.set('policies', true); // Auto-link risks + vendors to existing tasks BEFORE mitigation generation // runs, so the AI prompt for both risks AND vendors sees the linked @@ -198,10 +179,6 @@ export const onboardOrganization = task({ }); } - // Get policy count for the step message - const policyCount = policyList.length; - metadata.set('currentStep', `Tailoring Policies... (0/${policyCount})`); - // Fan-out vendor + risk mitigations now that linkage has populated the // grounding context for both kinds of entities. Done in parallel — // each fan-out task itself batchTriggers per-entity children. @@ -216,11 +193,6 @@ export const onboardOrganization = task({ ), ]); - // Update policies with progress tracking - await updateOrganizationPolicies(payload.organizationId, questionsAndAnswers, frameworks); - - // Mark policies step as complete in metadata (real-time) - metadata.set('policies', true); metadata.set('currentStep', 'Finalizing...'); // Mark onboarding as completed in metadata From c41f21305b32277516d01d955d9a22e792fd25fa Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 11:12:30 +0100 Subject: [PATCH 02/28] perf(onboarding): parallelize risk + vendor matching in linkage step The link-risks-and-vendors-to-work task was taking ~2.5 minutes because risk matching (11 LLM rerank calls) and vendor matching (10 LLM rerank calls) ran sequentially. Both phases write to independent DB tables (Risk.tasks vs Vendor.tasks) and share only a read-only taskById map, so they can safely overlap. Wraps the two mapWithConcurrency calls in Promise.all. Expected wall-time reduction: ~75s (from ~150s to ~75s) since the slower phase no longer has to wait for the other to finish. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/app/src/lib/embedding/run-linkage.ts | 46 ++++++++++++----------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/apps/app/src/lib/embedding/run-linkage.ts b/apps/app/src/lib/embedding/run-linkage.ts index b1954be0a1..1b58be3e15 100644 --- a/apps/app/src/lib/embedding/run-linkage.ts +++ b/apps/app/src/lib/embedding/run-linkage.ts @@ -604,14 +604,22 @@ export async function runLinkage({ let suggestions: RunLinkageOutput['suggestions']; - // Risk matching — fan out with bounded concurrency. Each iteration is one - // vector query + (rerank | DB update). Order doesn't matter here; we sum - // riskLinks at the end and emit `current` based on completion count. + // Emit initial matching phases before the parallel fan-out so the UI shows + // both phases starting at once. if (risks.length > 0) { onPhase?.({ name: 'matching-risks', current: 0, total: risks.length }); } + if (vendors.length > 0) { + onPhase?.({ name: 'matching-vendors', current: 0, total: vendors.length }); + } + + // Risk + vendor matching run in parallel — they write to separate DB + // tables (Risk.tasks vs Vendor.tasks) and the shared taskById is read-only. let completedRisks = 0; - const riskOutcomes = await mapWithConcurrency(risks, MATCH_CONCURRENCY, async (risk) => { + let completedVendors = 0; + + const [riskOutcomes, vendorOutcomes] = await Promise.all([ + mapWithConcurrency(risks, MATCH_CONCURRENCY, async (risk) => { const similar = await findSimilarTasks({ organizationId, queryText: riskQueryText(risk), @@ -687,23 +695,8 @@ export async function runLinkage({ completedRisks += 1; onPhase?.({ name: 'matching-risks', current: completedRisks, total: risks.length }); return { count, perEntitySuggestions }; - }); - const riskLinks = riskOutcomes.reduce((sum, r) => sum + r.count, 0); - // suggestionsOnly endpoints always pass a single riskId, so at most one - // outcome carries suggestions — pick the first non-null. - for (const r of riskOutcomes) { - if (r.perEntitySuggestions) { - suggestions = r.perEntitySuggestions; - break; - } - } - - // Vendor matching — same pattern. - if (vendors.length > 0) { - onPhase?.({ name: 'matching-vendors', current: 0, total: vendors.length }); - } - let completedVendors = 0; - const vendorOutcomes = await mapWithConcurrency(vendors, MATCH_CONCURRENCY, async (vendor) => { + }), + mapWithConcurrency(vendors, MATCH_CONCURRENCY, async (vendor) => { const similar = await findSimilarTasks({ organizationId, queryText: vendorQueryText(vendor), @@ -751,8 +744,17 @@ export async function runLinkage({ completedVendors += 1; onPhase?.({ name: 'matching-vendors', current: completedVendors, total: vendors.length }); return { count, perEntitySuggestions }; - }); + }), + ]); + + const riskLinks = riskOutcomes.reduce((sum, r) => sum + r.count, 0); const vendorLinks = vendorOutcomes.reduce((sum, v) => sum + v.count, 0); + for (const r of riskOutcomes) { + if (r.perEntitySuggestions) { + suggestions = r.perEntitySuggestions; + break; + } + } if (!suggestions) { for (const v of vendorOutcomes) { if (v.perEntitySuggestions) { From a02aeb77ed153ebf32184a16ad1329202930b74c Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 11:17:26 +0100 Subject: [PATCH 03/28] perf(onboarding): switch linkage reranker from gpt-5-mini to gemini-3-flash The LLM reranker dominates linkage wall time (~7s per entity). Gemini 3 Flash is significantly faster and cheaper while being sufficient for the scoring task (0-10 relevance rating on compliance task candidates). Uses the AI SDK gateway so the model swap is a one-line change with no new API key management needed. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/app/src/lib/rerank-suggestions.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/app/src/lib/rerank-suggestions.ts b/apps/app/src/lib/rerank-suggestions.ts index 742d74a64c..a35b397e86 100644 --- a/apps/app/src/lib/rerank-suggestions.ts +++ b/apps/app/src/lib/rerank-suggestions.ts @@ -1,4 +1,4 @@ -import { openai } from '@ai-sdk/openai'; +import { createGatewayProvider } from '@ai-sdk/gateway'; import { generateObject, jsonSchema } from 'ai'; /** @@ -39,7 +39,11 @@ export interface RerankedCandidate { rerankScore: number; } -const RERANK_MODEL = 'gpt-5-mini'; +const gateway = createGatewayProvider({ + baseURL: process.env.AI_GATEWAY_BASE_URL, +}); + +const RERANK_MODEL = 'google/gemini-3-flash' as const; const SYSTEM_PROMPT = `You are a GRC analyst evaluating which compliance tasks would meaningfully reduce a specific risk or vendor exposure. @@ -105,7 +109,7 @@ export async function rerankSuggestions({ .join('\n'); const result = await generateObject({ - model: openai(RERANK_MODEL), + model: gateway(RERANK_MODEL), system: SYSTEM_PROMPT, prompt: userPrompt, schema: rerankSchema, From d29f7b9ec4b1604f8867caf5337cb9a698604c47 Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 11:29:34 +0100 Subject: [PATCH 04/28] chore: upgrade AI SDK to v6/v3, switch reranker to gemini-3-flash via gateway MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upgrades ai@6.0.175, @ai-sdk/openai@3.0.62, @ai-sdk/anthropic@3.0.75, @ai-sdk/gateway@3.0.110, @ai-sdk/google@3.0.68 across all workspaces. The gateway v3 upgrade resolves the v2 specification compatibility warning. Reranker now uses google/gemini-3-flash via the AI gateway for faster, cheaper linkage scoring. Fixes: - gateway.ts ModelOptions type: LanguageModelV2 → LanguageModelV3 - policies.controller.ts: await convertToModelMessages (now async in v6) Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/api/package.json | 8 +- apps/api/src/policies/policies.controller.ts | 2 +- apps/app/package.json | 16 ++- .../[automationId]/tools/gateway.ts | 4 +- bun.lock | 126 ++++++++++-------- packages/integrations/package.json | 4 +- packages/ui/package.json | 4 +- 7 files changed, 93 insertions(+), 71 deletions(-) diff --git a/apps/api/package.json b/apps/api/package.json index c510425c68..acf8fe92c4 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -4,9 +4,9 @@ "version": "0.0.1", "author": "", "dependencies": { - "@ai-sdk/anthropic": "^2.0.53", - "@ai-sdk/groq": "^2.0.32", - "@ai-sdk/openai": "^2.0.65", + "@ai-sdk/anthropic": "^3.0.75", + "@ai-sdk/groq": "^3.0.38", + "@ai-sdk/openai": "^3.0.62", "@aws-sdk/client-acm": "^3.948.0", "@aws-sdk/client-api-gateway": "^3.948.0", "@aws-sdk/client-apigatewayv2": "^3.948.0", @@ -88,7 +88,7 @@ "@upstash/redis": "^1.34.2", "@upstash/vector": "^1.2.2", "adm-zip": "^0.5.16", - "ai": "^5.0.60", + "ai": "^6.0.175", "archiver": "^7.0.1", "axios": "^1.12.2", "better-auth": "^1.4.22", diff --git a/apps/api/src/policies/policies.controller.ts b/apps/api/src/policies/policies.controller.ts index 67a4587c40..0eab46dc33 100644 --- a/apps/api/src/policies/policies.controller.ts +++ b/apps/api/src/policies/policies.controller.ts @@ -1321,7 +1321,7 @@ Keep responses helpful and focused on the policy editing task.`; const result = streamText({ model: openai('gpt-5.5'), system: systemPrompt, - messages: convertToModelMessages(messages), + messages: await convertToModelMessages(messages), }); return result.pipeTextStreamToResponse(res); diff --git a/apps/app/package.json b/apps/app/package.json index ac2bc13498..3103e15404 100644 --- a/apps/app/package.json +++ b/apps/app/package.json @@ -3,12 +3,14 @@ "version": "0.1.0", "type": "module", "dependencies": { - "@ai-sdk/anthropic": "^3.0.0", - "@ai-sdk/groq": "^3.0.0", - "@ai-sdk/openai": "^3.0.0", - "@ai-sdk/provider": "^3.0.0", - "@ai-sdk/react": "^3.0.0", - "@ai-sdk/rsc": "^2.0.0", + "@ai-sdk/anthropic": "^3.0.75", + "@ai-sdk/gateway": "^3.0.110", + "@ai-sdk/google": "^3.0.68", + "@ai-sdk/groq": "^3.0.38", + "@ai-sdk/openai": "^3.0.62", + "@ai-sdk/provider": "^3.0.10", + "@ai-sdk/react": "^3.0.177", + "@ai-sdk/rsc": "^2.0.175", "@aws-sdk/client-ec2": "^3.948.0", "@aws-sdk/client-lambda": "^3.948.0", "@aws-sdk/client-s3": "3.1013.0", @@ -84,7 +86,7 @@ "@vercel/sandbox": "^0.0.21", "@vercel/sdk": "^1.7.1", "@xyflow/react": "^12.10.0", - "ai": "^6.0.116", + "ai": "^6.0.175", "ai-elements": "^1.6.1", "axios": "^1.9.0", "better-auth": "^1.4.22", diff --git a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/tools/gateway.ts b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/tools/gateway.ts index 744e1b1441..49706fd9f1 100644 --- a/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/tools/gateway.ts +++ b/apps/app/src/app/(app)/[orgId]/tasks/[taskId]/automation/[automationId]/tools/gateway.ts @@ -1,6 +1,6 @@ import { createGatewayProvider } from '@ai-sdk/gateway'; import type { OpenAIResponsesProviderOptions } from '@ai-sdk/openai'; -import type { LanguageModelV2 } from '@ai-sdk/provider'; +import type { LanguageModelV3 } from '@ai-sdk/provider'; import type { JSONValue } from 'ai'; export async function getAvailableModels() { @@ -10,7 +10,7 @@ export async function getAvailableModels() { } export interface ModelOptions { - model: LanguageModelV2; + model: LanguageModelV3; providerOptions?: Record>; headers?: Record; } diff --git a/bun.lock b/bun.lock index a31aac1737..390869b3ea 100644 --- a/bun.lock +++ b/bun.lock @@ -71,9 +71,9 @@ "name": "@trycompai/api", "version": "0.0.1", "dependencies": { - "@ai-sdk/anthropic": "^2.0.53", - "@ai-sdk/groq": "^2.0.32", - "@ai-sdk/openai": "^2.0.65", + "@ai-sdk/anthropic": "^3.0.75", + "@ai-sdk/groq": "^3.0.38", + "@ai-sdk/openai": "^3.0.62", "@aws-sdk/client-acm": "^3.948.0", "@aws-sdk/client-api-gateway": "^3.948.0", "@aws-sdk/client-apigatewayv2": "^3.948.0", @@ -155,7 +155,7 @@ "@upstash/redis": "^1.34.2", "@upstash/vector": "^1.2.2", "adm-zip": "^0.5.16", - "ai": "^5.0.60", + "ai": "^6.0.175", "archiver": "^7.0.1", "axios": "^1.12.2", "better-auth": "^1.4.22", @@ -219,12 +219,14 @@ "name": "@trycompai/app", "version": "0.1.0", "dependencies": { - "@ai-sdk/anthropic": "^3.0.0", - "@ai-sdk/groq": "^3.0.0", - "@ai-sdk/openai": "^3.0.0", - "@ai-sdk/provider": "^3.0.0", - "@ai-sdk/react": "^3.0.0", - "@ai-sdk/rsc": "^2.0.0", + "@ai-sdk/anthropic": "^3.0.75", + "@ai-sdk/gateway": "^3.0.110", + "@ai-sdk/google": "^3.0.68", + "@ai-sdk/groq": "^3.0.38", + "@ai-sdk/openai": "^3.0.62", + "@ai-sdk/provider": "^3.0.10", + "@ai-sdk/react": "^3.0.177", + "@ai-sdk/rsc": "^2.0.175", "@aws-sdk/client-ec2": "^3.948.0", "@aws-sdk/client-lambda": "^3.948.0", "@aws-sdk/client-s3": "3.1013.0", @@ -300,7 +302,7 @@ "@vercel/sandbox": "^0.0.21", "@vercel/sdk": "^1.7.1", "@xyflow/react": "^12.10.0", - "ai": "^6.0.116", + "ai": "^6.0.175", "ai-elements": "^1.6.1", "axios": "^1.9.0", "better-auth": "^1.4.22", @@ -538,7 +540,7 @@ }, "packages/db": { "name": "@trycompai/db", - "version": "2.1.1", + "version": "2.3.0", "bin": { "comp-prisma-postinstall": "./dist/postinstall.js", }, @@ -650,12 +652,12 @@ "name": "@trycompai/integrations", "version": "1.0.0", "dependencies": { - "@ai-sdk/openai": "^2.0.0", + "@ai-sdk/openai": "^3.0.62", "@aws-sdk/client-securityhub": "^3.0.0", "@aws-sdk/client-sts": "^3.0.0", "@azure/identity": "^4.10.0", "@trycompai/app": "workspace:*", - "ai": "^5.0.0", + "ai": "^6.0.175", "jsonwebtoken": "^9.0.2", "node-fetch": "^2.6.7", "react": "^19.0.0", @@ -736,7 +738,7 @@ "@tiptap/suggestion": "3.22.1", "@uidotdev/usehooks": "^2.4.1", "@xyflow/react": "^12.9.3", - "ai": "^5.0.101", + "ai": "^6.0.175", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -809,7 +811,7 @@ "@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@3.0.99", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.79", "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-d/WsYOlqjQeEwTewawjrlhoWfHt3q1vRT5/XdFJ6U+KYd/3HnAlrA3rg0+T7xMk98XmctaILJb45Ct/8zrGxSA=="], - "@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.79", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-K0U09FPDO1kmLPjRLXFcNSvmnKHJBMARCb8r3Ulw7wU6/+Zh9djWcFDiPPNsklg6yAezcdLTcYPszgWJJ6iOTA=="], + "@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.75", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-5AV3CKwaOJFdGXhihVgvRLNrjwRn2Xmy71YygT8DYOA+5zTx93Seg2QSIS8b3tJxzZ7X4H84pEtrE8VZKBCZGA=="], "@ai-sdk/azure": ["@ai-sdk/azure@2.0.108", "", { "dependencies": { "@ai-sdk/openai": "2.0.106", "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-/F+lx3glCDiqJfqkZP9IOCubYlWABX2Jg9Yzm/JIxZR5qHfo9rsLwS4zVtghbELVbEjxakaFlDT/c6uTBj0uug=="], @@ -817,17 +819,17 @@ "@ai-sdk/deepseek": ["@ai-sdk/deepseek@1.0.39", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-5TXw7Pm0+/YL2WdnZpXBgruPayhqBgBMNDL95V14Sf4MQz+RmNMhansvK8Fv9Dcgp3Y0p7EasNsPWYJOfj0zoA=="], - "@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.86", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-pP9F5G7C5sqZtAwquFB+g50lVS/s4Wf/ll2WSm0ODk0Iix27trVgBDpFK5CBletcQXSDlAvSQQi25nBodYLt3g=="], + "@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.110", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-sbv8+1L9/BRKydn8dMNwoMQKupA4iLJ9N+yvxgW6wMQ/94UepDf3FeYWMj/dLdzolAHZ6izRUP4s5WqQkmJ2Zg=="], - "@ai-sdk/google": ["@ai-sdk/google@2.0.72", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-BjDY6l+rV4CmHKjZe4H0uRXW3M2o+g7PaYM8oFpW+9PP1qKNEybnJ6//Si7BSf6DT+86dKARrtEl09lxSSaMaA=="], + "@ai-sdk/google": ["@ai-sdk/google@3.0.68", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-bjQSuUmwStn7R0RDGl9I8kriY+xjmschzy5JN4eHPPEOdca2gS6zLc+oi8jhRiCqqROkk3U12Q9M8rmQw7gmbQ=="], "@ai-sdk/google-vertex": ["@ai-sdk/google-vertex@3.0.137", "", { "dependencies": { "@ai-sdk/anthropic": "2.0.79", "@ai-sdk/google": "2.0.72", "@ai-sdk/openai-compatible": "1.0.39", "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25", "google-auth-library": "^10.5.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-vDtCmwMy4CzVsv3PESmkE96qDSqnsArDDEc22eggujZI/WxmIeKa+8vyUYjJUx9HZLOCPo7HhYDXjH0R2mcM+Q=="], - "@ai-sdk/groq": ["@ai-sdk/groq@2.0.40", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-1EL8D1tyjOKjCFUt8XspDoA6zxDcalMsLR2O56ji8QklWsAPaf4TuMJAvf5x5KDrkuJaSAjk94KvPH5hOX+VNQ=="], + "@ai-sdk/groq": ["@ai-sdk/groq@3.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-mzn+KYeROVHFZnAr3qNX+eZ4Un4BFykOcs8XDH8LLzdfgrW6fxQkdiZyww0asYGjIYaa16dkyVtglp4GV6BeUQ=="], "@ai-sdk/mistral": ["@ai-sdk/mistral@2.0.33", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-oBR9nJQ8TRFU0JIIXF+0cFTo8VVEreA1V8AMD3c77BJj/1NUSBLrhyqAbX9k7YAtztvZHUdFcm3+vK8KIx0sUQ=="], - "@ai-sdk/openai": ["@ai-sdk/openai@2.0.106", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EFC0rpo1wfe4HIz5KZCE72edP2J7fOeR7wPXzjCDljaTRB1wectKDIKRLowpU4F0mbcJ+XScAsoYNPK/Z20aVQ=="], + "@ai-sdk/openai": ["@ai-sdk/openai@3.0.62", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Oy74Bztik2X25wZD9HRd83BAXOKcRvrfgz9gvVGqKj68yegf447NiElPbB6TSVb8zyiY9wv1GSGywMCxnnoF9g=="], "@ai-sdk/openai-compatible": ["@ai-sdk/openai-compatible@1.0.39", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-001hdQPPXxYBWrz5d+eAmBVYmwzsB+guIey1DFXi1ZEE5H3j7fRrhPpX55MdM9Fle2DS7WZ8b3qkumCIWE92YQ=="], @@ -3109,7 +3111,7 @@ "@vercel/analytics": ["@vercel/analytics@1.6.1", "", { "peerDependencies": { "@remix-run/react": "^2", "@sveltejs/kit": "^1 || ^2", "next": ">= 13", "react": "^18 || ^19 || ^19.0.0-rc", "svelte": ">= 4", "vue": "^3", "vue-router": "^4" }, "optionalPeers": ["@remix-run/react", "@sveltejs/kit", "next", "react", "svelte", "vue", "vue-router"] }, "sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg=="], - "@vercel/oidc": ["@vercel/oidc@2.0.2", "", { "dependencies": { "@types/ms": "2.1.0", "ms": "2.1.3" } }, "sha512-59PBFx3T+k5hLTEWa3ggiMpGRz1OVvl9eN8SUai+A43IsqiOuAe7qPBf+cray/Fj6mkgnxm/D7IAtjc8zSHi7g=="], + "@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], "@vercel/sandbox": ["@vercel/sandbox@0.0.21", "", { "dependencies": { "@vercel/oidc": "^2.0.2", "async-retry": "1.3.3", "jsonlines": "0.1.1", "ms": "2.1.3", "tar-stream": "3.1.7", "undici": "^7.16.0", "zod": "3.24.4" } }, "sha512-j6nAUQyuw6znVaZGd7yI0nab1EWhEtIZPnTXvpDatJ2SObodYZOz5hGoLjCopAuhQBC7vOngbZ1bwP6HxtB5+g=="], @@ -6683,9 +6685,13 @@ "@actions/http-client/undici": ["undici@6.25.0", "", {}, "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg=="], + "@ai-sdk/amazon-bedrock/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.79", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-K0U09FPDO1kmLPjRLXFcNSvmnKHJBMARCb8r3Ulw7wU6/+Zh9djWcFDiPPNsklg6yAezcdLTcYPszgWJJ6iOTA=="], + "@ai-sdk/amazon-bedrock/@ai-sdk/provider": ["@ai-sdk/provider@2.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww=="], - "@ai-sdk/anthropic/@ai-sdk/provider": ["@ai-sdk/provider@2.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww=="], + "@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.26", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CsKNLKsOpvPujRlIYvoz+Ybw+kGn7J4/fIZa/58+R7iWLLfwn6ifE2G6Yq8K9XvH/I/3bzaDAJ3NhRwEMsLBKQ=="], + + "@ai-sdk/azure/@ai-sdk/openai": ["@ai-sdk/openai@2.0.106", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EFC0rpo1wfe4HIz5KZCE72edP2J7fOeR7wPXzjCDljaTRB1wectKDIKRLowpU4F0mbcJ+XScAsoYNPK/Z20aVQ=="], "@ai-sdk/azure/@ai-sdk/provider": ["@ai-sdk/provider@2.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww=="], @@ -6693,19 +6699,21 @@ "@ai-sdk/deepseek/@ai-sdk/provider": ["@ai-sdk/provider@2.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww=="], - "@ai-sdk/gateway/@ai-sdk/provider": ["@ai-sdk/provider@2.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww=="], + "@ai-sdk/gateway/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.26", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CsKNLKsOpvPujRlIYvoz+Ybw+kGn7J4/fIZa/58+R7iWLLfwn6ifE2G6Yq8K9XvH/I/3bzaDAJ3NhRwEMsLBKQ=="], - "@ai-sdk/gateway/@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="], + "@ai-sdk/google/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.26", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CsKNLKsOpvPujRlIYvoz+Ybw+kGn7J4/fIZa/58+R7iWLLfwn6ifE2G6Yq8K9XvH/I/3bzaDAJ3NhRwEMsLBKQ=="], - "@ai-sdk/google/@ai-sdk/provider": ["@ai-sdk/provider@2.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww=="], + "@ai-sdk/google-vertex/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.79", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-K0U09FPDO1kmLPjRLXFcNSvmnKHJBMARCb8r3Ulw7wU6/+Zh9djWcFDiPPNsklg6yAezcdLTcYPszgWJJ6iOTA=="], + + "@ai-sdk/google-vertex/@ai-sdk/google": ["@ai-sdk/google@2.0.72", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-BjDY6l+rV4CmHKjZe4H0uRXW3M2o+g7PaYM8oFpW+9PP1qKNEybnJ6//Si7BSf6DT+86dKARrtEl09lxSSaMaA=="], "@ai-sdk/google-vertex/@ai-sdk/provider": ["@ai-sdk/provider@2.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww=="], - "@ai-sdk/groq/@ai-sdk/provider": ["@ai-sdk/provider@2.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww=="], + "@ai-sdk/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.26", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CsKNLKsOpvPujRlIYvoz+Ybw+kGn7J4/fIZa/58+R7iWLLfwn6ifE2G6Yq8K9XvH/I/3bzaDAJ3NhRwEMsLBKQ=="], "@ai-sdk/mistral/@ai-sdk/provider": ["@ai-sdk/provider@2.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww=="], - "@ai-sdk/openai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww=="], + "@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.26", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CsKNLKsOpvPujRlIYvoz+Ybw+kGn7J4/fIZa/58+R7iWLLfwn6ifE2G6Yq8K9XvH/I/3bzaDAJ3NhRwEMsLBKQ=="], "@ai-sdk/openai-compatible/@ai-sdk/provider": ["@ai-sdk/provider@2.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww=="], @@ -6775,6 +6783,14 @@ "@browserbasehq/sdk/@types/node": ["@types/node@18.19.130", "", { "dependencies": { "undici-types": "~5.26.4" } }, "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg=="], + "@browserbasehq/stagehand/@ai-sdk/anthropic": ["@ai-sdk/anthropic@2.0.79", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-K0U09FPDO1kmLPjRLXFcNSvmnKHJBMARCb8r3Ulw7wU6/+Zh9djWcFDiPPNsklg6yAezcdLTcYPszgWJJ6iOTA=="], + + "@browserbasehq/stagehand/@ai-sdk/google": ["@ai-sdk/google@2.0.72", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-BjDY6l+rV4CmHKjZe4H0uRXW3M2o+g7PaYM8oFpW+9PP1qKNEybnJ6//Si7BSf6DT+86dKARrtEl09lxSSaMaA=="], + + "@browserbasehq/stagehand/@ai-sdk/groq": ["@ai-sdk/groq@2.0.40", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-1EL8D1tyjOKjCFUt8XspDoA6zxDcalMsLR2O56ji8QklWsAPaf4TuMJAvf5x5KDrkuJaSAjk94KvPH5hOX+VNQ=="], + + "@browserbasehq/stagehand/@ai-sdk/openai": ["@ai-sdk/openai@2.0.106", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-EFC0rpo1wfe4HIz5KZCE72edP2J7fOeR7wPXzjCDljaTRB1wectKDIKRLowpU4F0mbcJ+XScAsoYNPK/Z20aVQ=="], + "@browserbasehq/stagehand/@ai-sdk/provider": ["@ai-sdk/provider@2.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww=="], "@browserbasehq/stagehand/@browserbasehq/sdk": ["@browserbasehq/sdk@2.10.0", "", { "dependencies": { "@types/node": "^18.11.18", "@types/node-fetch": "^2.6.4", "abort-controller": "^3.0.0", "agentkeepalive": "^4.2.1", "form-data-encoder": "1.7.2", "formdata-node": "^4.3.2", "node-fetch": "^2.6.7" } }, "sha512-pOL4yW8P8AI2+N5y6zEP6XXKqIXtYyKunr1JXppqQDOyKLxxvZEDqQCHJXWUzqgx3R1tGWpn7m9AjXN7MeYInA=="], @@ -7271,11 +7287,7 @@ "@trycompai/api/@react-email/render": ["@react-email/render@2.0.8", "", { "dependencies": { "html-to-text": "^9.0.5", "prettier": "^3.5.3" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-5udvVr3U/WuGJZfLdLBOhkzrqRWd2Q5ZYmF7ppcy7FzWcwgshdqLMNqJOXcVzAXJXg/2bm7D+WGJzTtZOZMQnQ=="], - "@trycompai/app/@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.74", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-Xew9rfz9WWhDSyF8rNhjT/XWOWelNfJrMlmG0Ahw210hStisRpQZ1s+7VeI9JTJOZ5y5tXqBi5kfPwYnCfyRTA=="], - - "@trycompai/app/@ai-sdk/groq": ["@ai-sdk/groq@3.0.38", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-mzn+KYeROVHFZnAr3qNX+eZ4Un4BFykOcs8XDH8LLzdfgrW6fxQkdiZyww0asYGjIYaa16dkyVtglp4GV6BeUQ=="], - - "@trycompai/app/@ai-sdk/openai": ["@ai-sdk/openai@3.0.60", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-vZKqbDSCF1T65gDMG2ILJ2NAEpG45U2VtvEve1Fv/WRLu2i5TPUqxjlGKm+5a7Dd57Yr6CGePxrJhsQJC8LJ3A=="], + "@trycompai/api/ai": ["ai@6.0.175", "", { "dependencies": { "@ai-sdk/gateway": "3.0.110", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-6fFFHzbh6FIZnYc31V6osOxq25ABJYCShfG0O6ajHiA4FB/DgnPi1mP8cO5aAU3HNSbQHiMazdlh9bIsp97mVA=="], "@trycompai/app/@mendable/firecrawl-js": ["@mendable/firecrawl-js@1.29.3", "", { "dependencies": { "axios": "^1.11.0", "typescript-event-target": "^1.1.1", "zod": "^3.23.8", "zod-to-json-schema": "^3.23.0" } }, "sha512-+uvDktesJmVtiwxMtimq+3f5bKlsan4T7TokxOI7DbxFkApwrRNss5GYEXbInveMTz8LpGth/9Ch5BTwCqrpfA=="], @@ -7305,8 +7317,12 @@ "@trycompai/framework-editor-cli/@types/node": ["@types/node@22.19.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="], + "@trycompai/integrations/ai": ["ai@6.0.175", "", { "dependencies": { "@ai-sdk/gateway": "3.0.110", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-6fFFHzbh6FIZnYc31V6osOxq25ABJYCShfG0O6ajHiA4FB/DgnPi1mP8cO5aAU3HNSbQHiMazdlh9bIsp97mVA=="], + "@trycompai/portal/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + "@trycompai/ui/ai": ["ai@6.0.175", "", { "dependencies": { "@ai-sdk/gateway": "3.0.110", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26", "@opentelemetry/api": "1.9.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-6fFFHzbh6FIZnYc31V6osOxq25ABJYCShfG0O6ajHiA4FB/DgnPi1mP8cO5aAU3HNSbQHiMazdlh9bIsp97mVA=="], + "@trycompai/ui/shiki": ["shiki@3.23.0", "", { "dependencies": { "@shikijs/core": "3.23.0", "@shikijs/engine-javascript": "3.23.0", "@shikijs/engine-oniguruma": "3.23.0", "@shikijs/langs": "3.23.0", "@shikijs/themes": "3.23.0", "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA=="], "@ts-morph/common/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], @@ -7323,6 +7339,8 @@ "@uploadthing/shared/effect": ["effect@3.17.7", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-dpt0ONUn3zzAuul6k4nC/coTTw27AL5nhkORXgTi6NfMPzqWYa1M05oKmOMTxpVSTKepqXVcW9vIwkuaaqx9zA=="], + "@vercel/sandbox/@vercel/oidc": ["@vercel/oidc@2.0.2", "", { "dependencies": { "@types/ms": "2.1.0", "ms": "2.1.3" } }, "sha512-59PBFx3T+k5hLTEWa3ggiMpGRz1OVvl9eN8SUai+A43IsqiOuAe7qPBf+cray/Fj6mkgnxm/D7IAtjc8zSHi7g=="], + "@vercel/sandbox/tar-stream": ["tar-stream@3.1.7", "", { "dependencies": { "b4a": "^1.6.4", "fast-fifo": "^1.2.0", "streamx": "^2.15.0" } }, "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ=="], "@vercel/sandbox/zod": ["zod@3.24.4", "", {}, "sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg=="], @@ -7333,6 +7351,8 @@ "accepts/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "ai/@ai-sdk/gateway": ["@ai-sdk/gateway@2.0.86", "", { "dependencies": { "@ai-sdk/provider": "2.0.3", "@ai-sdk/provider-utils": "3.0.25", "@vercel/oidc": "3.1.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-pP9F5G7C5sqZtAwquFB+g50lVS/s4Wf/ll2WSm0ODk0Iix27trVgBDpFK5CBletcQXSDlAvSQQi25nBodYLt3g=="], + "ai/@ai-sdk/provider": ["@ai-sdk/provider@2.0.3", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-h88OPkavHTiN9tMn2l5awAznGB0lXzjcLhgR1/rvjB2zlLprsNxbM2tt6OJsHUxduLC3klq0/eqaSf6fX5XVww=="], "ajv-formats/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], @@ -8431,13 +8451,19 @@ "zod-error/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@ai-sdk/react/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@ai-sdk/react/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.110", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-sbv8+1L9/BRKydn8dMNwoMQKupA4iLJ9N+yvxgW6wMQ/94UepDf3FeYWMj/dLdzolAHZ6izRUP4s5WqQkmJ2Zg=="], + "@ai-sdk/gateway/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@ai-sdk/rsc/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@ai-sdk/google/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/groq/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@ai-sdk/rsc/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.110", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-sbv8+1L9/BRKydn8dMNwoMQKupA4iLJ9N+yvxgW6wMQ/94UepDf3FeYWMj/dLdzolAHZ6izRUP4s5WqQkmJ2Zg=="], + "@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/react/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + + "@ai-sdk/rsc/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@angular-devkit/core/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], @@ -8871,16 +8897,10 @@ "@trigger.dev/core/socket.io/engine.io": ["engine.io@6.5.5", "", { "dependencies": { "@types/cookie": "^0.4.1", "@types/cors": "^2.8.12", "@types/node": ">=10.0.0", "accepts": "~1.3.4", "base64id": "2.0.0", "cookie": "~0.4.1", "cors": "~2.8.5", "debug": "~4.3.1", "engine.io-parser": "~5.2.1", "ws": "~8.17.1" } }, "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA=="], - "@trycompai/app/@ai-sdk/anthropic/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.26", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CsKNLKsOpvPujRlIYvoz+Ybw+kGn7J4/fIZa/58+R7iWLLfwn6ifE2G6Yq8K9XvH/I/3bzaDAJ3NhRwEMsLBKQ=="], - - "@trycompai/app/@ai-sdk/groq/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.26", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CsKNLKsOpvPujRlIYvoz+Ybw+kGn7J4/fIZa/58+R7iWLLfwn6ifE2G6Yq8K9XvH/I/3bzaDAJ3NhRwEMsLBKQ=="], - - "@trycompai/app/@ai-sdk/openai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.26", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CsKNLKsOpvPujRlIYvoz+Ybw+kGn7J4/fIZa/58+R7iWLLfwn6ifE2G6Yq8K9XvH/I/3bzaDAJ3NhRwEMsLBKQ=="], + "@trycompai/api/ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.26", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CsKNLKsOpvPujRlIYvoz+Ybw+kGn7J4/fIZa/58+R7iWLLfwn6ifE2G6Yq8K9XvH/I/3bzaDAJ3NhRwEMsLBKQ=="], "@trycompai/app/@mendable/firecrawl-js/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], - "@trycompai/app/ai/@ai-sdk/gateway": ["@ai-sdk/gateway@3.0.110", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.26", "@vercel/oidc": "3.2.0" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-sbv8+1L9/BRKydn8dMNwoMQKupA4iLJ9N+yvxgW6wMQ/94UepDf3FeYWMj/dLdzolAHZ6izRUP4s5WqQkmJ2Zg=="], - "@trycompai/app/ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.26", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CsKNLKsOpvPujRlIYvoz+Ybw+kGn7J4/fIZa/58+R7iWLLfwn6ifE2G6Yq8K9XvH/I/3bzaDAJ3NhRwEMsLBKQ=="], "@trycompai/app/resend/@react-email/render": ["@react-email/render@1.1.2", "", { "dependencies": { "html-to-text": "^9.0.5", "prettier": "^3.5.3", "react-promise-suspense": "^0.3.4" }, "peerDependencies": { "react": "^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-RnRehYN3v9gVlNMehHPHhyp2RQo7+pSkHDtXPvg3s0GbzM9SQMW4Qrf8GRNvtpLC4gsI+Wt0VatNRUFqjvevbw=="], @@ -8893,6 +8913,10 @@ "@trycompai/framework-editor/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "@trycompai/integrations/ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.26", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CsKNLKsOpvPujRlIYvoz+Ybw+kGn7J4/fIZa/58+R7iWLLfwn6ifE2G6Yq8K9XvH/I/3bzaDAJ3NhRwEMsLBKQ=="], + + "@trycompai/ui/ai/@ai-sdk/provider-utils": ["@ai-sdk/provider-utils@4.0.26", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@standard-schema/spec": "^1.1.0", "eventsource-parser": "^3.0.8" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-CsKNLKsOpvPujRlIYvoz+Ybw+kGn7J4/fIZa/58+R7iWLLfwn6ifE2G6Yq8K9XvH/I/3bzaDAJ3NhRwEMsLBKQ=="], + "@trycompai/ui/shiki/@shikijs/core": ["@shikijs/core@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA=="], "@trycompai/ui/shiki/@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.4" } }, "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA=="], @@ -8913,6 +8937,8 @@ "accepts/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + "ai/@ai-sdk/gateway/@vercel/oidc": ["@vercel/oidc@3.1.0", "", {}, "sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w=="], + "ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], "app-builder-lib/hosted-git-info/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="], @@ -9539,10 +9565,6 @@ "wrap-ansi/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "@ai-sdk/react/ai/@ai-sdk/gateway/@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], - - "@ai-sdk/rsc/ai/@ai-sdk/gateway/@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], - "@angular-devkit/schematics/ora/cli-cursor/restore-cursor": ["restore-cursor@3.1.0", "", { "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" } }, "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA=="], "@angular-devkit/schematics/ora/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], @@ -9755,15 +9777,13 @@ "@trigger.dev/core/socket.io/engine.io/ws": ["ws@8.17.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ=="], - "@trycompai/app/@ai-sdk/anthropic/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@trycompai/api/ai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@trycompai/app/@ai-sdk/groq/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - - "@trycompai/app/@ai-sdk/openai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@trycompai/app/ai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@trycompai/app/ai/@ai-sdk/gateway/@vercel/oidc": ["@vercel/oidc@3.2.0", "", {}, "sha512-UycprH3T6n3jH0k44NHMa7pnFHGu/N05MjojYr+Mc6I7obkoLIJujSWwin1pCvdy/eOxrI/l3uDLQsmcrOb4ug=="], + "@trycompai/integrations/ai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], - "@trycompai/app/ai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@trycompai/ui/ai/@ai-sdk/provider-utils/@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@ts-morph/common/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], diff --git a/packages/integrations/package.json b/packages/integrations/package.json index cb36cf72e1..b8e4ba161d 100644 --- a/packages/integrations/package.json +++ b/packages/integrations/package.json @@ -2,12 +2,12 @@ "name": "@trycompai/integrations", "version": "1.0.0", "dependencies": { - "@ai-sdk/openai": "^2.0.0", + "@ai-sdk/openai": "^3.0.62", "@aws-sdk/client-securityhub": "^3.0.0", "@aws-sdk/client-sts": "^3.0.0", "@azure/identity": "^4.10.0", "@trycompai/app": "workspace:*", - "ai": "^5.0.0", + "ai": "^6.0.175", "jsonwebtoken": "^9.0.2", "node-fetch": "^2.6.7", "react": "^19.0.0", diff --git a/packages/ui/package.json b/packages/ui/package.json index 811106279a..0b9da0cfd6 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -8,6 +8,7 @@ "tailwind-merge" ], "dependencies": { + "@floating-ui/dom": "^1.6.0", "@radix-ui/react-accordion": "1.2.12", "@radix-ui/react-alert-dialog": "1.1.15", "@radix-ui/react-avatar": "1.1.10", @@ -36,7 +37,6 @@ "@radix-ui/react-tooltip": "1.2.8", "@radix-ui/react-use-controllable-state": "^1.2.2", "@radix-ui/react-use-is-hydrated": "^0.1.0", - "@floating-ui/dom": "^1.6.0", "@tailwindcss/typography": "^0.5.16", "@tiptap/extension-bold": "3.22.1", "@tiptap/extension-code-block-lowlight": "3.22.1", @@ -54,7 +54,7 @@ "@tiptap/suggestion": "3.22.1", "@uidotdev/usehooks": "^2.4.1", "@xyflow/react": "^12.9.3", - "ai": "^5.0.101", + "ai": "^6.0.175", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", From e8d05804b67a2f7784845be8c5f09b5c1abf80a4 Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 11:32:48 +0100 Subject: [PATCH 05/28] chore: remove caBundleExtension from trigger config The inlined RDS CA bundle was dropped in cd5046cec in favor of Node's default trust store. The extension was failing in dev because rds-global-bundle.pem no longer exists. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/app/trigger.config.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/app/trigger.config.ts b/apps/app/trigger.config.ts index 1a41b7888e..a08ad804da 100644 --- a/apps/app/trigger.config.ts +++ b/apps/app/trigger.config.ts @@ -1,6 +1,5 @@ import { puppeteer } from '@trigger.dev/build/extensions/puppeteer'; import { defineConfig } from '@trigger.dev/sdk'; -import { caBundleExtension } from './caBundleExtension'; import { prismaExtension } from './customPrismaExtension'; export default defineConfig({ @@ -15,7 +14,6 @@ export default defineConfig({ maxDuration: 300, // 5 minutes build: { extensions: [ - caBundleExtension(), prismaExtension({ version: '7.6.0', dbPackageVersion: '^2.0.0', From 6ac5f37596da3234f76bfffea0924c8c934313d4 Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 11:33:14 +0100 Subject: [PATCH 06/28] fix(api): await convertToModelMessages in assistant-chat controller MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same ai SDK v6 breaking change as policies.controller.ts — convertToModelMessages is now async. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/api/src/assistant-chat/assistant-chat.controller.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/api/src/assistant-chat/assistant-chat.controller.ts b/apps/api/src/assistant-chat/assistant-chat.controller.ts index bb7b680381..8c9c30f7df 100644 --- a/apps/api/src/assistant-chat/assistant-chat.controller.ts +++ b/apps/api/src/assistant-chat/assistant-chat.controller.ts @@ -127,7 +127,7 @@ Important: const result = streamText({ model: openai('gpt-5'), system: systemPrompt, - messages: convertToModelMessages(messages), + messages: await convertToModelMessages(messages), tools, stopWhen: stepCountIs(5), }); From d9e067f678c51a1c3544358f495a450dc0d87375 Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 11:36:26 +0100 Subject: [PATCH 07/28] fix(onboarding): restore tracker progress for parallel + fire-and-forget flow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three issues caused the onboarding tracker to lose step tracking: 1. Merged "Researching Vendors and Risks..." currentStep never matched the "Creating Risks" label in the tracker. Fixed by setting currentStep to "Researching Vendors..." initially and switching to "Creating Risks..." when vendors finish or risks start. 2. policies: true was set immediately after batchTrigger (fire-and-forget) before any policy was actually generated. Removed — the tracker already derives completion from policiesCompleted >= policiesTotal. 3. The tracker auto-minimized on run.status === COMPLETED, which now fires before policies/mitigations finish. Changed to also check that all counters (policies, vendors, risks) have reached their totals before auto-minimizing. Also added a hasBackgroundWork branch to the COMPLETED render path so the tracker shows live per-step progress while child tasks are still running, instead of prematurely showing "Setup Complete". Co-Authored-By: Claude Opus 4.7 (1M context) --- .../[orgId]/components/OnboardingTracker.tsx | 124 +++++++++++++++++- .../tasks/onboarding/onboard-organization.ts | 9 +- 2 files changed, 127 insertions(+), 6 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx b/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx index 9ae8559985..5b2cab8d0c 100644 --- a/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx +++ b/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx @@ -95,12 +95,30 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => } }, [dismissKey]); - // Auto-minimize when completed + // Auto-minimize when completed AND all background work is done. + // The main task completes before policies/mitigations finish (they + // run as fire-and-forget children), so also check the counters. useEffect(() => { - if (run?.status === 'COMPLETED' && !isMinimized) { + if (run?.status !== 'COMPLETED' || isMinimized) return; + const meta = run?.metadata as Record | undefined; + if (!meta) return; + + const policiesTotal = (meta.policiesTotal as number) || 0; + const policiesCompleted = (meta.policiesCompleted as number) || 0; + const policiesDone = policiesTotal === 0 || policiesCompleted >= policiesTotal; + + const vendorsTotal = (meta.vendorsTotal as number) || 0; + const vendorsCompleted = (meta.vendorsCompleted as number) || 0; + const vendorsDone = vendorsTotal === 0 || vendorsCompleted >= vendorsTotal; + + const risksTotal = (meta.risksTotal as number) || 0; + const risksCompleted = (meta.risksCompleted as number) || 0; + const risksDone = risksTotal === 0 || risksCompleted >= risksTotal; + + if (policiesDone && vendorsDone && risksDone) { setIsMinimized(true); } - }, [run?.status, isMinimized]); + }, [run?.status, run?.metadata, isMinimized]); // Extract step completion from metadata (real-time updates) const stepStatus = useMemo(() => { @@ -433,6 +451,21 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => const friendlyStatus = getFriendlyStatusName(run.status); + // When the main task is COMPLETED but child tasks (policies, mitigations) + // are still running, show the progress view instead of "Setup Complete". + const hasBackgroundWork = (() => { + if (run.status !== 'COMPLETED') return false; + const meta = run.metadata as Record | undefined; + if (!meta) return false; + const pt = (meta.policiesTotal as number) || 0; + const pc = (meta.policiesCompleted as number) || 0; + const vt = (meta.vendorsTotal as number) || 0; + const vc = (meta.vendorsCompleted as number) || 0; + const rt = (meta.risksTotal as number) || 0; + const rc = (meta.risksCompleted as number) || 0; + return (pt > 0 && pc < pt) || (vt > 0 && vc < vt) || (rt > 0 && rc < rt); + })(); + switch (run.status) { case 'WAITING': case 'QUEUED': @@ -868,6 +901,91 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => ); case 'COMPLETED': + if (hasBackgroundWork) { + return ( +
+ {/* Header — task completed but policies/mitigations still running */} +
+
+ +

+ Setting up your organization +

+
+
+ + +
+
+ + {/* Reuse the same step progress list from the EXECUTING case */} +
+ {ONBOARDING_STEPS.map((step) => { + const isVendorsStep = step.key === 'vendors'; + const isRisksStep = step.key === 'risk'; + const isPoliciesStep = step.key === 'policies'; + + const vendorsComplete = + uniqueVendorsCounts.total > 0 && + uniqueVendorsCounts.completed >= uniqueVendorsCounts.total; + const risksComplete = + stepStatus.risksTotal > 0 && stepStatus.risksCompleted >= stepStatus.risksTotal; + const policiesComplete = + stepStatus.policiesTotal > 0 && + stepStatus.policiesCompleted >= stepStatus.policiesTotal; + + const isDone = + (isVendorsStep && vendorsComplete) || + (isRisksStep && risksComplete) || + (isPoliciesStep && policiesComplete); + + const isProcessing = + (isVendorsStep && !vendorsComplete && stepStatus.vendorsTotal > 0) || + (isRisksStep && !risksComplete && stepStatus.risksTotal > 0) || + (isPoliciesStep && !policiesComplete && stepStatus.policiesTotal > 0); + + const count = isPoliciesStep + ? `${stepStatus.policiesCompleted}/${stepStatus.policiesTotal}` + : isRisksStep + ? `${stepStatus.risksCompleted}/${stepStatus.risksTotal}` + : `${uniqueVendorsCounts.completed}/${uniqueVendorsCounts.total}`; + + return ( +
+ {isDone ? ( + + ) : isProcessing ? ( + + ) : ( +
+ )} + + {step.label} + + {(stepStatus.policiesTotal > 0 || stepStatus.risksTotal > 0 || uniqueVendorsCounts.total > 0) && ( + {count} + )} +
+ ); + })} +
+
+ ); + } return (
{/* Header */} diff --git a/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts b/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts index 3c8650ce81..271aaf00ff 100644 --- a/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts +++ b/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts @@ -100,8 +100,10 @@ export const onboardOrganization = task({ }), ]); - // Extract vendors + risks in parallel (both are independent LLM calls) - metadata.set('currentStep', 'Researching Vendors and Risks...'); + // Extract vendors + risks in parallel (both are independent LLM calls). + // Each branch sets its own currentStep so the tracker highlights + // whichever phase is still running. + metadata.set('currentStep', 'Researching Vendors...'); const [vendors, risks] = await Promise.all([ (async () => { @@ -133,9 +135,11 @@ export const onboardOrganization = task({ }); } metadata.set('vendors', true); + metadata.set('currentStep', 'Creating Risks...'); return created; })(), (async () => { + metadata.set('currentStep', 'Creating Risks...'); const created = await createRisks( questionsAndAnswers, payload.organizationId, @@ -158,7 +162,6 @@ export const onboardOrganization = task({ const policyCount = policyList.length; metadata.set('currentStep', `Tailoring Policies... (0/${policyCount})`); await updateOrganizationPolicies(payload.organizationId, questionsAndAnswers, frameworks); - metadata.set('policies', true); // Auto-link risks + vendors to existing tasks BEFORE mitigation generation // runs, so the AI prompt for both risks AND vendors sees the linked From a5628301053b8e9a7300992f3f7621f22ac6b45a Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 11:37:20 +0100 Subject: [PATCH 08/28] fix(onboarding): handle pipeda and other framework conditionals in policy templates Policy templates use {{#if pipeda}}...{{/if}} handlebars syntax but the prompt only evaluated soc2 and hipaa, defaulting everything else to false. Added a generic framework matcher that covers pipeda, gdpr, iso27001, pci, nist, and ccpa so new framework conditionals don't silently strip content. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/app/src/trigger/lib/prompts.ts | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/apps/app/src/trigger/lib/prompts.ts b/apps/app/src/trigger/lib/prompts.ts index 1372487a22..a18332193c 100644 --- a/apps/app/src/trigger/lib/prompts.ts +++ b/apps/app/src/trigger/lib/prompts.ts @@ -1,6 +1,24 @@ import { FrameworkEditorFramework, FrameworkEditorPolicyTemplate } from '@db'; import { logger } from '@trigger.dev/sdk'; +const FRAMEWORK_MATCHERS: Array<{ flag: string; test: (name: string) => boolean }> = [ + { flag: 'soc2', test: (n) => /soc\s*2/i.test(n) || n.includes('soc') }, + { flag: 'hipaa', test: (n) => n.includes('hipaa') }, + { flag: 'pipeda', test: (n) => n.includes('pipeda') }, + { flag: 'gdpr', test: (n) => n.includes('gdpr') }, + { flag: 'iso27001', test: (n) => /iso\s*27001/i.test(n) }, + { flag: 'pci', test: (n) => /pci/i.test(n) }, + { flag: 'nist', test: (n) => /nist/i.test(n) }, + { flag: 'ccpa', test: (n) => n.includes('ccpa') }, +]; + +function buildFrameworkFlags(frameworks: FrameworkEditorFramework[]): string { + return FRAMEWORK_MATCHERS.map(({ flag, test }) => { + const active = frameworks.some((f) => test(f.name.toLowerCase())); + return ` - ${flag} is ${active ? 'true' : 'false'}`; + }).join('\n'); +} + export const generatePrompt = ({ policyTemplate, contextHub, @@ -29,10 +47,7 @@ export const generatePrompt = ({ frameworks.length > 0 ? frameworks.map((f) => `${f.name} v${f.version}`).join(', ') : 'None explicitly selected'; - const hasHIPAA = frameworks.some((f) => f.name.toLowerCase().includes('hipaa')); - const hasSOC2 = frameworks.some( - (f) => /soc\s*2/i.test(f.name) || f.name.toLowerCase().includes('soc'), - ); + const frameworkFlags = buildFrameworkFlags(frameworks); return ` Company: ${companyName} (${companyWebsite}) @@ -67,10 +82,9 @@ Required rules (keep this simple): - Do NOT copy instruction cue lines (e.g., "Add a HIPAA checklist...", "State that...", "Clarify that..."). Convert such cues into real policy language, and then remove the cue line entirely. If a cue precedes bullet points, keep the bullets but delete the cue line. 3) Handlebars-style conditionals - - The template may contain conditional blocks using {{#if var}}...{{/if}} syntax (e.g., {{#if soc2}}, {{#if hipaa}}). + - The template may contain conditional blocks using {{#if var}}...{{/if}} syntax (e.g., {{#if soc2}}, {{#if hipaa}}, {{#if pipeda}}). - Evaluate these using the selected frameworks: - - soc2 is ${hasSOC2 ? 'true' : 'false'} - - hipaa is ${hasHIPAA ? 'true' : 'false'} +${frameworkFlags} - If the condition is true: keep only the inner content and remove the {{#if}}/{{/if}} markers. - If the condition is false: remove the entire block including its content. - For any other unknown {{#if X}} variables: assume false and remove the block. From bff0b2440b8c8d4148be24d7ee0e676d91a97f2b Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 11:41:36 +0100 Subject: [PATCH 09/28] fix(onboarding): restore expandable sections in tracker during background work When the main task is COMPLETED but child tasks are still running, the switch now maps to the EXECUTING render path (which has the full expandable vendor/risk/policy lists) instead of a simplified flat view that was missing the expand arrows. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../[orgId]/components/OnboardingTracker.tsx | 87 +------------------ 1 file changed, 1 insertion(+), 86 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx b/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx index 5b2cab8d0c..945a8f34fb 100644 --- a/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx +++ b/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx @@ -466,7 +466,7 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => return (pt > 0 && pc < pt) || (vt > 0 && vc < vt) || (rt > 0 && rc < rt); })(); - switch (run.status) { + switch (hasBackgroundWork ? 'EXECUTING' : run.status) { case 'WAITING': case 'QUEUED': case 'EXECUTING': @@ -901,91 +901,6 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) =>
); case 'COMPLETED': - if (hasBackgroundWork) { - return ( -
- {/* Header — task completed but policies/mitigations still running */} -
-
- -

- Setting up your organization -

-
-
- - -
-
- - {/* Reuse the same step progress list from the EXECUTING case */} -
- {ONBOARDING_STEPS.map((step) => { - const isVendorsStep = step.key === 'vendors'; - const isRisksStep = step.key === 'risk'; - const isPoliciesStep = step.key === 'policies'; - - const vendorsComplete = - uniqueVendorsCounts.total > 0 && - uniqueVendorsCounts.completed >= uniqueVendorsCounts.total; - const risksComplete = - stepStatus.risksTotal > 0 && stepStatus.risksCompleted >= stepStatus.risksTotal; - const policiesComplete = - stepStatus.policiesTotal > 0 && - stepStatus.policiesCompleted >= stepStatus.policiesTotal; - - const isDone = - (isVendorsStep && vendorsComplete) || - (isRisksStep && risksComplete) || - (isPoliciesStep && policiesComplete); - - const isProcessing = - (isVendorsStep && !vendorsComplete && stepStatus.vendorsTotal > 0) || - (isRisksStep && !risksComplete && stepStatus.risksTotal > 0) || - (isPoliciesStep && !policiesComplete && stepStatus.policiesTotal > 0); - - const count = isPoliciesStep - ? `${stepStatus.policiesCompleted}/${stepStatus.policiesTotal}` - : isRisksStep - ? `${stepStatus.risksCompleted}/${stepStatus.risksTotal}` - : `${uniqueVendorsCounts.completed}/${uniqueVendorsCounts.total}`; - - return ( -
- {isDone ? ( - - ) : isProcessing ? ( - - ) : ( -
- )} - - {step.label} - - {(stepStatus.policiesTotal > 0 || stepStatus.risksTotal > 0 || uniqueVendorsCounts.total > 0) && ( - {count} - )} -
- ); - })} -
-
- ); - } return (
{/* Header */} From 4b4a88d92ddf4c666ebb6b532aef4c9cda5a80d6 Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 11:42:56 +0100 Subject: [PATCH 10/28] fix(onboarding): use tasks.batchTrigger for policies to preserve parent metadata updatePolicy.batchTrigger() creates independent runs with no parent relationship, so metadata.parent in the policy tasks was null and completion counters never reached the onboarding task's metadata. Switched to tasks.batchTrigger() which creates child runs, restoring the parent-child relationship needed for per-policy progress tracking. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../trigger/tasks/onboarding/onboard-organization-helpers.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts b/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts index 6f8f5774f2..0cc51b6a96 100644 --- a/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts +++ b/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts @@ -1312,7 +1312,8 @@ export async function triggerPolicyUpdates( metadata.set(`policy_${policy.id}_status`, 'queued'); }); - await updatePolicy.batchTrigger( + await tasks.batchTrigger( + 'update-policy', policies.map((policy) => ({ payload: { organizationId, @@ -1320,7 +1321,6 @@ export async function triggerPolicyUpdates( contextHub: questionsAndAnswers.map((c) => `${c.question}\n${c.answer}`).join('\n'), frameworks, }, - concurrencyKey: organizationId, })), ); } From df813cd1925c1771b6247cea36c81f818d485842 Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 11:46:34 +0100 Subject: [PATCH 11/28] fix(onboarding): restore concurrencyKey for policy batch trigger The concurrencyKey was lost when switching from updatePolicy.batchTrigger to tasks.batchTrigger. Without it, the queue's concurrencyLimit: 5 applies globally across all orgs instead of per-org, causing policy runs from different onboarding sessions to block each other. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/trigger/tasks/onboarding/onboard-organization-helpers.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts b/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts index 0cc51b6a96..623ccab0fe 100644 --- a/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts +++ b/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts @@ -1321,6 +1321,7 @@ export async function triggerPolicyUpdates( contextHub: questionsAndAnswers.map((c) => `${c.question}\n${c.answer}`).join('\n'), frameworks, }, + options: { concurrencyKey: organizationId }, })), ); } From dcd783bbeb0f7305ca1e3a67e2f5d436e02ed8e8 Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 11:48:31 +0100 Subject: [PATCH 12/28] fix(onboarding): sync all tracker spinner animations Each Loader2 spinner computed its own Date.now() offset independently, causing them to rotate out of phase. Moved the offset into a single useMemo so all spinners share the same animation delay. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../[orgId]/components/OnboardingTracker.tsx | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx b/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx index 945a8f34fb..b87ca4cf44 100644 --- a/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx +++ b/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx @@ -60,6 +60,10 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => const [isPoliciesExpanded, setIsPoliciesExpanded] = useState(false); const [isVendorsExpanded, setIsVendorsExpanded] = useState(false); const [isRisksExpanded, setIsRisksExpanded] = useState(false); + const spinnerStyle = useMemo(() => ({ + animation: 'spin 1s linear infinite', + animationDelay: `${-(Date.now() % 1000)}ms`, + }), []); // useRealtimeRun will automatically get the token from TriggerProvider context // This gives us real-time updates including metadata changes @@ -413,7 +417,7 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => if (!run && !error) { return (
- +

Initializing...

Checking onboarding status

@@ -565,7 +569,7 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => {isCompleted ? ( ) : isCurrent || isActivelyProcessing ? ( - + ) : vendorsQueued ? ( ) : ( @@ -617,7 +621,7 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => {isVendorCompleted ? ( ) : isVendorProcessing ? ( - + ) : isVendorQueued ? ( ) : ( @@ -671,7 +675,7 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => {isCompleted ? ( ) : isCurrent || isActivelyProcessing ? ( - + ) : risksQueued ? ( ) : ( @@ -722,7 +726,7 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => {isRiskCompleted ? ( ) : isRiskProcessing ? ( - + ) : (
)} @@ -774,7 +778,7 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => {isCompleted ? ( ) : isCurrent || isActivelyProcessing ? ( - + ) : policiesQueued ? ( ) : ( @@ -828,7 +832,7 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => {isPolicyCompleted ? ( ) : isPolicyProcessing ? ( - + ) : isPolicyQueued ? ( ) : ( @@ -877,7 +881,7 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => {isCompleted ? ( ) : isCurrent ? ( - + ) : policiesQueued ? ( ) : ( From 29bcf3768c0e8bffb96e0f08f07a632b0d314c7e Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 11:49:30 +0100 Subject: [PATCH 13/28] perf(onboarding): raise policy queue concurrency from 5 to 15 Policies now fire before mitigations (different phase) and mitigations use their own queues, so the original concern about slot starvation no longer applies. 15 concurrent cuts policy wall time roughly 3x. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/trigger/tasks/onboarding/update-policy.ts | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/apps/app/src/trigger/tasks/onboarding/update-policy.ts b/apps/app/src/trigger/tasks/onboarding/update-policy.ts index f17bcbafde..d5b4b0d2e3 100644 --- a/apps/app/src/trigger/tasks/onboarding/update-policy.ts +++ b/apps/app/src/trigger/tasks/onboarding/update-policy.ts @@ -6,17 +6,7 @@ if (!process.env.OPENAI_API_KEY) { throw new Error('OPENAI_API_KEY is not set'); } -// v4: define queue ahead of time. -// concurrencyLimit is intentionally low so a 30+ policy onboarding fan-out -// can't hog all of dev's 25-slot env budget. Each policy update takes ~20-40s -// (LLM call + DB writes), while risk + vendor mitigations take ~8-12s. With -// no cap the slower policies fill every slot first and the user-visible -// mitigation tasks sit queued for minutes. Capping at 5 leaves ≥20 slots -// for the faster, latency-sensitive mitigation fan-out to finish quickly, -// then policies drain through. Total wall time is ~the same, but the user -// sees their treatment plans populate immediately instead of waiting for -// every policy to finish first. -export const updatePolicyQueue = queue({ name: 'update-policy', concurrencyLimit: 5 }); +export const updatePolicyQueue = queue({ name: 'update-policy', concurrencyLimit: 15 }); export const updatePolicy = schemaTask({ id: 'update-policy', From 4e29a15f9842ff9a2199d82f389b60fcf25f09cb Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 11:55:47 +0100 Subject: [PATCH 14/28] perf(onboarding): switch policy generation from gpt-5-mini to gemini-3-flash All three LLM calls in update-policies-helpers.ts (content generation, format reconciliation, format check) now use google/gemini-3-flash via the AI gateway. Gemini Flash is significantly faster for structured output tasks like TipTap JSON generation. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../tasks/onboarding/update-policies-helpers.ts | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/apps/app/src/trigger/tasks/onboarding/update-policies-helpers.ts b/apps/app/src/trigger/tasks/onboarding/update-policies-helpers.ts index 899b02c074..a24941b9b1 100644 --- a/apps/app/src/trigger/tasks/onboarding/update-policies-helpers.ts +++ b/apps/app/src/trigger/tasks/onboarding/update-policies-helpers.ts @@ -1,4 +1,4 @@ -import { openai } from '@ai-sdk/openai'; +import { createGatewayProvider } from '@ai-sdk/gateway'; import { db, FrameworkEditorFramework, FrameworkEditorPolicyTemplate, type Policy } from '@db/server'; import type { JSONContent } from '@tiptap/react'; import { logger } from '@trigger.dev/sdk'; @@ -7,6 +7,11 @@ import { z } from 'zod'; import { generatePrompt } from '../../lib/prompts'; // Sanitization utilities +const gateway = createGatewayProvider({ + baseURL: process.env.AI_GATEWAY_BASE_URL, +}); +const POLICY_MODEL = 'google/gemini-3-flash' as const; + const PLACEHOLDER_REGEX = /<<\s*TO\s*REVIEW\s*>>/gi; function extractText(node: Record): string { @@ -194,7 +199,7 @@ export async function reconcileFormatWithTemplate( ): Promise<{ type: 'document'; content: Record[] }> { try { const { object } = await generateObject({ - model: openai('gpt-5-mini'), + model: gateway(POLICY_MODEL), output: 'no-schema', system: `You are an expert policy editor. Given an ORIGINAL policy TipTap JSON and a DRAFT TipTap JSON, produce a FINAL TipTap JSON that: @@ -232,7 +237,7 @@ export async function aiCheckFormatWithTemplate( ): Promise<{ isConforming: boolean; reasons: string[] }> { try { const { object } = await generateObject({ - model: openai('gpt-5-mini'), + model: gateway(POLICY_MODEL), system: `You are validating policy layout. Compare ORIGINAL vs DRAFT (TipTap JSON). Determine if DRAFT conforms to ORIGINAL format: - Same top-level section titles present and in the same order @@ -457,7 +462,7 @@ export async function generatePolicyContent(prompt: string): Promise<{ }> { try { const { object } = await generateObject({ - model: openai('gpt-5-mini'), + model: gateway(POLICY_MODEL), output: 'no-schema', system: `You are an expert at writing security policies. Generate content directly as TipTap JSON format. From 41b8cc1e41602bcd56d6f82329ae6a7b8e9cf3d9 Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 12:00:54 +0100 Subject: [PATCH 15/28] perf(onboarding): policies first, gemini for all LLM calls, non-blocking vendor side-effects Three changes: 1. Policy fan-out now fires BEFORE vendor/risk extraction. Policies only need frameworks + Q&A context, so they start draining immediately instead of waiting 10-50s for vendor/risk creation. 2. All four LLM calls in onboard-organization-helpers (vendor extraction, risk extraction, vendor mitigation, risk mitigation) switched from openai gpt-4.1-mini/gpt-5-mini to google/gemini-3-flash via the AI gateway. 3. triggerVendorRiskAssessmentsViaApi and triggerVendorResearch are now fire-and-forget (void instead of await). These are side effects that were adding 2-15s to the vendor creation path without contributing to the onboarding flow. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../onboard-organization-helpers.ts | 26 ++++++++++--------- .../tasks/onboarding/onboard-organization.ts | 17 +++++------- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts b/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts index 623ccab0fe..cc6289d01a 100644 --- a/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts +++ b/apps/app/src/trigger/tasks/onboarding/onboard-organization-helpers.ts @@ -1,4 +1,4 @@ -import { openai } from '@ai-sdk/openai'; +import { createGatewayProvider } from '@ai-sdk/gateway'; import { Departments, FrameworkEditorFramework, @@ -14,6 +14,11 @@ import { import { db } from '@db/server'; import { logger, metadata, tasks } from '@trigger.dev/sdk'; import { generateObject, jsonSchema } from 'ai'; + +const gateway = createGatewayProvider({ + baseURL: process.env.AI_GATEWAY_BASE_URL, +}); +const ONBOARDING_MODEL = 'google/gemini-3-flash' as const; import axios from 'axios'; import { z } from 'zod'; import type { researchVendor } from '../scrape/research'; @@ -496,7 +501,7 @@ export async function extractVendorsFromContext( const customVendorNameSet = new Set(customVendors.map((v) => v.name.toLowerCase())); const { object } = await generateObject({ - model: openai('gpt-4.1-mini'), + model: gateway(ONBOARDING_MODEL), schema: jsonSchema({ type: 'object', properties: { @@ -664,7 +669,7 @@ Citations (write one sentence per item, in order): ${formatCitationsBlock(citations)}`; const result = await generateObject({ - model: openai('gpt-5-mini'), + model: gateway(ONBOARDING_MODEL), system: RISK_MITIGATION_PROMPT, prompt: userPrompt, schema: sentencesSchema, @@ -1033,7 +1038,7 @@ Citations (write one sentence per item, in order): ${formatCitationsBlock(citations)}`; const result = await generateObject({ - model: openai('gpt-5-mini'), + model: gateway(ONBOARDING_MODEL), system: RISK_MITIGATION_PROMPT, prompt: userPrompt, schema: sentencesSchema, @@ -1105,7 +1110,7 @@ export async function extractRisksFromContext( existingRisks: { title: string }[], ): Promise { const { object } = await generateObject({ - model: openai('gpt-4.1-mini'), + model: gateway(ONBOARDING_MODEL), schema: jsonSchema({ type: 'object', properties: { @@ -1366,17 +1371,14 @@ export async function createVendors( triggeredCount: vendorsForRiskAssessment.length, }); - // TODO: Un-comment this when UI part is ready - await triggerVendorRiskAssessmentsViaApi({ + // Fire-and-forget: risk assessments + research are side effects that + // don't need to block the main onboarding flow. + void triggerVendorRiskAssessmentsViaApi({ organizationId, vendors: vendorsForRiskAssessment, - // Onboarding should NOT force expensive research if GlobalVendors already has data. - // If data is missing, the API/Trigger pipeline will still do research. withResearch: false, }); - - // Trigger background research for each vendor (best-effort) - await triggerVendorResearch(createdVendors); + void triggerVendorResearch(createdVendors); return createdVendors; } diff --git a/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts b/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts index 271aaf00ff..e47a12d98f 100644 --- a/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts +++ b/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts @@ -100,9 +100,14 @@ export const onboardOrganization = task({ }), ]); + // Policies only need frameworks + Q&A — start them immediately so they + // drain while vendors/risks/linkage run. Fire-and-forget; per-policy + // progress tracked via child metadata (policy_${id}_status). + const policyCount = policyList.length; + metadata.set('currentStep', `Tailoring Policies... (0/${policyCount})`); + await updateOrganizationPolicies(payload.organizationId, questionsAndAnswers, frameworks); + // Extract vendors + risks in parallel (both are independent LLM calls). - // Each branch sets its own currentStep so the tracker highlights - // whichever phase is still running. metadata.set('currentStep', 'Researching Vendors...'); const [vendors, risks] = await Promise.all([ @@ -155,14 +160,6 @@ export const onboardOrganization = task({ })(), ]); - // Start policy fan-out first — policies depend on framework + Q&A - // context only, NOT on linkage. Fire-and-forget so the main task can - // proceed to linkage + mitigations while policies drain in parallel. - // Per-policy progress is tracked via child metadata (policy_${id}_status). - const policyCount = policyList.length; - metadata.set('currentStep', `Tailoring Policies... (0/${policyCount})`); - await updateOrganizationPolicies(payload.organizationId, questionsAndAnswers, frameworks); - // Auto-link risks + vendors to existing tasks BEFORE mitigation generation // runs, so the AI prompt for both risks AND vendors sees the linked // tasks/controls and produces grounded plans. Fan-out for both happens From faa8d0e662c11343c9cd60a82bb4d597ae917f26 Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 12:09:00 +0100 Subject: [PATCH 16/28] perf(onboarding): replace LLM policy generation with programmatic template processor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Policy generation was the single slowest step — each of the 28 policies required a 25-35s LLM call to regenerate the entire TipTap JSON document. Since the templates already contain the full policy structure with handlebars placeholders ({{COMPANY}}, {{#if hipaa}}...{{/if}}), the LLM was doing work that can be done programmatically: - {{PLACEHOLDER}} replacement with values from the onboarding Q&A - {{#if framework}}...{{/if}} conditional evaluation based on selected frameworks (soc2, hipaa, pipeda, gdpr, iso27001, pci, nist, ccpa) The new process-policy-template.ts walks the TipTap JSON tree, replaces placeholders, evaluates conditionals (including nested and multi-node spans), and returns the processed content. No LLM call needed. Result: ~10ms per policy instead of ~30s. 28 policies finish in under 1 second total instead of ~60s. Also removes ~350 lines of dead LLM-based code (generatePolicyContent, reconcileFormatWithTemplate, aiCheckFormatWithTemplate, and all their helpers) and the OPENAI_API_KEY requirement from update-policy.ts. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../onboarding/process-policy-template.ts | 196 ++++++++ .../onboarding/update-policies-helpers.ts | 450 +----------------- .../trigger/tasks/onboarding/update-policy.ts | 4 - 3 files changed, 204 insertions(+), 446 deletions(-) create mode 100644 apps/app/src/trigger/tasks/onboarding/process-policy-template.ts diff --git a/apps/app/src/trigger/tasks/onboarding/process-policy-template.ts b/apps/app/src/trigger/tasks/onboarding/process-policy-template.ts new file mode 100644 index 0000000000..3928d648f8 --- /dev/null +++ b/apps/app/src/trigger/tasks/onboarding/process-policy-template.ts @@ -0,0 +1,196 @@ +type JsonNode = Record; + +const PLACEHOLDER_QUESTIONS: Array<[string, RegExp]> = [ + ['COMPANYINFO', /describe your company/i], + ['INDUSTRY', /what industry/i], + ['EMPLOYEES', /how many employees/i], + ['DEVICES', /what devices/i], + ['SOFTWARE', /what software/i], + ['LOCATION', /how does your team work/i], + ['CRITICAL', /where do you host/i], + ['DATA', /what type(s)? of data/i], + ['GEO', /where is your data/i], +]; + +const FRAMEWORK_MATCHERS: Array<[string, (n: string) => boolean]> = [ + ['soc2', (n) => /soc\s*2/i.test(n) || n.includes('soc')], + ['hipaa', (n) => n.includes('hipaa')], + ['pipeda', (n) => n.includes('pipeda')], + ['gdpr', (n) => n.includes('gdpr')], + ['iso27001', (n) => /iso\s*27001/i.test(n)], + ['pci', (n) => /pci/i.test(n)], + ['nist', (n) => /nist/i.test(n)], + ['ccpa', (n) => n.includes('ccpa')], +]; + +export function buildVariables({ + companyName, + contextHub, +}: { + companyName: string; + contextHub: string; +}): Record { + const vars: Record = { COMPANY: companyName }; + const lines = contextHub.split('\n'); + + for (let i = 0; i < lines.length - 1; i++) { + for (const [key, pattern] of PLACEHOLDER_QUESTIONS) { + if (!vars[key] && pattern.test(lines[i])) { + vars[key] = lines[i + 1]?.trim() || 'N/A'; + } + } + } + + return vars; +} + +export function buildFlags( + frameworks: Array<{ name: string }>, +): Record { + const flags: Record = {}; + for (const [flag, test] of FRAMEWORK_MATCHERS) { + flags[flag] = frameworks.some((f) => test(f.name.toLowerCase())); + } + return flags; +} + +function extractText(node: JsonNode): string { + if (typeof node.text === 'string') return node.text; + if (Array.isArray(node.content)) { + return (node.content as JsonNode[]).map(extractText).join(''); + } + return ''; +} + +function replacePlaceholdersInText(text: string, vars: Record): string { + return text.replace(/\{\{(\w+)\}\}/g, (match, key: string) => vars[key] ?? 'N/A'); +} + +function processInlineConditionals(text: string, flags: Record): string { + return text.replace( + /\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, + (_match, flag: string, inner: string) => (flags[flag] ? inner : ''), + ); +} + +function processTextNode(node: JsonNode, vars: Record, flags: Record): JsonNode | null { + if (typeof node.text !== 'string') return node; + + let text = node.text; + text = processInlineConditionals(text, flags); + text = replacePlaceholdersInText(text, vars); + text = text.trim(); + + if (!text) return null; + return { ...node, text }; +} + +function processNode(node: JsonNode, vars: Record, flags: Record): JsonNode | null { + if (node.type === 'text') { + return processTextNode(node, vars, flags); + } + + if (!Array.isArray(node.content)) return node; + + const processed = processContentArray(node.content as JsonNode[], vars, flags); + if (processed.length === 0 && node.type !== 'document') return null; + + return { ...node, content: processed }; +} + +/** + * Walks a TipTap content array, evaluates {{#if}}…{{/if}} blocks that + * span multiple sibling nodes, replaces {{PLACEHOLDER}} values, and + * returns the cleaned array. Fully deterministic — no LLM calls. + */ +export function processContentArray( + nodes: JsonNode[], + vars: Record, + flags: Record, +): JsonNode[] { + const result: JsonNode[] = []; + let skipDepth = 0; + + for (const node of nodes) { + const text = extractText(node); + + const openMatch = text.match(/\{\{#if\s+(\w+)\}\}/); + const closeMatch = text.includes('{{/if}}'); + const hasOnlyMarker = /^\s*\{\{#if\s+\w+\}\}\s*$/.test(text) || + /^\s*\{\{\/if\}\}\s*$/.test(text); + + if (openMatch && closeMatch) { + if (skipDepth === 0) { + const processed = processNode(node, vars, flags); + if (processed) result.push(processed); + } + continue; + } + + if (openMatch) { + const flag = openMatch[1]; + const isTrue = flags[flag] ?? false; + if (!isTrue) { + skipDepth++; + } else if (!hasOnlyMarker) { + const processed = processNode(node, vars, flags); + if (processed) result.push(processed); + } + continue; + } + + if (closeMatch) { + if (skipDepth > 0) { + skipDepth--; + } else if (!hasOnlyMarker) { + const processed = processNode(node, vars, flags); + if (processed) result.push(processed); + } + continue; + } + + if (skipDepth > 0) continue; + + const processed = processNode(node, vars, flags); + if (processed) result.push(processed); + } + + return result; +} + +/** + * Processes a full TipTap document: replaces handlebars placeholders with + * real values and evaluates conditional blocks based on framework flags. + * Returns the processed content array (inner nodes of the document). + */ +export function processTemplate({ + content, + companyName, + contextHub, + frameworks, +}: { + content: unknown; + companyName: string; + contextHub: string; + frameworks: Array<{ name: string }>; +}): JsonNode[] { + const vars = buildVariables({ companyName, contextHub }); + const flags = buildFlags(frameworks); + + let nodes: JsonNode[]; + if ( + content && + typeof content === 'object' && + 'type' in (content as JsonNode) && + (content as JsonNode).type === 'doc' && + Array.isArray((content as JsonNode).content) + ) { + nodes = (content as JsonNode).content as JsonNode[]; + } else if (Array.isArray(content)) { + nodes = content as JsonNode[]; + } else { + return []; + } + + return processContentArray(nodes, vars, flags); +} diff --git a/apps/app/src/trigger/tasks/onboarding/update-policies-helpers.ts b/apps/app/src/trigger/tasks/onboarding/update-policies-helpers.ts index a24941b9b1..36ec9c5c5d 100644 --- a/apps/app/src/trigger/tasks/onboarding/update-policies-helpers.ts +++ b/apps/app/src/trigger/tasks/onboarding/update-policies-helpers.ts @@ -1,368 +1,7 @@ -import { createGatewayProvider } from '@ai-sdk/gateway'; import { db, FrameworkEditorFramework, FrameworkEditorPolicyTemplate, type Policy } from '@db/server'; import type { JSONContent } from '@tiptap/react'; import { logger } from '@trigger.dev/sdk'; -import { generateObject, NoObjectGeneratedError } from 'ai'; -import { z } from 'zod'; -import { generatePrompt } from '../../lib/prompts'; - -// Sanitization utilities -const gateway = createGatewayProvider({ - baseURL: process.env.AI_GATEWAY_BASE_URL, -}); -const POLICY_MODEL = 'google/gemini-3-flash' as const; - -const PLACEHOLDER_REGEX = /<<\s*TO\s*REVIEW\s*>>/gi; - -function extractText(node: Record): string { - const text = node && typeof node['text'] === 'string' ? (node['text'] as string) : ''; - const content = Array.isArray((node as any)?.content) - ? ((node as any).content as Record[]) - : null; - if (content && content.length > 0) { - return content.map(extractText).join(''); - } - return text || ''; -} - -function sanitizeNodePlaceholders(node: Record): Record { - const cloned: Record = { ...node }; - if (typeof cloned['text'] === 'string') { - const replaced = (cloned['text'] as string) - .replace(PLACEHOLDER_REGEX, '') - .replace(/\s{2,}/g, ' ') - .trim(); - cloned['text'] = replaced; - } - const content = Array.isArray((cloned as any).content) - ? ((cloned as any).content as Record[]) - : null; - if (content) { - (cloned as any).content = content.map(sanitizeNodePlaceholders); - } - return cloned; -} - -function shouldRemoveAuditorArtifactsHeading(headingText: string): boolean { - const lower = headingText.trim().toLowerCase(); - // Match variations: artefacts/artifacts and with/without "evidence" - return lower.includes('auditor') && (lower.includes('artefact') || lower.includes('artifact')); -} - -function removeAuditorArtifactsSection( - content: Record[], -): Record[] { - const result: Record[] = []; - let i = 0; - while (i < content.length) { - const node = content[i] as Record; - const nodeType = typeof node['type'] === 'string' ? (node['type'] as string) : ''; - if (nodeType === 'heading') { - const headingText = extractText(node); - if (shouldRemoveAuditorArtifactsHeading(headingText)) { - // Skip this heading and subsequent nodes until next heading or end - i += 1; - while (i < content.length) { - const nextNode = content[i] as Record; - const nextType = typeof nextNode['type'] === 'string' ? (nextNode['type'] as string) : ''; - if (nextType === 'heading') break; - i += 1; - } - continue; - } - } - result.push(sanitizeNodePlaceholders(node)); - i += 1; - } - return result; -} - -function sanitizeDocument(document: { type: 'document'; content: Record[] }) { - const content = Array.isArray(document.content) ? document.content : []; - const withoutAuditorArtifacts = removeAuditorArtifactsSection(content); - return { - type: 'document' as const, - content: withoutAuditorArtifacts, - }; -} - -/** - * Extract text from a heading node - */ -function extractHeadingText(node: Record): string { - const type = typeof node['type'] === 'string' ? (node['type'] as string) : ''; - if (type !== 'heading') return ''; - return extractText(node).trim(); -} - -/** - * Get allowed top-level heading titles from the original/template content - * We consider headings with level 1 or 2 as top-level anchors for section boundaries. - */ -function getAllowedTopLevelHeadings(originalContent: Record[]): string[] { - const allowed: string[] = []; - for (const node of originalContent) { - const type = typeof node['type'] === 'string' ? (node['type'] as string) : ''; - if (type === 'heading') { - const level = (node as any)?.attrs?.level; - if (typeof level === 'number' && level >= 1 && level <= 2) { - const text = extractHeadingText(node); - if (text) allowed.push(text.toLowerCase()); - } - } - } - return allowed; -} - -/** - * Remove sections that should not exist (Table of Contents, Mapping sections) and - * drop any new top-level sections not present in the original/template headings. - */ -function alignToTemplateStructure( - updated: { type: 'document'; content: Record[] }, - originalContent: Record[], -): { type: 'document'; content: Record[] } { - const allowedTopHeadings = getAllowedTopLevelHeadings(originalContent); - if (allowedTopHeadings.length === 0) { - // Nothing to enforce; return as-is - return updated; - } - - const isForbiddenHeading = (headingText: string): boolean => { - const lower = headingText.toLowerCase(); - if (lower.includes('table of contents')) return true; - if (lower.includes('mapping') && lower.includes('soc')) return true; // e.g., SOC 2 mappings - return false; - }; - - const result: Record[] = []; - let i = 0; - const content = Array.isArray(updated.content) ? updated.content : []; - - while (i < content.length) { - const node = content[i] as Record; - const nodeType = typeof node['type'] === 'string' ? (node['type'] as string) : ''; - - if (nodeType === 'heading') { - const level = (node as any)?.attrs?.level; - const headingText = extractHeadingText(node); - - // Skip forbidden sections entirely - if (isForbiddenHeading(headingText)) { - i += 1; - while (i < content.length) { - const nextNode = content[i] as Record; - const nextType = typeof nextNode['type'] === 'string' ? (nextNode['type'] as string) : ''; - if (nextType === 'heading') break; - i += 1; - } - continue; - } - - // Enforce allowed top-level headings - if (typeof level === 'number' && level >= 1 && level <= 2) { - const normalized = headingText.toLowerCase(); - if (!allowedTopHeadings.includes(normalized)) { - // Drop this new top-level section and its content until next heading - i += 1; - while (i < content.length) { - const nextNode = content[i] as Record; - const nextType = - typeof nextNode['type'] === 'string' ? (nextNode['type'] as string) : ''; - if (nextType === 'heading') break; - i += 1; - } - continue; - } - } - } - - // Keep node (with placeholder sanitization already applied earlier) - result.push(node); - i += 1; - } - - return { type: 'document', content: result }; -} - -/** - * AI reconciliation step: ensure the draft keeps the same top-level section structure - * as the original template while using the new content where headings match. - * - Preserve the order and heading levels from the original. - * - For each top-level heading in the original, use the draft section content if present - * (matched by heading text, case-insensitive); otherwise keep the original section content. - * - Do not introduce new top-level sections, TOC, or mapping sections. - */ -export async function reconcileFormatWithTemplate( - originalContent: Record[], - draft: { type: 'document'; content: Record[] }, -): Promise<{ type: 'document'; content: Record[] }> { - try { - const { object } = await generateObject({ - model: gateway(POLICY_MODEL), - output: 'no-schema', - system: `You are an expert policy editor. -Given an ORIGINAL policy TipTap JSON and a DRAFT TipTap JSON, produce a FINAL TipTap JSON that: -- Preserves the ORIGINAL top-level section structure (order and presence of titles) and visual presentation of titles. -- VISUAL CONSISTENCY: For each ORIGINAL top-level title, match its visual style in the FINAL exactly: - - If the ORIGINAL uses a heading, keep the same heading level in the FINAL. - - If the ORIGINAL uses a bold paragraph as the title, use a bold paragraph for that title in the FINAL (single text node with a bold mark). - - After each title, ensure at least one paragraph node exists (may be empty if content is not provided). -- CONTENT SELECTION: For each ORIGINAL title, prefer the DRAFT's corresponding section content when the title text matches (case-insensitive). If no matching DRAFT section exists, keep the ORIGINAL section content. -- COMPLETENESS: Include every ORIGINAL top-level title exactly once and in the same order as the ORIGINAL. Do not omit any original section, even if the DRAFT lacks content for it (in that case, keep the ORIGINAL section or include an empty paragraph placeholder under the title). -- PROHIBITIONS: Do not add new top-level sections. Do not include a Table of Contents. Do not add framework mapping sections unless they already exist in the ORIGINAL. -- OUTPUT FORMAT: Valid TipTap JSON with root {"type":"document","content":[...]}.`, - prompt: `ORIGINAL (TipTap JSON):\n${JSON.stringify({ type: 'document', content: originalContent })}\n\nDRAFT (TipTap JSON):\n${JSON.stringify(draft)}\n\nReturn ONLY the FINAL TipTap JSON document with type "document" and a "content" array. -Follow the structure rules above strictly.`, - }); - const parsed = object as { type?: string; content?: unknown }; - if (parsed?.type !== 'document' || !Array.isArray(parsed?.content)) { - throw new Error('AI response did not match expected TipTap document structure'); - } - return { type: 'document' as const, content: parsed.content as Record[] }; - } catch (error) { - logger.error('AI reconcile format step failed; falling back to deterministic alignment', { - error: error instanceof Error ? error.message : String(error), - }); - return draft; - } -} - -/** - * AI format checker: returns whether DRAFT conforms to ORIGINAL's format - */ -export async function aiCheckFormatWithTemplate( - originalContent: Record[], - draft: { type: 'document'; content: Record[] }, -): Promise<{ isConforming: boolean; reasons: string[] }> { - try { - const { object } = await generateObject({ - model: gateway(POLICY_MODEL), - system: `You are validating policy layout. -Compare ORIGINAL vs DRAFT (TipTap JSON). Determine if DRAFT conforms to ORIGINAL format: -- Same top-level section titles present and in the same order -- Title visual style matches (heading level vs bold paragraph) -- No new top-level sections added; no Table of Contents; no framework mapping sections if not in ORIGINAL -- After every title there is at least one paragraph node -Return JSON { isConforming: boolean, reasons: string[] }. -`, - prompt: `ORIGINAL:\n${JSON.stringify({ type: 'document', content: originalContent })}\n\nDRAFT:\n${JSON.stringify(draft)}\n\nRespond only with the JSON object.`, - schema: z.object({ - isConforming: z.boolean(), - reasons: z.array(z.string()).default([]), - }), - }); - return object; - } catch (error) { - logger.error('AI format check failed, defaulting to not conforming', { - error: error instanceof Error ? error.message : String(error), - }); - return { isConforming: false, reasons: ['checker_failed'] }; - } -} - -/** - * VISUAL LAYOUT ENFORCEMENT - * Make the draft visually match the template with respect to section title presentation: - * - If the template uses a heading (level 1/2) for a title, ensure the draft uses the same heading level for that title - * - If the template uses a bold paragraph as a title, ensure the draft does the same (single text node, bold mark) - * - After each title, ensure at least one paragraph node exists - */ -function isBoldParagraphTitle(node: Record): boolean { - if ((node as any)?.type !== 'paragraph') return false; - const content = Array.isArray((node as any)?.content) ? ((node as any).content as any[]) : []; - if (content.length !== 1) return false; - const t = content[0]; - if (!t || t.type !== 'text' || typeof t.text !== 'string') return false; - const marks = Array.isArray(t.marks) ? (t.marks as any[]) : []; - return marks.some((m) => m?.type === 'bold'); -} - -function toBoldTitleParagraph(text: string): Record { - return { - type: 'paragraph', - content: [ - { - type: 'text', - text, - marks: [{ type: 'bold' }], - }, - ], - } as Record; -} - -type TitlePattern = { kind: 'heading'; level: number } | { kind: 'boldParagraph' }; - -function getTitlePatternMap(original: Record[]): Map { - const map = new Map(); - for (const node of original) { - const type = (node as any)?.type as string; - if (type === 'heading') { - const level = (node as any)?.attrs?.level; - const text = extractHeadingText(node); - if (text && typeof level === 'number') { - map.set(text.trim().toLowerCase(), { kind: 'heading', level }); - } - } else if (isBoldParagraphTitle(node)) { - const text = extractText(node); - if (text) { - map.set(text.trim().toLowerCase(), { kind: 'boldParagraph' }); - } - } - } - return map; -} - -export function enforceVisualLayoutWithTemplate( - original: Record[], - draft: { type: 'document'; content: Record[] }, -): { type: 'document'; content: Record[] } { - const content = Array.isArray(draft.content) ? draft.content : []; - const patternMap = getTitlePatternMap(original); - if (patternMap.size === 0) return draft; - - const out: Record[] = []; - - for (let i = 0; i < content.length; i += 1) { - const node = content[i] as Record; - const type = (node as any)?.type as string; - let pushed = false; - - if (type === 'heading' || isBoldParagraphTitle(node)) { - const titleText = (type === 'heading' ? extractHeadingText(node) : extractText(node)).trim(); - const key = titleText.toLowerCase(); - const pattern = titleText ? patternMap.get(key) : undefined; - - if (pattern) { - if (pattern.kind === 'heading') { - out.push({ - type: 'heading', - attrs: { level: pattern.level }, - content: [{ type: 'text', text: titleText }], - }); - pushed = true; - } else if (pattern.kind === 'boldParagraph') { - out.push(toBoldTitleParagraph(titleText)); - pushed = true; - } - - if (pushed) { - // Ensure at least one paragraph follows a title - const next = content[i + 1] as Record | undefined; - const nextType = (next as any)?.type as string | undefined; - if (!next || nextType === 'heading') { - out.push({ type: 'paragraph', content: [] }); - } - continue; - } - } - } - - out.push(node); - } - - return { type: 'document', content: out }; -} +import { processTemplate } from './process-policy-template'; // Types export type OrganizationData = { @@ -435,80 +74,6 @@ export async function fetchOrganizationAndPolicy( return { organization, policy, policyTemplate }; } -/** - * Generates the prompt for policy content generation - */ -export async function generatePolicyPrompt( - policyTemplate: FrameworkEditorPolicyTemplate, - contextHub: string, - organization: OrganizationData, - frameworks: FrameworkEditorFramework[], -): Promise { - return generatePrompt({ - contextHub, - policyTemplate, - companyName: organization.name ?? 'Company', - companyWebsite: organization.website ?? 'https://company.com', - frameworks, - }); -} - -/** - * Generates policy content using AI with TipTap JSON schema - */ -export async function generatePolicyContent(prompt: string): Promise<{ - type: 'document'; - content: Record[]; -}> { - try { - const { object } = await generateObject({ - model: gateway(POLICY_MODEL), - output: 'no-schema', - system: `You are an expert at writing security policies. Generate content directly as TipTap JSON format. - -TipTap JSON structure: -- Root: {"type": "document", "content": [array of nodes]} -- Paragraphs: {"type": "paragraph", "content": [text nodes]} -- Headings: {"type": "heading", "attrs": {"level": 1-6}, "content": [text nodes]} -- Lists: {"type": "orderedList"/"bulletList", "content": [listItem nodes]} -- List items: {"type": "listItem", "content": [paragraph nodes]} -- Text: {"type": "text", "text": "content", "marks": [formatting]} -- Bold: {"type": "bold"} in marks array -- Italic: {"type": "italic"} in marks array - -IMPORTANT: Follow ALL formatting instructions in the prompt, implementing them as proper TipTap JSON structures. -Return a JSON object with exactly this shape: {"type": "document", "content": [array of TipTap nodes]}`, - prompt: `Generate a SOC 2 compliant security policy as a complete TipTap JSON document. - -INSTRUCTIONS TO IMPLEMENT IN TIPTAP JSON: -${prompt.replace(/\\n/g, '\n')} - -Return the complete TipTap document following ALL the above requirements using proper TipTap JSON structure.`, - }); - - const parsed = object as { type?: string; content?: unknown }; - if (parsed?.type !== 'document' || !Array.isArray(parsed?.content)) { - throw new Error('AI response did not match expected TipTap document structure'); - } - - return { type: 'document' as const, content: parsed.content as Record[] }; - } catch (aiError) { - logger.error(`Error generating AI content: ${aiError}`); - - if (NoObjectGeneratedError.isInstance(aiError)) { - logger.error( - `NoObjectGeneratedError: ${JSON.stringify({ - cause: aiError.cause, - text: aiError.text, - response: aiError.response, - usage: aiError.usage, - })}`, - ); - } - throw aiError; - } -} - /** * Updates policy content in the database with versioning support. * Creates a new version 1 and sets it as the current (published) version. @@ -615,19 +180,20 @@ export async function updatePolicyInDatabase( export async function processPolicyUpdate(params: UpdatePolicyParams): Promise { const { organizationId, policyId, contextHub, frameworks, memberId } = params; - // Fetch organization and policy data const { organization, policyTemplate } = await fetchOrganizationAndPolicy( organizationId, policyId, ); - // Generate prompt for AI - const prompt = await generatePolicyPrompt(policyTemplate, contextHub, organization, frameworks); + const processedContent = processTemplate({ + content: policyTemplate.content, + companyName: organization.name ?? 'Company', + contextHub, + frameworks, + }); - // Generate new policy content - const updatedContent = await generatePolicyContent(prompt); + const updatedContent = { type: 'document' as const, content: processedContent }; - // Update policy in database with versioning support await updatePolicyInDatabase(policyId, updatedContent.content, memberId); return { diff --git a/apps/app/src/trigger/tasks/onboarding/update-policy.ts b/apps/app/src/trigger/tasks/onboarding/update-policy.ts index d5b4b0d2e3..da53708a0e 100644 --- a/apps/app/src/trigger/tasks/onboarding/update-policy.ts +++ b/apps/app/src/trigger/tasks/onboarding/update-policy.ts @@ -2,10 +2,6 @@ import { logger, metadata, queue, schemaTask } from '@trigger.dev/sdk'; import { z } from 'zod'; import { processPolicyUpdate } from './update-policies-helpers'; -if (!process.env.OPENAI_API_KEY) { - throw new Error('OPENAI_API_KEY is not set'); -} - export const updatePolicyQueue = queue({ name: 'update-policy', concurrencyLimit: 15 }); export const updatePolicy = schemaTask({ From 5840046607e2492e0977eba155b9ccf44674791b Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 12:10:39 +0100 Subject: [PATCH 17/28] fix(onboarding): switch tracker from useRealtimeRun to useRun with polling useRealtimeRun's SSE stream closes when the parent run completes, causing the last few child task metadata updates (counter increments) to never reach the UI. This left counters stuck at e.g. 25/28. Switched to useRun with refreshInterval: 1000ms which polls the run metadata every second. This guarantees the UI eventually reflects all child completions regardless of when the parent run finishes. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../app/(app)/[orgId]/components/OnboardingTracker.tsx | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx b/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx index b87ca4cf44..66c52f9159 100644 --- a/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx +++ b/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx @@ -3,7 +3,7 @@ import { Button } from '@trycompai/ui/button'; import { Card, CardContent } from '@trycompai/ui/card'; import type { Onboarding } from '@db'; -import { useRealtimeRun } from '@trigger.dev/react-hooks'; +import { useRun } from '@trigger.dev/react-hooks'; import { AnimatePresence, motion } from 'framer-motion'; import { AlertTriangle, @@ -65,10 +65,8 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => animationDelay: `${-(Date.now() % 1000)}ms`, }), []); - // useRealtimeRun will automatically get the token from TriggerProvider context - // This gives us real-time updates including metadata changes - const { run, error } = useRealtimeRun(triggerJobId || '', { - enabled: !!triggerJobId, + const { run, error } = useRun(triggerJobId || '', { + refreshInterval: 1000, }); const dismissKey = triggerJobId ? `onboarding-tracker-dismissed:${triggerJobId}` : null; From a3335b3c6fe2e2e9d6b9671d47f20d4d383c711f Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 12:15:57 +0100 Subject: [PATCH 18/28] feat(onboarding): add targeted LLM pass to rewrite policy cue lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some policy templates contain instruction text ("State that...", "Define...", "Add a...") that needs to be converted to direct policy language. The programmatic processor can't handle these since they require judgment. Adds a refineCueLines step that: 1. Scans processed content for cue line patterns 2. If found, batches them into a single LLM call (claude-sonnet-4.6) 3. Splices the rewrites back into the TipTap nodes Only fires for ~10/28 policies that have cue lines. Policies without cue lines skip the LLM entirely and remain instant. Fails soft — if the LLM call errors, the original instruction text is preserved. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../onboarding/update-policies-helpers.ts | 82 ++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/apps/app/src/trigger/tasks/onboarding/update-policies-helpers.ts b/apps/app/src/trigger/tasks/onboarding/update-policies-helpers.ts index 36ec9c5c5d..6024a1f890 100644 --- a/apps/app/src/trigger/tasks/onboarding/update-policies-helpers.ts +++ b/apps/app/src/trigger/tasks/onboarding/update-policies-helpers.ts @@ -1,8 +1,87 @@ +import { createGatewayProvider } from '@ai-sdk/gateway'; import { db, FrameworkEditorFramework, FrameworkEditorPolicyTemplate, type Policy } from '@db/server'; import type { JSONContent } from '@tiptap/react'; import { logger } from '@trigger.dev/sdk'; +import { generateObject } from 'ai'; +import { z } from 'zod'; import { processTemplate } from './process-policy-template'; +const gateway = createGatewayProvider({ + baseURL: process.env.AI_GATEWAY_BASE_URL, +}); + +const CUE_LINE_PATTERN = + /^(State that|Clarify that|Add a |Include a |Specify |List |Note that|Require that|Describe |Define )/; + +type JsonNode = Record; + +/** + * Finds text nodes containing instruction cue lines (e.g. "State that...", + * "Define..."). Returns an array of { path, text } entries where path is + * the index chain to reach the text node in the content tree. + */ +function findCueLines( + nodes: JsonNode[], + path: number[] = [], +): Array<{ path: number[]; text: string }> { + const results: Array<{ path: number[]; text: string }> = []; + for (let i = 0; i < nodes.length; i++) { + const node = nodes[i]; + if (node.type === 'text' && typeof node.text === 'string' && CUE_LINE_PATTERN.test(node.text)) { + results.push({ path: [...path, i], text: node.text }); + } + if (Array.isArray(node.content)) { + results.push(...findCueLines(node.content as JsonNode[], [...path, i])); + } + } + return results; +} + +function setTextAtPath(nodes: JsonNode[], path: number[], newText: string): void { + let current: JsonNode[] = nodes; + for (let i = 0; i < path.length - 1; i++) { + const node = current[path[i]]; + current = node.content as JsonNode[]; + } + const target = current[path[path.length - 1]]; + target.text = newText; +} + +/** + * Rewrites instruction cue lines into direct policy language using a + * targeted LLM call. Only fires when cue lines are detected — most + * policies skip this entirely. + */ +async function refineCueLines( + content: JsonNode[], + policyName: string, +): Promise { + const cueLines = findCueLines(content); + if (cueLines.length === 0) return content; + + try { + const { object } = await generateObject({ + model: gateway('anthropic/claude-sonnet-4.6'), + system: `You rewrite policy template instructions into direct, professional policy language. Each input is an instruction (e.g. "State that...", "Define..."). Return the equivalent text as it should appear in a published security policy — authoritative, concise, no instructional phrasing.`, + prompt: `Policy: "${policyName}"\n\nRewrite each instruction:\n${cueLines.map((c, i) => `${i + 1}. ${c.text}`).join('\n')}`, + schema: z.object({ + rewrites: z.array(z.string()).length(cueLines.length), + }), + }); + + for (let i = 0; i < cueLines.length; i++) { + setTextAtPath(content, cueLines[i].path, object.rewrites[i]); + } + } catch (err) { + logger.warn('Cue line refinement failed; keeping original text', { + policyName, + error: err instanceof Error ? err.message : String(err), + }); + } + + return content; +} + // Types export type OrganizationData = { id: string; @@ -192,7 +271,8 @@ export async function processPolicyUpdate(params: UpdatePolicyParams): Promise

Date: Thu, 7 May 2026 12:33:01 +0100 Subject: [PATCH 19/28] perf(onboarding): switch reranker to gemini-3.1-flash-lite-preview MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The reranker just scores 0-10 relevance on pre-filtered candidates — doesn't need a heavy model. Flash lite should cut the per-call latency that was causing 44s stragglers in the linkage pipeline. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/app/src/lib/rerank-suggestions.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/app/src/lib/rerank-suggestions.ts b/apps/app/src/lib/rerank-suggestions.ts index a35b397e86..1dad5df7c6 100644 --- a/apps/app/src/lib/rerank-suggestions.ts +++ b/apps/app/src/lib/rerank-suggestions.ts @@ -43,7 +43,7 @@ const gateway = createGatewayProvider({ baseURL: process.env.AI_GATEWAY_BASE_URL, }); -const RERANK_MODEL = 'google/gemini-3-flash' as const; +const RERANK_MODEL = 'google/gemini-3.1-flash-lite-preview' as const; const SYSTEM_PROMPT = `You are a GRC analyst evaluating which compliance tasks would meaningfully reduce a specific risk or vendor exposure. From 0d3a2dca50bf68221c55ddd23d5c3d90b830e312 Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 12:39:15 +0100 Subject: [PATCH 20/28] fix(onboarding): use tasks.batchTrigger for vendor/risk mitigations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Same issue as the policy fix — task.batchTrigger() creates independent runs with no parent relationship, so metadata.root was null in the individual mitigation tasks and counter updates never reached the onboarding run's metadata. The tracker showed 0/N permanently. Switched both fan-out tasks to tasks.batchTrigger() which preserves the parent-child hierarchy needed for metadata.root to resolve. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../trigger/tasks/onboarding/generate-risk-mitigation.ts | 7 ++++--- .../trigger/tasks/onboarding/generate-vendor-mitigation.ts | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/apps/app/src/trigger/tasks/onboarding/generate-risk-mitigation.ts b/apps/app/src/trigger/tasks/onboarding/generate-risk-mitigation.ts index b56993af63..3c902e6b0d 100644 --- a/apps/app/src/trigger/tasks/onboarding/generate-risk-mitigation.ts +++ b/apps/app/src/trigger/tasks/onboarding/generate-risk-mitigation.ts @@ -1,5 +1,5 @@ import { RiskStatus, db } from '@db/server'; -import { logger, metadata, queue, tags, task } from '@trigger.dev/sdk'; +import { logger, metadata, queue, tags, task, tasks } from '@trigger.dev/sdk'; import axios from 'axios'; import { createRiskMitigationComment, @@ -114,7 +114,8 @@ export const generateRiskMitigationsForOrg = task({ const policies = policyRows.map((p) => ({ name: p.name, description: p.description })); - await generateRiskMitigation.batchTrigger( + await tasks.batchTrigger( + 'generate-risk-mitigation', risks.map((r) => ({ payload: { organizationId, @@ -122,7 +123,7 @@ export const generateRiskMitigationsForOrg = task({ authorId: author?.id, policies, }, - concurrencyKey: `${organizationId}:${r.id}`, + options: { concurrencyKey: `${organizationId}:${r.id}` }, })), ); diff --git a/apps/app/src/trigger/tasks/onboarding/generate-vendor-mitigation.ts b/apps/app/src/trigger/tasks/onboarding/generate-vendor-mitigation.ts index 38c91d44d0..1418d18131 100644 --- a/apps/app/src/trigger/tasks/onboarding/generate-vendor-mitigation.ts +++ b/apps/app/src/trigger/tasks/onboarding/generate-vendor-mitigation.ts @@ -1,5 +1,5 @@ import { VendorStatus, db } from '@db/server'; -import { logger, metadata, queue, tags, task } from '@trigger.dev/sdk'; +import { logger, metadata, queue, tags, task, tasks } from '@trigger.dev/sdk'; import axios from 'axios'; import { createVendorRiskComment, @@ -116,7 +116,8 @@ export const generateVendorMitigationsForOrg = task({ const policies = policyRows.map((p) => ({ name: p.name, description: p.description })); - await generateVendorMitigation.batchTrigger( + await tasks.batchTrigger( + 'generate-vendor-mitigation', vendors.map((v) => ({ payload: { organizationId, @@ -124,7 +125,7 @@ export const generateVendorMitigationsForOrg = task({ authorId: author?.id, policies, }, - concurrencyKey: `${organizationId}:${v.id}`, + options: { concurrencyKey: `${organizationId}:${v.id}` }, })), ); From 64a2da3dce467b0e9f4e06bfc18ff80e07e2c533 Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 12:43:34 +0100 Subject: [PATCH 21/28] fix: switch onboarding status hooks from useRealtimeRun to useRun useRealtimeRun's SSE stream closes when the parent run completes, leaving metadata stale. This caused the policies page to show "Tailoring your policies" with a spinner even after 28/28 were done. Switched to useRun with refreshInterval: 1000ms in all components that track onboarding run metadata: policies-table, policy onboarding status hook, risk/vendor onboarding status hook, and ToDoOverview. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../app/(app)/[orgId]/overview/components/ToDoOverview.tsx | 6 +++--- .../(overview)/hooks/use-policy-onboarding-status.ts | 6 +++--- .../[orgId]/policies/all/components/policies-table.tsx | 6 +++--- .../[orgId]/risk/(overview)/hooks/use-onboarding-status.ts | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/overview/components/ToDoOverview.tsx b/apps/app/src/app/(app)/[orgId]/overview/components/ToDoOverview.tsx index 3a5da51221..7645fca14b 100644 --- a/apps/app/src/app/(app)/[orgId]/overview/components/ToDoOverview.tsx +++ b/apps/app/src/app/(app)/[orgId]/overview/components/ToDoOverview.tsx @@ -5,7 +5,7 @@ import { Card, CardContent, CardHeader, CardTitle } from '@trycompai/ui/card'; import { ScrollArea } from '@trycompai/ui/scroll-area'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@trycompai/ui/tabs'; import { Policy, Task } from '@db'; -import { useRealtimeRun } from '@trigger.dev/react-hooks'; +import { useRun } from '@trigger.dev/react-hooks'; import { ArrowRight, CheckCircle2, @@ -49,8 +49,8 @@ export function ToDoOverview({ const [isConfirmDialogOpen, setIsConfirmDialogOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); - const { run: onboardingRun } = useRealtimeRun(onboardingTriggerJobId || '', { - enabled: !!onboardingTriggerJobId, + const { run: onboardingRun } = useRun(onboardingTriggerJobId || '', { + refreshInterval: 1000, }); const IN_PROGRESS_STATUSES = [ diff --git a/apps/app/src/app/(app)/[orgId]/policies/(overview)/hooks/use-policy-onboarding-status.ts b/apps/app/src/app/(app)/[orgId]/policies/(overview)/hooks/use-policy-onboarding-status.ts index 9ecf5944f8..b3f8e203d6 100644 --- a/apps/app/src/app/(app)/[orgId]/policies/(overview)/hooks/use-policy-onboarding-status.ts +++ b/apps/app/src/app/(app)/[orgId]/policies/(overview)/hooks/use-policy-onboarding-status.ts @@ -1,6 +1,6 @@ 'use client'; -import { useRealtimeRun } from '@trigger.dev/react-hooks'; +import { useRun } from '@trigger.dev/react-hooks'; import { useMemo } from 'react'; import type { PolicyTailoringStatus } from '../../all/components/policy-tailoring-context'; @@ -20,8 +20,8 @@ export function usePolicyOnboardingStatus( onboardingRunId: string | null | undefined, ) { const shouldSubscribe = Boolean(onboardingRunId); - const { run } = useRealtimeRun(shouldSubscribe ? onboardingRunId! : '', { - enabled: shouldSubscribe, + const { run } = useRun(shouldSubscribe ? onboardingRunId! : '', { + refreshInterval: 1000, }); const itemStatuses = useMemo>(() => { diff --git a/apps/app/src/app/(app)/[orgId]/policies/all/components/policies-table.tsx b/apps/app/src/app/(app)/[orgId]/policies/all/components/policies-table.tsx index 81bd08465a..a86d1deaf4 100644 --- a/apps/app/src/app/(app)/[orgId]/policies/all/components/policies-table.tsx +++ b/apps/app/src/app/(app)/[orgId]/policies/all/components/policies-table.tsx @@ -1,6 +1,6 @@ 'use client'; -import { useRealtimeRun } from '@trigger.dev/react-hooks'; +import { useRun } from '@trigger.dev/react-hooks'; import { Download, Loader2 } from 'lucide-react'; import * as React from 'react'; import { toast } from 'sonner'; @@ -31,8 +31,8 @@ export function PoliciesTable({ promises, onboardingRunId }: PoliciesTableProps) const orgId = params.orgId as string; const shouldSubscribeToRun = Boolean(onboardingRunId); - const { run } = useRealtimeRun(shouldSubscribeToRun ? onboardingRunId! : '', { - enabled: shouldSubscribeToRun, + const { run } = useRun(shouldSubscribeToRun ? onboardingRunId! : '', { + refreshInterval: 1000, }); const policyStatuses = React.useMemo(() => { diff --git a/apps/app/src/app/(app)/[orgId]/risk/(overview)/hooks/use-onboarding-status.ts b/apps/app/src/app/(app)/[orgId]/risk/(overview)/hooks/use-onboarding-status.ts index 5b53b1975f..d3661d0831 100644 --- a/apps/app/src/app/(app)/[orgId]/risk/(overview)/hooks/use-onboarding-status.ts +++ b/apps/app/src/app/(app)/[orgId]/risk/(overview)/hooks/use-onboarding-status.ts @@ -1,6 +1,6 @@ 'use client'; -import { useRealtimeRun } from '@trigger.dev/react-hooks'; +import { useRun } from '@trigger.dev/react-hooks'; import { useMemo } from 'react'; export type OnboardingItemStatus = 'pending' | 'processing' | 'created' | 'assessing' | 'completed'; @@ -15,8 +15,8 @@ export function useOnboardingStatus( itemType: 'risks' | 'vendors', ) { const shouldSubscribe = Boolean(onboardingRunId); - const { run } = useRealtimeRun(shouldSubscribe ? onboardingRunId! : '', { - enabled: shouldSubscribe, + const { run } = useRun(shouldSubscribe ? onboardingRunId! : '', { + refreshInterval: 1000, }); const itemStatuses = useMemo>(() => { From a57fced8401ab181a4dce4ff58bd9f57583fe942 Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 12:47:45 +0100 Subject: [PATCH 22/28] feat(onboarding): split tracker into 6 steps with separate mitigation tracking MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tracker now shows granular progress for each phase: 1. Tailoring Policies (28/28) — expandable per-policy list 2. Creating Vendors — simple checkmark when done 3. Creating Risks — simple checkmark when done 4. Linking to Controls — checkmark when linkage completes 5. Assessing Vendors (0/11) — expandable per-vendor mitigation list 6. Assessing Risks (0/12) — expandable per-risk mitigation list Previously vendor/risk creation and mitigation were conflated into one step, showing 0/N for a long time during linkage even though vendors and risks were already created. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../[orgId]/components/OnboardingTracker.tsx | 396 ++++-------------- .../tasks/onboarding/onboard-organization.ts | 5 +- 2 files changed, 88 insertions(+), 313 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx b/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx index 66c52f9159..bc3be56ebc 100644 --- a/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx +++ b/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx @@ -26,9 +26,12 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { createPortal } from 'react-dom'; const ONBOARDING_STEPS = [ - { key: 'vendors', label: 'Researching Vendors', order: 1 }, - { key: 'risk', label: 'Creating Risks', order: 2 }, - { key: 'policies', label: 'Tailoring Policies', order: 3 }, + { key: 'policies', label: 'Tailoring Policies', order: 1 }, + { key: 'vendors', label: 'Creating Vendors', order: 2 }, + { key: 'risk', label: 'Creating Risks', order: 3 }, + { key: 'linkage', label: 'Linking to Controls', order: 4 }, + { key: 'vendorMitigations', label: 'Assessing Vendors', order: 5 }, + { key: 'riskMitigations', label: 'Assessing Risks', order: 6 }, ] as const; const IN_PROGRESS_STATUSES = [ @@ -129,6 +132,9 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => vendors: false, risk: false, policies: false, + linkage: false, + vendorMitigations: false, + riskMitigations: false, currentStep: null, vendorsTotal: 0, vendorsCompleted: 0, @@ -181,10 +187,20 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => (meta[statusKey] as 'queued' | 'pending' | 'processing' | 'completed') || 'queued'; }); + const vTotal = (meta.vendorsTotal as number) || 0; + const vCompleted = (meta.vendorsCompleted as number) || 0; + const rTotal = (meta.risksTotal as number) || 0; + const rCompleted = (meta.risksCompleted as number) || 0; + const pTotal = (meta.policiesTotal as number) || 0; + const pCompleted = (meta.policiesCompleted as number) || 0; + return { vendors: meta.vendors === true, risk: meta.risk === true, - policies: meta.policies === true, + policies: pTotal > 0 && pCompleted >= pTotal, + linkage: meta.linkage === true, + vendorMitigations: vTotal > 0 && vCompleted >= vTotal, + riskMitigations: rTotal > 0 && rCompleted >= rTotal, currentStep: (meta.currentStep as string) || null, vendorsTotal: (meta.vendorsTotal as number) || 0, vendorsCompleted: (meta.vendorsCompleted as number) || 0, @@ -221,12 +237,11 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => const stepKey = currentStep.key; - // Expand current step if it has items to show - if (stepKey === 'vendors' && stepStatus.vendorsTotal > 0) { + if (stepKey === 'vendorMitigations' && stepStatus.vendorsTotal > 0) { setIsVendorsExpanded(true); setIsRisksExpanded(false); setIsPoliciesExpanded(false); - } else if (stepKey === 'risk' && stepStatus.risksTotal > 0) { + } else if (stepKey === 'riskMitigations' && stepStatus.risksTotal > 0) { setIsVendorsExpanded(false); setIsRisksExpanded(true); setIsPoliciesExpanded(false); @@ -234,6 +249,10 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => setIsVendorsExpanded(false); setIsRisksExpanded(false); setIsPoliciesExpanded(true); + } else { + setIsVendorsExpanded(false); + setIsRisksExpanded(false); + setIsPoliciesExpanded(false); } }, [currentStep?.key, stepStatus.vendorsTotal, stepStatus.risksTotal, stepStatus.policiesTotal]); @@ -507,151 +526,56 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) =>

{ONBOARDING_STEPS.map((step) => { const isCurrent = currentStep?.key === step.key; - const isVendorsStep = step.key === 'vendors'; - const isRisksStep = step.key === 'risk'; - const isPoliciesStep = step.key === 'policies'; - - // Determine completion based on unique counts, not raw metadata totals - const vendorsCompleted = - uniqueVendorsCounts.total > 0 && - uniqueVendorsCounts.completed >= uniqueVendorsCounts.total; - const risksCompleted = - stepStatus.risksTotal > 0 && stepStatus.risksCompleted >= stepStatus.risksTotal; - const policiesCompleted = - stepStatus.policiesTotal > 0 && - stepStatus.policiesCompleted >= stepStatus.policiesTotal; + const isCompleted = stepStatus[step.key as keyof typeof stepStatus] === true; - const isCompleted = - (isVendorsStep && vendorsCompleted) || - (isRisksStep && risksCompleted) || - (isPoliciesStep && policiesCompleted); - - // Check if any items are actively being processed (not just queued) - const vendorsProcessing = Object.values(stepStatus.vendorsStatus || {}).some( - (status) => status === 'processing' || status === 'assessing', - ); - const risksProcessing = Object.values(stepStatus.risksStatus || {}).some( - (status) => status === 'processing' || status === 'assessing', - ); - const policiesProcessing = Object.values(stepStatus.policiesStatus || {}).some( - (status) => status === 'processing', + const isProcessing = !isCompleted && ( + (step.key === 'policies' && Object.values(stepStatus.policiesStatus).some((s) => s === 'processing')) || + (step.key === 'vendorMitigations' && Object.values(stepStatus.vendorsStatus).some((s) => s === 'processing' || s === 'assessing')) || + (step.key === 'riskMitigations' && Object.values(stepStatus.risksStatus).some((s) => s === 'processing' || s === 'assessing')) ); - // Show spinner if actively processing, even if not the current step - const isActivelyProcessing = - (isVendorsStep && vendorsProcessing) || - (isRisksStep && risksProcessing) || - (isPoliciesStep && policiesProcessing); + const stepIcon = isCompleted ? ( + + ) : isCurrent || isProcessing ? ( + + ) : ( +
+ ); - const vendorsQueued = - stepStatus.vendorsCompleted < stepStatus.vendorsTotal && - stepStatus.vendorsTotal > 0 && - !vendorsProcessing; - const risksQueued = - stepStatus.risksCompleted < stepStatus.risksTotal && - stepStatus.risksTotal > 0 && - !risksProcessing; - const policiesQueued = - stepStatus.policiesCompleted < stepStatus.policiesTotal && - stepStatus.policiesTotal > 0 && - !policiesProcessing; + const stepTextClass = `text-sm ${ + isCompleted ? 'text-primary' : isCurrent || isProcessing ? 'text-primary font-medium' : 'text-muted-foreground' + }`; - // Vendors step with expandable dropdown - if (isVendorsStep && stepStatus.vendorsTotal > 0) { + // Expandable step with per-entity items + if (step.key === 'vendorMitigations' && stepStatus.vendorsTotal > 0) { return (
- - - {/* Expanded vendor list */} {isVendorsExpanded && uniqueVendorsInfo.length > 0 && ( - +
{uniqueVendorsInfo.map((vendor) => { - const vendorStatus = stepStatus.vendorsStatus[vendor.id] || 'pending'; - const isVendorCompleted = vendorStatus === 'completed'; - const isVendorProcessing = vendorStatus === 'processing'; - const isVendorQueued = vendorStatus === 'pending'; - + const status = stepStatus.vendorsStatus[vendor.id] || 'pending'; + const done = status === 'completed'; + const processing = status === 'processing'; const content = ( <> - {isVendorCompleted ? ( - - ) : isVendorProcessing ? ( - - ) : isVendorQueued ? ( - - ) : ( -
- )} - - {vendor.name} - + {done ? : processing ? : } + {vendor.name} ); - return (
- {isVendorCompleted && orgId ? ( - - {content} - - ) : ( - content - )} + {done && orgId ? {content} : content}
); })} @@ -662,99 +586,35 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => ); } - // Risks step with expandable dropdown - if (isRisksStep && stepStatus.risksTotal > 0) { + if (step.key === 'riskMitigations' && stepStatus.risksTotal > 0) { return (
- - - {/* Expanded risk list */} {isRisksExpanded && stepStatus.risksInfo.length > 0 && ( - +
{stepStatus.risksInfo.map((risk) => { - const riskStatus = stepStatus.risksStatus[risk.id] || 'pending'; - const isRiskCompleted = riskStatus === 'completed'; - const isRiskProcessing = riskStatus === 'processing'; - + const status = stepStatus.risksStatus[risk.id] || 'pending'; + const done = status === 'completed'; + const processing = status === 'processing'; const content = ( <> - {isRiskCompleted ? ( - - ) : isRiskProcessing ? ( - - ) : ( -
- )} - - {risk.name} - + {done ? : processing ? :
} + {risk.name} ); - return (
- {isRiskCompleted && orgId ? ( - - {content} - - ) : ( - content - )} + {done && orgId ? {content} : content}
); })} @@ -765,104 +625,36 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => ); } - if (isPoliciesStep && stepStatus.policiesTotal > 0) { - // Policies step with expandable dropdown + if (step.key === 'policies' && stepStatus.policiesTotal > 0) { return (
- - - {/* Expanded policy list */} {isPoliciesExpanded && stepStatus.policiesInfo.length > 0 && ( - +
{stepStatus.policiesInfo.map((policy) => { - const policyStatus = stepStatus.policiesStatus[policy.id] || 'queued'; - const isPolicyCompleted = policyStatus === 'completed'; - const isPolicyProcessing = policyStatus === 'processing'; - const isPolicyQueued = - policyStatus === 'queued' || policyStatus === 'pending'; - + const status = stepStatus.policiesStatus[policy.id] || 'queued'; + const done = status === 'completed'; + const processing = status === 'processing'; + const queued = status === 'queued' || status === 'pending'; const content = ( <> - {isPolicyCompleted ? ( - - ) : isPolicyProcessing ? ( - - ) : isPolicyQueued ? ( - - ) : ( -
- )} - - {policy.name} - + {done ? : processing ? : queued ? :
} + {policy.name} ); - return (
- {isPolicyCompleted && orgId ? ( - - {content} - - ) : ( - content - )} + {done && orgId ? {content} : content}
); })} @@ -873,29 +665,11 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => ); } - // Regular step + // Simple step row (creation, linkage) return (
- {isCompleted ? ( - - ) : isCurrent ? ( - - ) : policiesQueued ? ( - - ) : ( -
- )} - - {step.label} - + {stepIcon} + {step.label}
); })} diff --git a/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts b/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts index e47a12d98f..dc98f64a00 100644 --- a/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts +++ b/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts @@ -108,7 +108,7 @@ export const onboardOrganization = task({ await updateOrganizationPolicies(payload.organizationId, questionsAndAnswers, frameworks); // Extract vendors + risks in parallel (both are independent LLM calls). - metadata.set('currentStep', 'Researching Vendors...'); + metadata.set('currentStep', 'Creating Vendors...'); const [vendors, risks] = await Promise.all([ (async () => { @@ -165,7 +165,7 @@ export const onboardOrganization = task({ // tasks/controls and produces grounded plans. Fan-out for both happens // after this gate. Fails-soft: a timeout/error degrades to today's // behavior. (ENG-221 + Cubic findings #7 / #26.) - metadata.set('currentStep', 'Linking risks to tasks...'); + metadata.set('currentStep', 'Linking to Controls...'); try { await tasks.triggerAndWait( 'link-risks-and-vendors-to-work', @@ -182,6 +182,7 @@ export const onboardOrganization = task({ // Fan-out vendor + risk mitigations now that linkage has populated the // grounding context for both kinds of entities. Done in parallel — // each fan-out task itself batchTriggers per-entity children. + metadata.set('currentStep', 'Assessing Vendors...'); await Promise.all([ tasks.trigger( 'generate-vendor-mitigations-for-org', From b16e3114263444afdba26f78542af8bcac10df92 Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 12:51:35 +0100 Subject: [PATCH 23/28] fix(onboarding): keep task alive for mitigations, redirect via metadata flag Trigger.dev silently drops metadata writes to completed runs. Since the main task completed before mitigations started, all per-entity status updates (vendor_${id}_status, risksCompleted, etc.) were lost. Fix: switch mitigations from tasks.trigger (fire-and-forget) to tasks.triggerAndWait so the main task stays alive and metadata.root remains writable. The user still redirects early via a readyForDashboard metadata flag set before mitigations begin. The redirect page now checks metadata.readyForDashboard instead of only run.status === COMPLETED, so the user hits the dashboard in ~27s while the task continues running for another ~90s to complete mitigations with live progress tracking. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../go/[id]/components/onboarding-status.tsx | 7 +++++- .../tasks/onboarding/onboard-organization.ts | 25 ++++++++++--------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/apps/app/src/app/(app)/setup/go/[id]/components/onboarding-status.tsx b/apps/app/src/app/(app)/setup/go/[id]/components/onboarding-status.tsx index e2aaf4f39d..28edd653d6 100644 --- a/apps/app/src/app/(app)/setup/go/[id]/components/onboarding-status.tsx +++ b/apps/app/src/app/(app)/setup/go/[id]/components/onboarding-status.tsx @@ -16,8 +16,13 @@ export function OnboardingStatus({ runId }: { runId: string }) { useEffect(() => { if (run?.status === 'COMPLETED') { router.replace('/'); + return; } - }, [run?.status, router]); + const meta = run?.metadata as Record | undefined; + if (meta?.readyForDashboard === true) { + router.replace('/'); + } + }, [run?.status, run?.metadata, router]); return (
diff --git a/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts b/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts index dc98f64a00..34001454f3 100644 --- a/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts +++ b/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts @@ -179,32 +179,33 @@ export const onboardOrganization = task({ }); } + // Redirect the user to the dashboard now — policies, vendors, and + // risks are created. The task stays alive so child metadata writes + // (mitigation progress) keep landing on the root run. + metadata.set('readyForDashboard', true); + await db.onboarding.update({ + where: { organizationId: payload.organizationId }, + data: { triggerJobCompleted: true }, + }); + // Fan-out vendor + risk mitigations now that linkage has populated the - // grounding context for both kinds of entities. Done in parallel — - // each fan-out task itself batchTriggers per-entity children. + // grounding context for both kinds of entities. triggerAndWait keeps + // this task alive so metadata.root stays writable for child tasks. metadata.set('currentStep', 'Assessing Vendors...'); await Promise.all([ - tasks.trigger( + tasks.triggerAndWait( 'generate-vendor-mitigations-for-org', { organizationId: payload.organizationId }, ), - tasks.trigger( + tasks.triggerAndWait( 'generate-risk-mitigations-for-org', { organizationId: payload.organizationId }, ), ]); metadata.set('currentStep', 'Finalizing...'); - - // Mark onboarding as completed in metadata metadata.set('completed', true); - // Mark onboarding as completed in database - await db.onboarding.update({ - where: { organizationId: payload.organizationId }, - data: { triggerJobCompleted: true }, - }); - logger.info(`Created ${vendors.length} vendors`); logger.info(`Onboarding completed for organization ${payload.organizationId}`); } catch (error) { From c67cbfe829b3b9c3e5e6d17fcd04301020b78470 Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 12:53:19 +0100 Subject: [PATCH 24/28] fix(onboarding): show vendor/risk count on creation steps Creating Vendors and Creating Risks steps now show the total count (e.g. "Creating Vendors 11") so the user sees how many were created. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../app/(app)/[orgId]/components/OnboardingTracker.tsx | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx b/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx index bc3be56ebc..14bf22176a 100644 --- a/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx +++ b/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx @@ -666,10 +666,16 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => } // Simple step row (creation, linkage) + const count = step.key === 'vendors' ? uniqueVendorsCounts.total + : step.key === 'risk' ? stepStatus.risksTotal + : null; return (
{stepIcon} - {step.label} + {step.label} + {count !== null && count > 0 && ( + {count} + )}
); })} From 7dd06dad05c7d7319304a9d6eda1f3faa2321689 Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 12:54:08 +0100 Subject: [PATCH 25/28] fix(onboarding): sequential triggerAndWait for mitigations MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Trigger.dev doesn't support Promise.all with triggerAndWait. Changed to sequential awaits — vendor mitigations finish first, then risk mitigations. Both fan-out tasks now use batchTriggerAndWait so they stay alive until all children complete, keeping the full task hierarchy alive for metadata.root writes. Individual mitigations still run with full queue concurrency (50) within each fan-out. Risk mitigations start queueing while vendor mitigations drain, so the sequential overhead is minimal. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../onboarding/generate-risk-mitigation.ts | 2 +- .../onboarding/generate-vendor-mitigation.ts | 2 +- .../tasks/onboarding/onboard-organization.ts | 27 ++++++++++--------- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/apps/app/src/trigger/tasks/onboarding/generate-risk-mitigation.ts b/apps/app/src/trigger/tasks/onboarding/generate-risk-mitigation.ts index 3c902e6b0d..733847323b 100644 --- a/apps/app/src/trigger/tasks/onboarding/generate-risk-mitigation.ts +++ b/apps/app/src/trigger/tasks/onboarding/generate-risk-mitigation.ts @@ -114,7 +114,7 @@ export const generateRiskMitigationsForOrg = task({ const policies = policyRows.map((p) => ({ name: p.name, description: p.description })); - await tasks.batchTrigger( + await tasks.batchTriggerAndWait( 'generate-risk-mitigation', risks.map((r) => ({ payload: { diff --git a/apps/app/src/trigger/tasks/onboarding/generate-vendor-mitigation.ts b/apps/app/src/trigger/tasks/onboarding/generate-vendor-mitigation.ts index 1418d18131..8e16b540ae 100644 --- a/apps/app/src/trigger/tasks/onboarding/generate-vendor-mitigation.ts +++ b/apps/app/src/trigger/tasks/onboarding/generate-vendor-mitigation.ts @@ -116,7 +116,7 @@ export const generateVendorMitigationsForOrg = task({ const policies = policyRows.map((p) => ({ name: p.name, description: p.description })); - await tasks.batchTrigger( + await tasks.batchTriggerAndWait( 'generate-vendor-mitigation', vendors.map((v) => ({ payload: { diff --git a/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts b/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts index 34001454f3..bf2a805c1f 100644 --- a/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts +++ b/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts @@ -188,20 +188,21 @@ export const onboardOrganization = task({ data: { triggerJobCompleted: true }, }); - // Fan-out vendor + risk mitigations now that linkage has populated the - // grounding context for both kinds of entities. triggerAndWait keeps - // this task alive so metadata.root stays writable for child tasks. + // Fan-out vendor + risk mitigations. triggerAndWait keeps this task + // alive so metadata.root stays writable for child tasks. Sequential + // because Trigger.dev doesn't support parallel waits, but both + // fan-outs use batchTriggerAndWait internally so their children + // run with full queue concurrency. metadata.set('currentStep', 'Assessing Vendors...'); - await Promise.all([ - tasks.triggerAndWait( - 'generate-vendor-mitigations-for-org', - { organizationId: payload.organizationId }, - ), - tasks.triggerAndWait( - 'generate-risk-mitigations-for-org', - { organizationId: payload.organizationId }, - ), - ]); + await tasks.triggerAndWait( + 'generate-vendor-mitigations-for-org', + { organizationId: payload.organizationId }, + ); + metadata.set('currentStep', 'Assessing Risks...'); + await tasks.triggerAndWait( + 'generate-risk-mitigations-for-org', + { organizationId: payload.organizationId }, + ); metadata.set('currentStep', 'Finalizing...'); metadata.set('completed', true); From 27a7b8d5fdb96d2e39a09e47b034e8ec9b6c3317 Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 12:56:40 +0100 Subject: [PATCH 26/28] fix(onboarding): link completed assessments to treatment-plan tab Vendor and risk links in the tracker now include ?tab=treatment-plan so clicking a completed assessment goes directly to the generated mitigation plan. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/app/(app)/[orgId]/components/OnboardingTracker.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx b/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx index 14bf22176a..1c2deb6736 100644 --- a/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx +++ b/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx @@ -575,7 +575,7 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => ); return (
- {done && orgId ? {content} : content} + {done && orgId ? {content} : content}
); })} @@ -614,7 +614,7 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => ); return (
- {done && orgId ? {content} : content} + {done && orgId ? {content} : content}
); })} From cd50157f0671404b87773d02e744c81b1b96ade8 Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 13:00:16 +0100 Subject: [PATCH 27/28] fix: address cubic review findings in template processor and linkage MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit P1: Nested {{#if}} inside a false block could leak content. When skipDepth > 0 (inside a skipped block), any new {{#if}} now always increments skipDepth without evaluating its condition or processing content. Previously a true inner condition would emit its content even though the outer block was false. P2: Text node trim removed meaningful whitespace between adjacent TipTap inline nodes, causing merged words. Removed the trim() call from processTextNode — placeholder replacement and conditional evaluation don't introduce extra whitespace that needs cleaning. P2: Parallel risk+vendor matching could double peak LLM concurrency. Reduced MATCH_CONCURRENCY from 32 to 16 so both sides combined stay under ~32 concurrent rerank calls. Co-Authored-By: Claude Opus 4.7 (1M context) --- apps/app/src/lib/embedding/run-linkage.ts | 17 ++++------------- .../tasks/onboarding/process-policy-template.ts | 5 ++++- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/apps/app/src/lib/embedding/run-linkage.ts b/apps/app/src/lib/embedding/run-linkage.ts index 1b58be3e15..138cca6f7c 100644 --- a/apps/app/src/lib/embedding/run-linkage.ts +++ b/apps/app/src/lib/embedding/run-linkage.ts @@ -109,19 +109,10 @@ const AUTONOMOUS_FINAL_TOP_K = 8; const AUTONOMOUS_MIN_RERANK_SCORE = 5; const AUTONOMOUS_MIN_LINKS_FLOOR = 3; -// How many risks/vendors to match concurrently in the bulk onboarding path. -// Each iteration makes 1 vector query (Upstash) + 1 OpenAI rerank call + 1 -// Prisma update — typical wall-clock per iteration is 3–10 seconds (the -// rerank LLM call dominates). With 32 in-flight at once a 20-entity -// onboarding finishes in roughly one batch, well within gpt-5-mini / -// Upstash rate limits. -// -// NOTE: this is in-process concurrency on a single trigger.dev task. The -// natural next step (true fan-out per entity using `task.batchTrigger`) -// would unlock trigger.dev's queue-level concurrency (50), but requires -// passing the embedded-task metadata to children rather than rebuilding -// taskById per child. Filed as a follow-up. -const MATCH_CONCURRENCY = 32; +// Risk and vendor matching run in parallel, so this limit applies to +// EACH side independently. Keep it at 16 so both sides combined stay +// under ~32 concurrent LLM rerank calls. +const MATCH_CONCURRENCY = 16; async function mapWithConcurrency( items: T[], diff --git a/apps/app/src/trigger/tasks/onboarding/process-policy-template.ts b/apps/app/src/trigger/tasks/onboarding/process-policy-template.ts index 3928d648f8..4296331e0b 100644 --- a/apps/app/src/trigger/tasks/onboarding/process-policy-template.ts +++ b/apps/app/src/trigger/tasks/onboarding/process-policy-template.ts @@ -79,7 +79,6 @@ function processTextNode(node: JsonNode, vars: Record, flags: Re let text = node.text; text = processInlineConditionals(text, flags); text = replacePlaceholdersInText(text, vars); - text = text.trim(); if (!text) return null; return { ...node, text }; @@ -128,6 +127,10 @@ export function processContentArray( } if (openMatch) { + if (skipDepth > 0) { + skipDepth++; + continue; + } const flag = openMatch[1]; const isTrue = flags[flag] ?? false; if (!isTrue) { From da3618d930976a018c8e86858dfdd248e28844fd Mon Sep 17 00:00:00 2001 From: Mariano Date: Thu, 7 May 2026 13:13:20 +0100 Subject: [PATCH 28/28] fix(onboarding): fix currentStep mismatch and treat assessing as active - Initial currentStep was still 'Researching Vendors...' which doesn't match any tracker label. Changed to 'Tailoring Policies...' since that's the first step now. - Vendor/risk item rows only treated 'processing' as active (spinner). The 'assessing' status (set during creation) is also an active state and now shows a spinner instead of a clock. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../(app)/[orgId]/components/OnboardingTracker.tsx | 12 ++++++------ .../trigger/tasks/onboarding/onboard-organization.ts | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx b/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx index 1c2deb6736..f2c5dd82d1 100644 --- a/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx +++ b/apps/app/src/app/(app)/[orgId]/components/OnboardingTracker.tsx @@ -566,11 +566,11 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => {uniqueVendorsInfo.map((vendor) => { const status = stepStatus.vendorsStatus[vendor.id] || 'pending'; const done = status === 'completed'; - const processing = status === 'processing'; + const active = status === 'processing' || status === 'assessing'; const content = ( <> - {done ? : processing ? : } - {vendor.name} + {done ? : active ? : } + {vendor.name} ); return ( @@ -605,11 +605,11 @@ export const OnboardingTracker = ({ onboarding }: { onboarding: Onboarding }) => {stepStatus.risksInfo.map((risk) => { const status = stepStatus.risksStatus[risk.id] || 'pending'; const done = status === 'completed'; - const processing = status === 'processing'; + const active = status === 'processing' || status === 'assessing'; const content = ( <> - {done ? : processing ? :
} - {risk.name} + {done ? : active ? :
} + {risk.name} ); return ( diff --git a/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts b/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts index bf2a805c1f..478e05b475 100644 --- a/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts +++ b/apps/app/src/trigger/tasks/onboarding/onboard-organization.ts @@ -27,7 +27,7 @@ export const onboardOrganization = task({ await tags.add([`org:${payload.organizationId}`]); // Initialize metadata for real-time tracking - metadata.set('currentStep', 'Researching Vendors...'); + metadata.set('currentStep', 'Tailoring Policies...'); metadata.set('vendors', false); metadata.set('risk', false); metadata.set('policies', false);