Skip to content

Release: revenue attribution, users LTV + email lookup, feedback pipeline, slack agent reliability and chart images#541

Open
izadoesdev wants to merge 136 commits into
mainfrom
staging
Open

Release: revenue attribution, users LTV + email lookup, feedback pipeline, slack agent reliability and chart images#541
izadoesdev wants to merge 136 commits into
mainfrom
staging

Conversation

@izadoesdev

@izadoesdev izadoesdev commented Jul 4, 2026

Copy link
Copy Markdown
Member

What's in this release

Users page

  • LTV column: lifetime revenue per visitor (refunds netted), computed via a revenue join in profile_list
  • Email search: exact-match lookup via the deterministic email hash, jumps straight to the profile; works against encrypted PII; pending state and error-vs-miss handling hardened
  • Trait filter value picker distinguishes loading from empty; filter chip styling deduped; trait distribution ranked per key

Revenue attribution

  • invoice.paid / invoice.payment_succeeded handling: recurring subscription payments now attribute to sessions and identified users. Metadata is read from the invoice, subscription_details, and parent.subscription_details (covers Autumn and newer Stripe API versions where payment intents carry no metadata)
  • Duplicate prevention: invoice rows reuse the matching payment-intent row's transaction id (same rule as the query-time dedup, applied at ingestion), so one payment stays one row
  • Attribution carry-forward across event ordering: succeeded/failed/refund events no longer replace an attributed row with an unattributed version; refunds inherit attribution from the original payment
  • Failed invoices stay at zero amount to preserve the revenue sum invariant (attribution still carried through failed and refund handlers)
  • Five duplicated ClickHouse insert blocks collapsed into one helper (net -54 lines)
  • Setup UI and docs list invoice.paid as a required webhook event

Feedback pipeline

  • Feedback rows now carry source, website, conversation id, and metadata; website_id fkey indexed
  • Shared submission service in @databuddy/services with source-aware Slack alerts
  • submit_feedback agent tool for dashboard chat and the Slack bot, with consent-aware prompting and a single source of truth for the prompt rules
  • Vague-report gate: vague complaints route to clarification instead of silent submission
  • feedback-preview chat component with send and receipt modes
  • Eval cases for tool behavior and vague-complaint routing

Slack agent

  • Deploy-safe shutdown: active runs are aborted with a user-facing notice and run handlers settle before the telemetry drain flushes. Fixes runs dying with an unexplained "Something went wrong" and their agent_run events never reaching Axiom when a deploy landed mid-answer
  • 5-minute run timeout: hung runs fail visibly with slack_run_timed_out telemetry instead of sitting on "Thinking..." until the next deploy kills them
  • Chart images: new @databuddy/charts package renders the agent's chart components (line, area, bar, stacked bar, pie, donut) to dashboard-themed PNGs server-side (ECharts SSR + resvg, LT Superior fonts, dark/light design tokens). Slack answers upload up to 3 charts into the thread and fall back to the existing text tables if rendering or upload fails. Activating uploads requires adding the files:write scope to the Slack app

Agent & AI

  • list_profile_traits tool: trait key/value distribution with profile counts, guiding agents to trait:<key> segmentation before querying
  • Public query errors sanitized behind an allowlist across get_data, MCP tools, and raw SQL paths; telemetry gated on the exported sanitized constant; error messaging keyed on error type, not input shape
  • Trait filters resolved and validated centrally; unknown query types reject explicitly
  • Trait segmentation eval cases

Dashboard & misc

  • has_active_subscription trait dropped (redundant with plan)
  • OpenAI Ads signup attribution improved and the route hardened
  • Session guard no longer revokes sessions on transient fetch failures; owner-scoped persisted-cache storage refinements
  • Vitals e2e test asserts the real payload shape (was vacuously passing)
  • Prose style cleanup in tool descriptions and payments docs

Custom event page path (new)

  • Custom events now capture the page path they fired on: the tracker sends path and basket persists it to custom_events.path. The column existed but was never populated (0 of 144,409 custom events over the prior 30 days had a path).
  • Tracker rebuilt and deployed to the CDN (v5).

Hash-route pageviews (new)

  • screen_view events now include the URL hash fragment in path when trackHashChanges is enabled. Previously buildPagePath used pathname only and dropped the hash. This also fixes a pre-existing failing tracker E2E test.

Insights becomes Findings (new)

  • Detection, investigation, and delivery pipeline overhaul across the insights app, @databuddy/ai, rpc, redis, db, shared, and evals.
  • Dashboard renames "Insights" to "Findings" and refreshes the feed UI.

Misc (new)

  • Corrected the public tracker-size claim to 11 KB gzipped across marketing and comparison copy (was stale at 10 KB; the bundle already gzipped to ~11 KB).

@vercel

vercel Bot commented Jul 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
dashboard (staging) Ready Ready Preview, Comment Jul 14, 2026 12:46pm
databuddy-status Ready Ready Preview, Comment Jul 14, 2026 12:46pm
documentation (staging) Ready Ready Preview, Comment Jul 14, 2026 12:46pm

@coderabbitai

coderabbitai Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: f2784af9-ea8c-4dfa-9bc0-be670aa04acd

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch staging

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@unkey-deploy

unkey-deploy Bot commented Jul 4, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Unkey Deploy

Name Status Preview Inspect Updated (UTC)
api (preview) Ready Visit Preview Inspect Jul 14, 2026 12:45pm

@greptile-apps

greptile-apps Bot commented Jul 4, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR centralizes AI query trait-filter resolution into a new trait-filters.ts module, adds filter-field validation for MCP batch requests, and removes the authClient.signOut() call in the session guard to prevent spurious sign-outs on transient fetch failures.

  • Trait filter centralization: resolveRequestTraitFilters and invalidFilterFieldError now live in one place and are re-used by executeQuery, executeBatch, and the MCP batch-request builder, replacing per-caller ad-hoc logic.
  • Session guard fix: The guard now redirects directly to /login without calling signOut(); this stops a transient null-session response from permanently revoking a still-valid session on the server.
  • Error surfacing: MCP batch results now return the real error string (r.error) instead of the redacted "Query failed", giving the AI agent actionable context for self-correction.

Confidence Score: 4/5

The core refactor is sound — trait resolution is cleanly centralized, index bookkeeping in executeBatch is correct, and the session guard fix prevents accidental sign-outs. The two flagged items are edge-case inefficiencies that do not break the happy path.

The unknown-type guard in resolveRequestTraitFilters lets an unnecessary identity-service round-trip happen before an unknown query type fails downstream, and the raw ClickHouse error strings now reach the LLM context without any sanitization pass. Neither causes incorrect data or broken flows on valid inputs, but both deserve a follow-up before the code sees heavy use with novel query types or noisy ClickHouse errors.

packages/ai/src/query/trait-filters.ts (unknown-type guard) and packages/ai/src/ai/mcp/agent-tools.ts (raw error forwarding) are worth a second look.

Important Files Changed

Filename Overview
packages/ai/src/query/trait-filters.ts New module centralizing trait-filter resolution and field validation; logic is clean, but unknown query types bypass the profile_id guard and trigger an unnecessary identity service call.
packages/ai/src/query/batch-executor.ts Trait resolution moved into executeBatch; index bookkeeping for traitFailures is correct and the fast-path single-query shortcut is properly gated on zero failures.
packages/ai/src/ai/mcp/agent-tools.ts Error surfacing upgraded from a static "Query failed" string to the real error; could leak internal ClickHouse error details to the LLM context.
apps/dashboard/components/providers/session-guard.tsx Removes signOut() before redirect to /login, preventing irreversible session revocation on transient network failures; intentional and correct.
packages/ai/src/ai/tools/get-data.ts Per-item trait resolution removed; now delegates to executeQuery which handles it centrally; error branch now differentiates trait vs. generic failures correctly.
packages/ai/src/ai/mcp/mcp-utils.ts Adds invalidFilterFieldError() call between type resolution and request push, giving the MCP layer early rejection before execution.
packages/ai/src/query/index.ts executeQuery now validates filter fields and resolves trait filters before building; validation runs twice for executeBatch callers (once at MCP layer, once here) but no functional impact.
packages/ai/src/query/trait-filters.test.ts New test file with good coverage of happy path, org-scoped rejection, unknown-type rejection, and field allowlist logic.
packages/ai/src/query/builders/sessions.ts Adds profile_id/anonymous_id to allowedFilters and threads filterConditions/filterParams into custom SQL queries for session builders.

Sequence Diagram

%%{init: {'theme': 'neutral'}}%%
sequenceDiagram
    participant AI as AI Agent / Tool
    participant MCP as MCP buildBatchQueryRequests
    participant EX as executeBatch
    participant TF as resolveRequestTraitFilters
    participant ID as Identity Service (resolveTraitSegment)
    participant CH as ClickHouse

    AI->>MCP: "queries[] with trait:<key> filters"
    MCP->>MCP: invalidFilterFieldError() per query
    MCP-->>AI: invalid[] (bad field) + requests[] (ok)
    AI->>EX: executeBatch(requests)
    loop per request (parallel)
        EX->>TF: resolveRequestTraitFilters(req)
        alt has trait filters
            TF->>ID: resolveTraitSegment(projectId, traitFilters)
            ID-->>TF: profile_id[]
            TF-->>EX: req with profile_id filter
        else no trait filters
            TF-->>EX: original req (no-op)
        end
    end
    EX->>CH: union query (successful requests)
    CH-->>EX: rows
    EX-->>AI: BatchResult[] (with errors for failures)
Loading
%%{init: {'theme': 'base', 'themeVariables': {"darkMode": true, "background": "#0d1117", "primaryColor": "#21262d", "primaryTextColor": "#e6edf3", "primaryBorderColor": "#8b949e", "lineColor": "#8b949e", "textColor": "#e6edf3", "edgeLabelBackground": "#161b22", "actorBkg": "#21262d", "actorBorder": "#8b949e", "actorTextColor": "#e6edf3", "actorLineColor": "#8b949e", "signalColor": "#8b949e", "signalTextColor": "#e6edf3", "noteBkgColor": "#373320", "noteBorderColor": "#d4a72c", "noteTextColor": "#f0e6c0", "labelBoxBkgColor": "#21262d", "labelBoxBorderColor": "#8b949e", "labelTextColor": "#e6edf3", "loopTextColor": "#e6edf3", "activationBkgColor": "#30363d", "activationBorderColor": "#8b949e"}}}%%
sequenceDiagram
    participant AI as AI Agent / Tool
    participant MCP as MCP buildBatchQueryRequests
    participant EX as executeBatch
    participant TF as resolveRequestTraitFilters
    participant ID as Identity Service (resolveTraitSegment)
    participant CH as ClickHouse

    AI->>MCP: "queries[] with trait:<key> filters"
    MCP->>MCP: invalidFilterFieldError() per query
    MCP-->>AI: invalid[] (bad field) + requests[] (ok)
    AI->>EX: executeBatch(requests)
    loop per request (parallel)
        EX->>TF: resolveRequestTraitFilters(req)
        alt has trait filters
            TF->>ID: resolveTraitSegment(projectId, traitFilters)
            ID-->>TF: profile_id[]
            TF-->>EX: req with profile_id filter
        else no trait filters
            TF-->>EX: original req (no-op)
        end
    end
    EX->>CH: union query (successful requests)
    CH-->>EX: rows
    EX-->>AI: BatchResult[] (with errors for failures)
Loading

Reviews (1): Last reviewed commit: "chore(staging): merge main" | Re-trigger Greptile

Comment on lines +50 to +55
const config = QueryBuilders[request.type];
if (config && !isFilterFieldAllowed(config, "profile_id")) {
throw new TraitFilterError(
`Trait filters are not supported for ${request.type}. Query types that support them accept a profile_id filter.`
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Unnecessary identity service call for unknown query types. When config is undefined (unknown query type), the guard if (config && !isFilterFieldAllowed(config, "profile_id")) is a no-op — config is falsy so the throw is skipped. The function then calls resolveTraitSegment (a network round-trip to the identity service) before the query ultimately fails downstream with an "unknown type" error. The guard should also reject when the config is missing entirely.

Suggested change
const config = QueryBuilders[request.type];
if (config && !isFilterFieldAllowed(config, "profile_id")) {
throw new TraitFilterError(
`Trait filters are not supported for ${request.type}. Query types that support them accept a profile_id filter.`
);
}
const config = QueryBuilders[request.type];
if (!config || !isFilterFieldAllowed(config, "profile_id")) {
throw new TraitFilterError(
`Trait filters are not supported for ${request.type}. Query types that support them accept a profile_id filter.`
);
}

Comment thread packages/ai/src/ai/mcp/agent-tools.ts Outdated
data: r.data,
rowCount: r.data.length,
...(r.error && { error: "Query failed" }),
...(r.error && { error: r.error }),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Raw errors now forwarded to AI agent context. Switching from the static "Query failed" to r.error means ClickHouse-level error messages (which can include table names, column names, and query fragments) are forwarded verbatim to the LLM context. For filter-validation errors this is intentional and useful, but a transient ClickHouse failure or a malformed query could expose internal schema details. Consider forwarding r.error as-is for known structured errors (e.g. TraitFilterError messages or the filter-field error format) and falling back to a sanitized string for raw database errors.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 521 files

Confidence score: 2/5

  • In apps/basket/src/hooks/auth.ts, _resolveOwnerId now appears to fail hard on member-query errors instead of returning null, which can make owner lookup failures take down the whole auth lookup path and cause avoidable sign-in/access regressions—restore the previous fallback behavior (capture + null) before merging.
  • In apps/api/src/http/errors.ts, production responses now unconditionally include raw Elysia validation messages, which can bypass the existing exposeStructured sanitization intent and leak internal validation detail to clients—reapply the production gating so only sanitized fields are exposed.
  • In apps/dashboard/components/layout/organization-selector.tsx, switching orgs calls queryClient.clear(), which can wipe unrelated cache and pending mutations (billing/session/flags) and introduce cross-page state loss or flaky UX after an org switch—replace this with targeted invalidation/removal for org-scoped queries.
  • In apps/docs/public/pricing.md, the dropped (Scale) label and removed Scale vs Enterprise cross-reference can mislead customers if entitlement configs still use scale, increasing support and sales confusion—restore the tier naming/cross-reference to match current product configuration before release.
Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/api/src/http/errors.ts">

<violation number="1" location="apps/api/src/http/errors.ts:83">
P2: Raw Elysia validation messages are exposed unconditionally in production responses, bypassing the existing `exposeStructured` sanitization gate that protects `why`, `fix`, and `link`.

In production, `exposeStructured` is `false` for `ValidationError` (it is not an `EvlogError`), so the top-level `error` field safely falls back to "Invalid request". However, the `details` array containing up to 20 raw `issue.summary` / `issue.message` strings is still included. Those messages can leak schema field names, custom validation logic, header/cookie expectations, or reflected input text that the `getSafeErrorMessage` policy is meant to suppress.

Consider gating `details` behind the same `exposeStructured` check, or introducing an explicit allowlist/gate for which validation messages are safe to expose publicly.</violation>
</file>

<file name="apps/basket/src/hooks/auth.ts">

<violation number="1" location="apps/basket/src/hooks/auth.ts:56">
P1: Owner lookup failures are now fatal to the entire website auth lookup, which is a regression for ingest resilience. `_resolveOwnerId` previously returned `null` on member-query failures (after `captureError`); now it throws `websiteLookupUnavailable`, and that error propagates through `getWebsiteByIdWithOwnerCached` and `getWebsiteByIdV2`. Since `ownerId` is typed as nullable (`string | null`) and all callers treat it as optional enrichment—not a hard auth gate—`ownerId` resolution should remain a soft failure. Keeping it soft means a transient member-table issue won't take down `/track` and `/identify` requests for otherwise-valid websites. Consider returning `null` from `_resolveOwnerId` on non-EvlogError DB failures so the website lookup still succeeds, or at least catching the owner-resolution error inside `getWebsiteByIdWithOwnerCached` and returning the website with `ownerId: null` so the optional enrichment doesn't block the auth path.</violation>
</file>

<file name="apps/dashboard/components/layout/organization-selector.tsx">

<violation number="1" location="apps/dashboard/components/layout/organization-selector.tsx:164">
P2: Organization switch now uses `queryClient.clear()`, which wipes the entire React Query cache including billing state, session metadata, feature flags, and any pending mutations. The previous code was more targeted: it removed only org-scoped query keys and explicitly invalidated the active-organization query so `useOrganizationsContext` promptly picked up the new org. Global `clear()` makes that refresh implicit and risks transient empty-state flicker during the redirect to `/websites`. Prefer restoring scoped invalidation — remove org-scoped keys and explicitly invalidate the active-organization query instead.</violation>
</file>

<file name="apps/docs/public/pricing.md">

<violation number="1" location="apps/docs/public/pricing.md:35">
P2: The product-limits table header lost the `(Scale)` annotation and the definitions section dropped the `Scale vs Enterprise` cross-reference. If internal tier IDs and entitlement configs still use `scale` — which the codebase suggests via `pricing/_pricing/estimator-scale.ts` — this removal removes an important breadcrumb for keeping docs and code aligned. Future edits to either side can drift silently. Consider keeping a visible reminder (e.g., restoring `Enterprise (Scale)` in the header or a short footnote) so reviewers know the internal plan id is still `scale`.</violation>
</file>

Note: This PR contains a large number of files. cubic only reviews up to 200 files per PR, so some files may not have been reviewed. cubic prioritizes the most important files to review.
Shadow auto-approve: would not auto-approve because issues were found.

Re-trigger cubic

message: "Failed to fetch workspace owner",
organizationId,
});
throw basketErrors.websiteLookupUnavailable();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Owner lookup failures are now fatal to the entire website auth lookup, which is a regression for ingest resilience. _resolveOwnerId previously returned null on member-query failures (after captureError); now it throws websiteLookupUnavailable, and that error propagates through getWebsiteByIdWithOwnerCached and getWebsiteByIdV2. Since ownerId is typed as nullable (string | null) and all callers treat it as optional enrichment—not a hard auth gate—ownerId resolution should remain a soft failure. Keeping it soft means a transient member-table issue won't take down /track and /identify requests for otherwise-valid websites. Consider returning null from _resolveOwnerId on non-EvlogError DB failures so the website lookup still succeeds, or at least catching the owner-resolution error inside getWebsiteByIdWithOwnerCached and returning the website with ownerId: null so the optional enrichment doesn't block the auth path.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/basket/src/hooks/auth.ts, line 56:

<comment>Owner lookup failures are now fatal to the entire website auth lookup, which is a regression for ingest resilience. `_resolveOwnerId` previously returned `null` on member-query failures (after `captureError`); now it throws `websiteLookupUnavailable`, and that error propagates through `getWebsiteByIdWithOwnerCached` and `getWebsiteByIdV2`. Since `ownerId` is typed as nullable (`string | null`) and all callers treat it as optional enrichment—not a hard auth gate—`ownerId` resolution should remain a soft failure. Keeping it soft means a transient member-table issue won't take down `/track` and `/identify` requests for otherwise-valid websites. Consider returning `null` from `_resolveOwnerId` on non-EvlogError DB failures so the website lookup still succeeds, or at least catching the owner-resolution error inside `getWebsiteByIdWithOwnerCached` and returning the website with `ownerId: null` so the optional enrichment doesn't block the auth path.</comment>

<file context>
@@ -45,10 +46,14 @@ function _resolveOwnerId(
 				message: "Failed to fetch workspace owner",
 				organizationId,
 			});
+			throw basketErrors.websiteLookupUnavailable();
 		}
 
</file context>

Comment thread apps/dashboard/app/(auth)/login/magic-sent/page.tsx
Comment thread apps/dashboard/lib/user-facing-error.ts
Comment thread apps/dashboard/app/(main)/websites/[id]/funnels/page.tsx Outdated
Comment thread apps/docs/content/docs/api/mcp.mdx Outdated
Comment thread apps/dashboard/app/(auth)/login/forgot/page.tsx
Checkout **Enterprise** maps to **Scale** entitlements in-app (same column below).

| | Free | Hobby | Pro | Enterprise (Scale) |
| | Free | Hobby | Pro | Enterprise |

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The product-limits table header lost the (Scale) annotation and the definitions section dropped the Scale vs Enterprise cross-reference. If internal tier IDs and entitlement configs still use scale — which the codebase suggests via pricing/_pricing/estimator-scale.ts — this removal removes an important breadcrumb for keeping docs and code aligned. Future edits to either side can drift silently. Consider keeping a visible reminder (e.g., restoring Enterprise (Scale) in the header or a short footnote) so reviewers know the internal plan id is still scale.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/docs/public/pricing.md, line 35:

<comment>The product-limits table header lost the `(Scale)` annotation and the definitions section dropped the `Scale vs Enterprise` cross-reference. If internal tier IDs and entitlement configs still use `scale` — which the codebase suggests via `pricing/_pricing/estimator-scale.ts` — this removal removes an important breadcrumb for keeping docs and code aligned. Future edits to either side can drift silently. Consider keeping a visible reminder (e.g., restoring `Enterprise (Scale)` in the header or a short footnote) so reviewers know the internal plan id is still `scale`.</comment>

<file context>
@@ -32,45 +32,33 @@ Overage = events **above** the monthly included amount. Cumulative overage is ch
-Checkout **Enterprise** maps to **Scale** entitlements in-app (same column below).
-
-| | Free | Hobby | Pro | Enterprise (Scale) |
+| | Free | Hobby | Pro | Enterprise |
 | --- | --- | --- | --- | --- |
 | Funnels | 1 | 5 | 50 | Unlimited |
</file context>
Suggested change
| | Free | Hobby | Pro | Enterprise |
+| | Free | Hobby | Pro | Enterprise (Scale) |

Comment thread apps/dashboard/app/(main)/billing/actions/cancel-feedback-action.ts Outdated
Comment thread apps/dashboard/components/feature-gate.tsx Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 7 files (changes from recent commits).

Shadow auto-approve: would not auto-approve. Auto-approval blocked by 51 unresolved issues from previous reviews.

Re-trigger cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/rpc/src/routers/insights.ts">

<violation number="1" location="packages/rpc/src/routers/insights.ts:337">
P2: The `.catch(() => false)` wrapper around `withWorkspace` in `getById` and `related` swallows every rejection, including database and infrastructure errors. After reviewing `resolveWorkspace` in `packages/rpc/src/procedures/with-workspace.ts`, it calls `getMemberRole` and `context.getBilling()` which can throw on DB/connectivity failures. When that happens the endpoint will return `{ success: true, insight: null }` (or an empty array) instead of a real error, masking outages and bypassing telemetry.

Preserve the anti-enumeration behavior for actual permission denials, but rethrow unexpected/infrastructure errors so they surface properly. A simple approach is to inspect the rejection and only swallow the expected `rpcError.forbidden` / `rpcError.unauthorized` kinds.</violation>
</file>

Shadow auto-approve: would not auto-approve because issues were found.
Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread packages/rpc/src/routers/insights.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Shadow auto-approve: would not auto-approve. Auto-approval blocked by 46 unresolved issues from previous reviews.

Re-trigger cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 22 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/dashboard/lib/safe-callback.ts">

<violation number="1" location="apps/dashboard/lib/safe-callback.ts:1">
P1: `safeCallbackPath` can be bypassed via URL-parser-stripped whitespace, leading to an open redirect. A value like `/\t/attacker.example` passes the `startsWith("/")` and `!startsWith("//")` checks because the tab character sits between the slashes. When the browser or Next.js router parses the URL later, the tab is stripped (per the WHATWG URL Standard) and the path becomes `//attacker.example`, which is a protocol-relative external URL. Consider rejecting tab, LF, and CR characters explicitly before the prefix test, or parsing the string against a known base origin with `new URL()` and verifying the resulting pathname has not collapsed into a protocol-relative form.</violation>
</file>

Shadow auto-approve: would not auto-approve because issues were found.
Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

@@ -0,0 +1,14 @@
export function safeCallbackPath(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: safeCallbackPath can be bypassed via URL-parser-stripped whitespace, leading to an open redirect. A value like /\t/attacker.example passes the startsWith("/") and !startsWith("//") checks because the tab character sits between the slashes. When the browser or Next.js router parses the URL later, the tab is stripped (per the WHATWG URL Standard) and the path becomes //attacker.example, which is a protocol-relative external URL. Consider rejecting tab, LF, and CR characters explicitly before the prefix test, or parsing the string against a known base origin with new URL() and verifying the resulting pathname has not collapsed into a protocol-relative form.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/dashboard/lib/safe-callback.ts, line 1:

<comment>`safeCallbackPath` can be bypassed via URL-parser-stripped whitespace, leading to an open redirect. A value like `/\t/attacker.example` passes the `startsWith("/")` and `!startsWith("//")` checks because the tab character sits between the slashes. When the browser or Next.js router parses the URL later, the tab is stripped (per the WHATWG URL Standard) and the path becomes `//attacker.example`, which is a protocol-relative external URL. Consider rejecting tab, LF, and CR characters explicitly before the prefix test, or parsing the string against a known base origin with `new URL()` and verifying the resulting pathname has not collapsed into a protocol-relative form.</comment>

<file context>
@@ -0,0 +1,14 @@
+export function safeCallbackPath(
+	callback: string | null | undefined,
+	fallback = "/websites"
</file context>

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Shadow auto-approve: would not auto-approve. Auto-approval blocked by 22 unresolved issues from previous reviews.

Re-trigger cubic

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

12 issues found across 91 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/insights/src/resolution.ts">

<violation number="1" location="apps/insights/src/resolution.ts:23">
P2: Active custom-event findings now bypass recovery tracking and become stale after the TTL despite an ongoing `custom_event:*` signal. Keep `custom_event_spike` in a transient family (and match its signals) so active findings remain open and disappeared signals recover.</violation>
</file>

<file name="apps/insights/src/detection.ts">

<violation number="1" location="apps/insights/src/detection.ts:651">
P2: Custom-event anomalies will no longer produce Findings: `custom_events_discovery` was removed from this detection pass and no replacement detector emits `custom_event:*` signals. Retain that family or move its logic into the new engine so event additions, volume shifts, and disappearances remain detectable.</violation>
</file>

<file name="packages/ai/src/ai/insights/product-context.ts">

<violation number="1" location="packages/ai/src/ai/insights/product-context.ts:131">
P3: Malformed or legacy funnel step data can make product-evidence collection throw at `step.name`, because this cast validates only the array, not its elements. Filter to non-null object steps before mapping.</violation>
</file>

<file name="apps/insights/src/delivery.ts">

<violation number="1" location="apps/insights/src/delivery.ts:440">
P2: Rate-limited Slack deliveries with `Retry-After` above five seconds retry too early and exhaust all three attempts. Preserve Slack’s requested delay rather than capping it so the durable effect can succeed after the limit window.</violation>
</file>

<file name="packages/ai/src/ai/insights/validate.ts">

<violation number="1" location="packages/ai/src/ai/insights/validate.ts:784">
P1: The deterministic decision validator is not connected to a production investigation flow, so this release will keep using existing behavior and never generate these validated insights. Wire `validateInvestigationDecision` into the investigation result path before returning/persisting a decision.</violation>
</file>

<file name="apps/insights/src/jobs.ts">

<violation number="1" location="apps/insights/src/jobs.ts:261">
P2: Concurrent delivery can run the same website generation twice because this predicate accepts an already `running` item, so both workers receive a row and proceed. Use an atomic claim/lease transition that only one worker can acquire before calling `generateWebsiteInsights`.</violation>
</file>

<file name="packages/ai/package.json">

<violation number="1" location="packages/ai/package.json:21">
P3: Export path `./insights/evidence-reader` maps to a source file that wasn't renamed to match. The file is still at `src/ai/tools/insights-agent-tools.ts` while every other export here follows the pattern where the path key matches the actual file location (`./insights/fetch-context` → `src/ai/insights/fetch-context.ts`, etc.). Someone searching for `evidence-reader` by filename or looking in `src/ai/insights/` won't find it. Consider renaming the source file to `evidence-reader.ts` and moving it to `src/ai/insights/` to match the export path and align with the convention.</violation>
</file>

<file name="package.json">

<violation number="1" location="package.json:67">
P2: `eval:insights` is missing `dotenv --` prefix, inconsistent with all other `eval*` scripts. This means `.env` variables won't be loaded before the script runs, so any dependencies requiring env vars (e.g., DB connections via `investigateWebsiteWithSources`) may fail or misbehave. Add `dotenv --` before `bun run` to match the project convention and ensure env variables are available.</violation>
</file>

<file name="apps/insights/src/generation.ts">

<violation number="1" location="apps/insights/src/generation.ts:756">
P1: Partial detector scans can still close an open finding when the selected signal returns `monitor` or `not_a_problem`. Gate `retiredSignalKey` on `analysis.detectionComplete` too, so incomplete scans remain non-destructive.</violation>
</file>

<file name="apps/insights/src/index.ts">

<violation number="1" location="apps/insights/src/index.ts:25">
P2: `readBooleanEnv` changes the default when `INSIGHTS_WORKER_ENABLED` is unset: old code defaulted to `true` (enabled), new code defaults to `false` (disabled). If any environment relies on the implicit default rather than setting this var explicitly, the worker will silently stop running after deploy. All documented configs (`.env.example`, selfhost, CI) do set it explicitly, so this is mainly a risk for unconfigured or ad-hoc deployments. Consider a separate `readBooleanEnv('...', true)` overload or verify the env var is set in every deployment that needs the worker.</violation>
</file>

<file name="apps/insights/src/funnel-detection.test.ts">

<violation number="1" location="apps/insights/src/funnel-detection.test.ts:615">
P2: The `aborts sibling workers when one definition fails fatally` test creates unhandled promise rejections. Workers that are blocked on `await blocked` resume after `release?.()` and throw `signal.reason` on promises that were part of the already-settled `Promise.all` in `mapWithConcurrency`. These throws become unhandled rejections that could cause the test to fail or produce flaky results depending on how `bun:test` collects unhandled rejections. Consider using the abort signal to short-circuit the `work` function instead, or guard the worker loop with a check that skips work on settled `Promise.all`.</violation>
</file>

<file name="packages/ai/src/query/builders/vitals.ts">

<violation number="1" location="packages/ai/src/query/builders/vitals.ts:142">
P2: Vitals overview percentiles can become inaccurate under traffic spikes because the deterministic sampler key can repeat across many rows. Using a higher-cardinality determinator (include identity/context columns) would keep deterministic output without the duplicate-key bias.</violation>
</file>

Shadow auto-approve: would not auto-approve because issues were found.
Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

return generatedInsightSchema.parse(insight);
}

export function validateInvestigationDecision(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: The deterministic decision validator is not connected to a production investigation flow, so this release will keep using existing behavior and never generate these validated insights. Wire validateInvestigationDecision into the investigation result path before returning/persisting a decision.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/ai/src/ai/insights/validate.ts, line 784:

<comment>The deterministic decision validator is not connected to a production investigation flow, so this release will keep using existing behavior and never generate these validated insights. Wire `validateInvestigationDecision` into the investigation result path before returning/persisting a decision.</comment>

<file context>
@@ -348,257 +680,272 @@ function toGeneratedInsight(
-
-export function validateInvestigationSubmission(
-	input: ValidateInvestigationInput
+export function validateInvestigationDecision(
+	input: unknown
 ): InvestigationValidationResult {
</file context>

Comment on lines +756 to +760
retiredSignalKey: retiredSignalKeyForOutcome({
disposition: analysis.decision?.disposition,
hasInsight: analysis.insight !== null,
signalKey: analysis.signal?.signalKey,
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: Partial detector scans can still close an open finding when the selected signal returns monitor or not_a_problem. Gate retiredSignalKey on analysis.detectionComplete too, so incomplete scans remain non-destructive.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/insights/src/generation.ts, line 756:

<comment>Partial detector scans can still close an open finding when the selected signal returns `monitor` or `not_a_problem`. Gate `retiredSignalKey` on `analysis.detectionComplete` too, so incomplete scans remain non-destructive.</comment>

<file context>
@@ -694,152 +673,127 @@ export async function generateWebsiteInsights(
 			detectedSignals: analysis.detectedSignals,
-			canRecover: analysis.status !== "no_data",
+			canRecover: analysis.status !== "no_data" && analysis.detectionComplete,
+			retiredSignalKey: retiredSignalKeyForOutcome({
+				disposition: analysis.decision?.disposition,
+				hasInsight: analysis.insight !== null,
</file context>
Suggested change
retiredSignalKey: retiredSignalKeyForOutcome({
disposition: analysis.decision?.disposition,
hasInsight: analysis.insight !== null,
signalKey: analysis.signal?.signalKey,
}),
retiredSignalKey: analysis.detectionComplete
? retiredSignalKeyForOutcome({
disposition: analysis.decision?.disposition,
hasInsight: analysis.insight !== null,
signalKey: analysis.signal?.signalKey,
})
: undefined,

@@ -16,26 +20,20 @@ type InsightFamily =
| "vitals"
| "traffic"
| "engagement"
| "conversion"
| "custom_event";
| "conversion";

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Active custom-event findings now bypass recovery tracking and become stale after the TTL despite an ongoing custom_event:* signal. Keep custom_event_spike in a transient family (and match its signals) so active findings remain open and disappeared signals recover.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/insights/src/resolution.ts, line 23:

<comment>Active custom-event findings now bypass recovery tracking and become stale after the TTL despite an ongoing `custom_event:*` signal. Keep `custom_event_spike` in a transient family (and match its signals) so active findings remain open and disappeared signals recover.</comment>

<file context>
@@ -18,26 +20,20 @@ type InsightFamily =
 	| "engagement"
-	| "conversion"
-	| "custom_event";
+	| "conversion";
 
 const TRANSIENT_TYPE_FAMILY: Record<string, InsightFamily> = {
</file context>

query("vitals_overview", previousFrom, previousTo),
query("custom_events_discovery", currentFrom, currentTo),
query("custom_events_discovery", previousFrom, previousTo),
const [summary, errors, revenue, vitals] = await Promise.all([

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Custom-event anomalies will no longer produce Findings: custom_events_discovery was removed from this detection pass and no replacement detector emits custom_event:* signals. Retain that family or move its logic into the new engine so event additions, volume shifts, and disappearances remain detectable.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/insights/src/detection.ts, line 651:

<comment>Custom-event anomalies will no longer produce Findings: `custom_events_discovery` was removed from this detection pass and no replacement detector emits `custom_event:*` signals. Retain that family or move its logic into the new engine so event additions, volume shifts, and disappearances remain detectable.</comment>

<file context>
@@ -482,33 +643,57 @@ async function detectWow(
-		query("vitals_overview", previousFrom, previousTo),
-		query("custom_events_discovery", currentFrom, currentTo),
-		query("custom_events_discovery", previousFrom, previousTo),
+	const [summary, errors, revenue, vitals] = await Promise.all([
+		readDetectorFamily({
+			abortSignal,
</file context>

? seconds * 1000
: SLACK_RATE_LIMIT_FALLBACK_MS;
return (
Math.min(requested, SLACK_RATE_LIMIT_MAX_WAIT_MS) +

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Rate-limited Slack deliveries with Retry-After above five seconds retry too early and exhaust all three attempts. Preserve Slack’s requested delay rather than capping it so the durable effect can succeed after the limit window.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/insights/src/delivery.ts, line 440:

<comment>Rate-limited Slack deliveries with `Retry-After` above five seconds retry too early and exhaust all three attempts. Preserve Slack’s requested delay rather than capping it so the durable effect can succeed after the limit window.</comment>

<file context>
@@ -409,138 +424,208 @@ export function buildThreadBlocks(
+			? seconds * 1000
+			: SLACK_RATE_LIMIT_FALLBACK_MS;
+	return (
+		Math.min(requested, SLACK_RATE_LIMIT_MAX_WAIT_MS) +
+		Math.floor(random() * 250)
+	);
</file context>

@@ -21,7 +22,7 @@ const environment =
process.env.APP_ENV ??
process.env.RAILWAY_ENVIRONMENT_NAME ??
(process.env.NODE_ENV === "development" ? "development" : "production");
const workerEnabled = process.env.INSIGHTS_WORKER_ENABLED !== "false";
const workerEnabled = readBooleanEnv("INSIGHTS_WORKER_ENABLED");

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: readBooleanEnv changes the default when INSIGHTS_WORKER_ENABLED is unset: old code defaulted to true (enabled), new code defaults to false (disabled). If any environment relies on the implicit default rather than setting this var explicitly, the worker will silently stop running after deploy. All documented configs (.env.example, selfhost, CI) do set it explicitly, so this is mainly a risk for unconfigured or ad-hoc deployments. Consider a separate readBooleanEnv('...', true) overload or verify the env var is set in every deployment that needs the worker.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/insights/src/index.ts, line 25:

<comment>`readBooleanEnv` changes the default when `INSIGHTS_WORKER_ENABLED` is unset: old code defaulted to `true` (enabled), new code defaults to `false` (disabled). If any environment relies on the implicit default rather than setting this var explicitly, the worker will silently stop running after deploy. All documented configs (`.env.example`, selfhost, CI) do set it explicitly, so this is mainly a risk for unconfigured or ad-hoc deployments. Consider a separate `readBooleanEnv('...', true)` overload or verify the env var is set in every deployment that needs the worker.</comment>

<file context>
@@ -21,7 +22,7 @@ const environment =
 	process.env.RAILWAY_ENVIRONMENT_NAME ??
 	(process.env.NODE_ENV === "development" ? "development" : "production");
-const workerEnabled = process.env.INSIGHTS_WORKER_ENABLED !== "false";
+const workerEnabled = readBooleanEnv("INSIGHTS_WORKER_ENABLED");
 const DRAIN_TIMEOUT_MS = 10_000;
 
</file context>

release = resolve;
});

const detection = detectFunnelGoalSignals(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: The aborts sibling workers when one definition fails fatally test creates unhandled promise rejections. Workers that are blocked on await blocked resume after release?.() and throw signal.reason on promises that were part of the already-settled Promise.all in mapWithConcurrency. These throws become unhandled rejections that could cause the test to fail or produce flaky results depending on how bun:test collects unhandled rejections. Consider using the abort signal to short-circuit the work function instead, or guard the worker loop with a check that skips work on settled Promise.all.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/insights/src/funnel-detection.test.ts, line 615:

<comment>The `aborts sibling workers when one definition fails fatally` test creates unhandled promise rejections. Workers that are blocked on `await blocked` resume after `release?.()` and throw `signal.reason` on promises that were part of the already-settled `Promise.all` in `mapWithConcurrency`. These throws become unhandled rejections that could cause the test to fail or produce flaky results depending on how `bun:test` collects unhandled rejections. Consider using the abort signal to short-circuit the `work` function instead, or guard the worker loop with a check that skips work on settled `Promise.all`.</comment>

<file context>
@@ -175,4 +292,375 @@ describe("detectFunnelGoalSignals", () => {
+			release = resolve;
+		});
+
+		const detection = detectFunnelGoalSignals(
+			PARAMS,
+			TODAY,
</file context>

quantilesTDigest(0.50, 0.75, 0.90, 0.95, 0.99)(metric_value) as _q,
quantilesDeterministic(0.50, 0.75, 0.90, 0.95, 0.99)(
metric_value,
cityHash64(tuple(timestamp, metric_value))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Vitals overview percentiles can become inaccurate under traffic spikes because the deterministic sampler key can repeat across many rows. Using a higher-cardinality determinator (include identity/context columns) would keep deterministic output without the duplicate-key bias.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/ai/src/query/builders/vitals.ts, line 142:

<comment>Vitals overview percentiles can become inaccurate under traffic spikes because the deterministic sampler key can repeat across many rows. Using a higher-cardinality determinator (include identity/context columns) would keep deterministic output without the duplicate-key bias.</comment>

<file context>
@@ -137,7 +137,10 @@ export const VitalsBuilders: Record<string, SimpleQueryConfig> = {
-						quantilesTDigest(0.50, 0.75, 0.90, 0.95, 0.99)(metric_value) as _q,
+						quantilesDeterministic(0.50, 0.75, 0.90, 0.95, 0.99)(
+							metric_value,
+							cityHash64(tuple(timestamp, metric_value))
+						) as _q,
 						avg(metric_value) as avg_value,
</file context>
Suggested change
cityHash64(tuple(timestamp, metric_value))
cityHash64(tuple(timestamp, session_id, anonymous_id, path, metric_value))

Comment on lines +131 to +133
const definitionSteps = Array.isArray(funnel.steps)
? (funnel.steps as Record<string, unknown>[])
: [];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Malformed or legacy funnel step data can make product-evidence collection throw at step.name, because this cast validates only the array, not its elements. Filter to non-null object steps before mapping.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/ai/src/ai/insights/product-context.ts, line 131:

<comment>Malformed or legacy funnel step data can make product-evidence collection throw at `step.name`, because this cast validates only the array, not its elements. Filter to non-null object steps before mapping.</comment>

<file context>
@@ -85,20 +107,46 @@ async function getFunnelsSummary(
+	const analyticsSteps = Array.isArray(analytics.steps_analytics)
+		? (analytics.steps_analytics as Record<string, unknown>[])
+		: [];
+	const definitionSteps = Array.isArray(funnel.steps)
+		? (funnel.steps as Record<string, unknown>[])
+		: [];
</file context>
Suggested change
const definitionSteps = Array.isArray(funnel.steps)
? (funnel.steps as Record<string, unknown>[])
: [];
const definitionSteps = Array.isArray(funnel.steps)
? funnel.steps.filter(
(step): step is Record<string, unknown> =>
typeof step === "object" && step !== null
)
: [];

Comment thread packages/ai/package.json
@@ -18,10 +18,8 @@
"./agents/types": "./src/ai/agents/types.ts",
"./config/context": "./src/ai/config/context.ts",
"./config/models": "./src/ai/config/models.ts",
"./insights/dedupe": "./src/ai/insights/dedupe.ts",
"./insights/evidence-reader": "./src/ai/tools/insights-agent-tools.ts",

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Export path ./insights/evidence-reader maps to a source file that wasn't renamed to match. The file is still at src/ai/tools/insights-agent-tools.ts while every other export here follows the pattern where the path key matches the actual file location (./insights/fetch-contextsrc/ai/insights/fetch-context.ts, etc.). Someone searching for evidence-reader by filename or looking in src/ai/insights/ won't find it. Consider renaming the source file to evidence-reader.ts and moving it to src/ai/insights/ to match the export path and align with the convention.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/ai/package.json, line 21:

<comment>Export path `./insights/evidence-reader` maps to a source file that wasn't renamed to match. The file is still at `src/ai/tools/insights-agent-tools.ts` while every other export here follows the pattern where the path key matches the actual file location (`./insights/fetch-context` → `src/ai/insights/fetch-context.ts`, etc.). Someone searching for `evidence-reader` by filename or looking in `src/ai/insights/` won't find it. Consider renaming the source file to `evidence-reader.ts` and moving it to `src/ai/insights/` to match the export path and align with the convention.</comment>

<file context>
@@ -18,6 +18,7 @@
     "./agents/types": "./src/ai/agents/types.ts",
     "./config/context": "./src/ai/config/context.ts",
     "./config/models": "./src/ai/config/models.ts",
+    "./insights/evidence-reader": "./src/ai/tools/insights-agent-tools.ts",
     "./insights/fetch-context": "./src/ai/insights/fetch-context.ts",
     "./insights/validate": "./src/ai/insights/validate.ts",
</file context>

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant