Skip to content

feat: Add Discord integration - #24

Open
demobdev wants to merge 2 commits into
mainfrom
feature/discord-integration
Open

feat: Add Discord integration#24
demobdev wants to merge 2 commits into
mainfrom
feature/discord-integration

Conversation

@demobdev

@demobdev demobdev commented Jun 20, 2026

Copy link
Copy Markdown
Owner

Adds Discord webhook integration.

Summary by CodeRabbit

Release Notes

  • New Features

    • Added Analytics dashboard with velocity trends and issue status metrics
    • Introduced Discord webhook integration for notifications
    • Added AI-powered duplicate detection and triage suggestions during issue creation
    • Enabled AI search directly from command palette
    • Integrated GitHub webhook triggers for automated PR review loops
  • Improvements

    • Enhanced loop management with pause/delete controls and detailed run history
    • Added analytics section to main navigation
    • Improved async action feedback with loading indicators and notifications

@vercel

vercel Bot commented Jun 20, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
open-grove Ready Ready Preview, Comment Jun 20, 2026 6:10pm

@coderabbitai

coderabbitai Bot commented Jun 20, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@demobdev, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 37 minutes and 45 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan refill rate.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, the refill rate gradually slows as usage increases. The highest same-day bursts are limited more strictly.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e7d72c5c-8557-403c-aac7-b3e339ab12ef

📥 Commits

Reviewing files that changed from the base of the PR and between cc7f09e and 79104e3.

📒 Files selected for processing (5)
  • app/(app)/[orgSlug]/skills/page.tsx
  • components/ai/composer.tsx
  • components/loops/create-loop-dialog.tsx
  • components/loops/loop-templates.tsx
  • convex/seed.ts
📝 Walkthrough

Walkthrough

This PR implements Phase 7 features: a new analytics dashboard with Recharts visualizations backed by a Convex getIssueStats query, AI-powered command palette search routing to the AI agent page via ?q=, real-time duplicate detection and triage suggestions in the create-issue dialog, a Discord webhook integration (Convex module, HTTP routes, settings UI) mirroring Slack, GitHub PR events triggering reviewer loops via a new internalStartLoop mutation, and bound vectorTools injection across the loop orchestrator and automations agent.

Changes

Analytics Dashboard

Layer / File(s) Summary
Analytics Convex query
convex/analytics.ts
Adds getIssueStats org-scoped query computing statusCounts, total, and a 30-day velocityData series from the issues table.
Analytics page UI and sidebar nav
app/(app)/[orgSlug]/analytics/page.tsx, components/shell/app-sidebar.tsx, package.json
New AnalyticsPage renders skeleton placeholders while loading, then a velocity line chart and a status bar chart via Recharts. Sidebar gains an "Analytics" nav item with Zap icon. recharts and date-fns added to dependencies.

AI Features: Command Palette Search, Initial Query, Issue Triage

Layer / File(s) Summary
New AI triage actions and labels query
convex/agent/triage.ts, convex/agent/data.ts
Adds findDuplicatesFromText (vector search + similarity scoring) and suggestTriageFromText (LLM with constrained JSON schema) public actions, plus a listOrgLabels internal query.
AI triage wiring in CreateIssueDialog
components/issues/create-issue-dialog.tsx
800ms debounced useEffect calls both triage actions concurrently when dialog is open; renders duplicate list, AI priority suggestion banner, and loading indicator.
Command palette AI search and initial query flow
components/commands/command-provider.tsx, components/ai/ai-agent-page.tsx, app/(app)/[orgSlug]/ai/page.tsx
Command palette gains controlled search state, "Ask OpenGrove AI" CommandEmpty UI, and runAiCommand navigating to /{orgSlug}/ai?q=.... AiAgentPage accepts initialQuery, auto-sends it once, then strips the query param via router.replace.

Discord and Slack Webhook Integration

Layer / File(s) Summary
Discord Convex module
convex/discord.ts
Adds handleDiscordWebhook (PING/slash-command dispatch), saveDiscordWebhook/getDiscordWebhook (connectedRepos persistence with discord: prefix), postToDiscord, and sendTestMessage.
Slack internal webhook handler
convex/slack.ts
Adds handleSlackWebhook internalAction handling /opengrove create <title> and URL verification challenges.
HTTP routes for Slack and Discord
convex/http.ts
Registers POST /slack-webhook (form-encoded or JSON) and POST /discord-webhook (JSON, 400 on parse error) routes delegating to their respective internal handlers.
Discord integration settings UI
app/(app)/[orgSlug]/settings/integrations/page.tsx, package.json
Adds Discord Convex hooks, state, ready-gate, save/test handlers with toasts, discord: prefix repo filter exclusion, and a new "Discord Notifications" settings card. @radix-ui/react-tooltip added as a dev dependency.

Agentic Loop Enhancements: GitHub Triggers, Orchestrator, and Loops UI

Layer / File(s) Summary
internalStartLoop mutation
convex/loops.ts
New internal mutation validates the loop, inserts a loopRuns record, and schedules executeLoopIteration with an optional initialInput.
Loop orchestrator and automations agent tool binding
convex/agent/loopOrchestrator.ts, convex/agent/automationsAgent.ts, convex/agent/templates.ts, convex/agent/tools.ts
executeLoopIteration gains an initialInput arg, constructs basePrompt, and binds vectorTools with org/user context. runSkillAction similarly fetches the automation record and injects bound tools. PR-reviewer template content expanded. Promise<any> return type annotations added to tool execute functions.
GitHub PR event → loop trigger
convex/github.ts
For PR opened/synchronize/reopened events, queries enabled loops, selects a reviewer/PR-named loop, and schedules internalStartLoop with PR metadata as initialInput.
Loops page UI improvements
app/(app)/[orgSlug]/loops/page.tsx
Adds tooltip-wrapped Pause/Delete/Run buttons, async startLoop with isStarting state and toast feedback, conditional run-history list with status icons and relative timestamps, and "Listening (Enabled)" label.

Supporting: Test Config, Type Annotations, Plan IDs, Docs

Layer / File(s) Summary
Vitest config and test infrastructure
vitest.config.ts, convex/data.test.ts, package.json
Switches from jsdom to node environment, narrows include globs, replaces React plugin with tsconfigPaths(), and updates import.meta cast in test setup. vite-tsconfig-paths added as a dev dependency.
Type annotations, plan IDs, and docs
convex/agent/embeddings.ts, convex/mergeQueue.ts, lib/plans.ts, docs/agent/task.md
Adds explicit any annotations in embeddings and mergeQueue; updates Clerk plan IDs for PRO and ENTERPRISE; task doc updated with Phase 7 completion status.

Sequence Diagram(s)

sequenceDiagram
  rect rgba(100, 149, 237, 0.5)
    Note over User,AiWorkspace: Command Palette → AI Agent Flow
    User->>CommandPalette: types search query
    CommandPalette->>Router: navigate to /orgSlug/ai?q=encodedQuery
    Router->>AiPage: render with searchParams.q
    AiPage->>AiWorkspace: initialQuery prop
    AiWorkspace->>AiWorkspace: useEffect fires send(initialQuery)
    AiWorkspace->>Router: router.replace(pathname) strips ?q
  end
  rect rgba(144, 238, 144, 0.5)
    Note over GitHubWebhook,executeLoopIteration: GitHub PR → Agentic Loop Flow
    GitHubWebhook->>handleGithubEvent: PR opened/synchronize/reopened
    handleGithubEvent->>internalStartLoop: scheduler.runAfter(0, { loopId, initialInput: PR metadata })
    internalStartLoop->>loopRuns: db.insert status=running
    internalStartLoop->>executeLoopIteration: scheduler.runAfter(0, { loopRunId, initialInput })
    executeLoopIteration->>vectorTools: bind with orgId/requestUserId ctx
    executeLoopIteration->>generateText: call with basePrompt + boundTools
    generateText-->>executeLoopIteration: LLM result
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 Hoppity-hop through the code I go,
Discord webhooks and charts all aglow,
AI suggests labels, finds dupes in a flash,
PR loops trigger—no waiting for cash!
Analytics line charts now dance in a row,
The warren is smarter with each little show!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: Add Discord integration' accurately describes the main feature being introduced, but understates the scope of changes which extend well beyond Discord alone.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/discord-integration

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 and usage tips.

@coderabbitai coderabbitai 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.

Actionable comments posted: 16

Note

Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/(app)/[orgSlug]/loops/page.tsx (1)

31-49: ⚠️ Potential issue | 🔴 Critical

Move useQuery(api.loops.listRuns) before the access-gate return to fix hook ordering violation.

Line 49 executes only when the early return is bypassed, causing hooks to be called in inconsistent order across renders. Move the runs hook before the conditional early return and combine the conditions with the skip parameter.

Suggested fix
  const [selectedId, setSelectedId] = useState<string | null>(null);
  const [isStarting, setIsStarting] = useState(false);

+ const selectedLoop = loops?.find((l: any) => l._id === selectedId);
+ const runs = useQuery(
+   api.loops.listRuns,
+   aiAccess.hasAccess && selectedLoop ? { loopId: selectedLoop._id } : "skip"
+ );
+
  if (!aiAccess.hasAccess) {
    return (
      <div className="flex h-full flex-col items-center justify-center p-8 text-center animate-in fade-in zoom-in duration-500">
        <div className="flex h-20 w-20 items-center justify-center rounded-full bg-muted/50 mb-6 border border-border shadow-sm">
          <Repeat className="h-10 w-10 text-muted-foreground" />
        </div>
        <h2 className="text-2xl font-semibold tracking-tight mb-2">Agentic Feedback Loops</h2>
        <p className="text-muted-foreground max-w-[500px] mb-8 leading-relaxed">
          The Loops Engine requires a Pro or Enterprise plan. Upgrade to let agents iteratively verify their own work until a goal is met.
        </p>
        <Button asChild size="lg" className="shadow-md transition-all hover:scale-105 active:scale-95">
          <a href={`/${orgSlug}/settings`}>Upgrade Plan</a>
        </Button>
      </div>
    );
  }

- const selectedLoop = loops?.find((l: any) => l._id === selectedId);
- const runs = useQuery(api.loops.listRuns, selectedLoop ? { loopId: selectedLoop._id } : "skip");
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`(app)/[orgSlug]/loops/page.tsx around lines 31 - 49, The useQuery hook
call for the runs variable is positioned after the early return statement that
checks aiAccess.hasAccess, causing it to execute in inconsistent order across
renders and violating React's rules of hooks. Move the entire runs variable
assignment using useQuery(api.loops.listRuns) to the beginning of the component
before the if (!aiAccess.hasAccess) conditional early return, and update the
skip condition in the hook to handle both the case where there is no access and
the case where there is no selected loop by combining these conditions with
logical operators.

Source: Linters/SAST tools

🟡 Minor comments (7)
app/(app)/[orgSlug]/analytics/page.tsx-39-39 (1)

39-39: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Escape the apostrophe to satisfy react/no-unescaped-entities.

Line 39 should use an escaped apostrophe (for example, team&apos;s) to avoid the current ESLint error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`(app)/[orgSlug]/analytics/page.tsx at line 39, The text string "Track
your team's velocity and issue burndown." contains an unescaped apostrophe in
"team's" which violates the react/no-unescaped-entities ESLint rule. Replace the
apostrophe in "team's" with the HTML entity `&apos;` (so it becomes
"team&apos;s") to properly escape the character and satisfy the linter
requirement.

Source: Linters/SAST tools

app/(app)/[orgSlug]/analytics/page.tsx-63-66 (1)

63-66: ⚠️ Potential issue | 🟡 Minor

Use UTC methods to avoid timezone shifts in date labels.

At line 63-66, calling new Date("YYYY-MM-DD") parses the date string as UTC midnight. Using local getMonth() and getDate() methods then interprets this UTC time in the user's local timezone, causing date labels to shift by one day in UTC-negative regions. Replace with getUTCMonth() and getUTCDate(), or parse the string directly without creating a Date object.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`(app)/[orgSlug]/analytics/page.tsx around lines 63 - 66, In the
tickFormatter function, replace the local timezone methods getMonth() and
getDate() with their UTC equivalents getUTCMonth() and getUTCDate() to ensure
consistent date labels across all timezones. The current implementation
interprets a UTC-parsed date in the user's local timezone, causing date shifts
in UTC-negative regions. Change both method calls in the return statement to use
the UTC variants to properly display the date without timezone-based shifts.
convex/http.ts-128-130 (1)

128-130: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Handle invalid Slack JSON payloads with a controlled 400 response.

The non-form branch parses JSON without a guard; malformed bodies can bubble into 500s. Mirror the Discord try/catch behavior and return a client error.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@convex/http.ts` around lines 128 - 130, The JSON parsing in the else branch
(non-form branch) lacks error handling for malformed payloads. Wrap the `await
request.json()` assignment in a try/catch block similar to the Discord error
handling pattern, and return a controlled 400 response when JSON parsing fails
instead of allowing the error to bubble up as a 500.
convex/agent/tools.ts-771-771 (1)

771-771: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Remove explicit Promise<any> annotations to satisfy lint and keep type safety.

Both changed execute signatures violate @typescript-eslint/no-explicit-any.

🧹 Suggested fix
-execute: async (ctx: VectorToolCtx, input): Promise<any> => {
+execute: async (ctx: VectorToolCtx, input) => {

Also applies to: 803-803

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@convex/agent/tools.ts` at line 771, Remove the explicit `Promise<any>` return
type annotation from the execute method signatures to comply with the
`@typescript-eslint/no-explicit-any` rule. In the execute method at line 771 and
its counterpart at line 803, replace the return type annotation `Promise<any>`
with either a specific return type based on what the method actually returns, or
remove the return type annotation entirely to allow TypeScript to infer the type
automatically.

Source: Linters/SAST tools

convex/agent/data.ts-1078-1082 (1)

1078-1082: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Drop explicit any from label mapping.

Line 1078 introduces any and trips @typescript-eslint/no-explicit-any. This mapping can stay fully inferred.

Proposed fix
-    return orgLabels.map((label: any) => ({
+    return orgLabels.map((label) => ({
       labelId: label._id,
       name: label.name,
       color: label.color,
     }));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@convex/agent/data.ts` around lines 1078 - 1082, The map callback function in
the orgLabels.map call contains an explicit `any` type annotation for the label
parameter which violates the no-explicit-any linting rule. Remove the `: any`
type annotation from the label parameter in the arrow function (label: any) =>
and allow TypeScript to infer the parameter type automatically from the
orgLabels array.

Source: Linters/SAST tools

components/ai/ai-agent-page.tsx-167-167 (1)

167-167: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Escape the apostrophe to satisfy JSX lint.

Line 167 uses an unescaped ' in text (Let's), which triggers react/no-unescaped-entities.

Proposed fix
-            create, update, or analyze them for you. Let's get to work.
+            create, update, or analyze them for you. Let&apos;s get to work.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/ai/ai-agent-page.tsx` at line 167, The text on line 167 contains
an unescaped apostrophe in "Let's" which violates the
react/no-unescaped-entities linting rule. Replace the single quotation mark in
"Let's" with an HTML entity escape sequence such as &apos; or &`#39`; to resolve
the linting error while maintaining the intended meaning of the text.

Source: Linters/SAST tools

components/commands/command-provider.tsx-149-160 (1)

149-160: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Escape unescaped quote entities in AI prompt labels.

Line 149 and Line 160 render raw " in JSX text, which triggers react/no-unescaped-entities.

Proposed fix
-                Ask OpenGrove AI: "{search}"
+                Ask OpenGrove AI: &ldquo;{search}&rdquo;
...
-                Ask OpenGrove AI: "{search}"
+                Ask OpenGrove AI: &ldquo;{search}&rdquo;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/commands/command-provider.tsx` around lines 149 - 160, The literal
double quote characters in the JSX text within the CommandEmpty and CommandItem
components are unescaped, causing react/no-unescaped-entities warnings. In both
instances where you have Ask OpenGrove AI: "{search}", replace the unescaped
double quotes with HTML entity equivalents using &quot; so the text renders as
Ask OpenGrove AI: &quot;{search}&quot; in both the CommandEmpty fallback text
and the CommandItem label.

Source: Linters/SAST tools

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/`(app)/[orgSlug]/ai/page.tsx:
- Around line 4-9: Make the Page component async by adding the async keyword to
the function declaration. Update the searchParams type annotation to declare it
as a Promise by wrapping the object type in Promise (e.g., Promise<{ q?: string
| string[] }>). Inside the component body, await the searchParams variable
before using it. When passing searchParams.q to AiAgentPage's initialQuery prop,
handle the case where q might be a string array by extracting the first element
(using Array.isArray check) to ensure a single string value is passed, since
repeated query parameters can result in arrays.

In `@app/`(app)/[orgSlug]/loops/page.tsx:
- Around line 72-90: The Pause button and Delete button components in the
TooltipTrigger elements are rendering as interactive buttons with no onClick
handlers or disabled states attached, creating a non-functional UI. Either add
onClick handlers to the Button components that trigger the corresponding pause
and delete mutations/dialogs for the loop, or add the disabled attribute to
disable them visually and functionally until the actions are implemented. Apply
the same fix to any Edit or More action buttons elsewhere in the component that
have the same issue.

In `@components/ai/ai-agent-page.tsx`:
- Around line 95-100: The useEffect hook with dependencies on initialQuery and
quotaExhausted currently calls send(initialQuery) and immediately calls
router.replace(pathname) without waiting for the send operation to complete or
ensuring the query is consumed only once. To fix this, add a ref or state
variable to track whether initialQuery has already been processed, check this
flag before calling send, and only call router.replace(pathname) after the send
operation completes successfully (using await or .then()). This ensures that if
send fails, the URL query parameter is preserved, and remounts or replays will
not trigger duplicate sends since the one-time guard prevents re-processing of
the same initialQuery.

In `@components/issues/create-issue-dialog.tsx`:
- Around line 67-106: The useEffect hook in the create-issue-dialog component
has a race condition where older async requests from findDuplicates and
suggestTriage can complete after newer ones and overwrite the state with stale
data. Add a request tracking mechanism (such as a ref counter or request ID) to
ensure only the most recent request's results are applied to state. When a new
request is initiated in the handler setTimeout, increment a counter, and before
updating state with setDuplicates or setAiSuggestion, verify that the current
request ID matches the most recent one. This ensures stale responses from
earlier requests are discarded and the cleanup function cancels any pending
operations when the dialog closes or dependencies change.

In `@convex/agent/automationsAgent.ts`:
- Around line 43-47: The issue is that untrusted webhook payload data is being
directly injected into the model prompt while the full set of mutating tools
(boundTools) is available to the model, creating a prompt injection
vulnerability allowing unauthorized state changes. To fix this, filter the
`boundTools` being passed to the generateText call to only include read-only or
non-mutating tools when processing untrusted webhook payloads, ensuring that
write-side operations are not accessible during prompt execution with
attacker-controlled input.
- Around line 36-48: The issue is that vectorTools are designed for the
`@convex-dev/agent` framework expecting ctx as the first parameter, but AI SDK
v6's generateText invokes tools with the signature execute(toolInputParameters,
options) where input comes first. Simply adding a ctx property to the tool
object will not work because AI SDK will still pass parameters first.
Additionally, the args.payload is directly serialized into the prompt without
escaping, creating a prompt injection vulnerability. Create wrapper functions
around the vectorTools that adapt them to AI SDK's tool contract by accepting
the tool input parameters and internally calling the original tool with the
properly bound context. Also escape or sanitize args.payload before embedding it
into the prompt string to prevent untrusted data from steering tool execution.

In `@convex/agent/loopOrchestrator.ts`:
- Line 54: The initialInput field defined in the args at line 54 is not being
forwarded when the loopOrchestrator function recursively schedules itself for
subsequent iterations. When recursively scheduling the next iteration (in the
retry or loop logic), ensure that initialInput is explicitly included in the
args passed to the recursive call so that the original task context from the
webhook payload is preserved throughout all iterations.
- Around line 90-104: The boundTools object construction is adding a custom ctx
property to tools, but AI SDK v6's generateText function ignores this and only
passes the input argument to tool.execute(). Refactor the tool binding in the
Object.entries(vectorTools).map() function to close over the context (orgId and
requestUserId) within each tool's execute callback instead of adding it as a
separate ctx property on the tool object. Apply this same pattern to
convex/agent/automationsAgent.ts where similar tool binding occurs.

In `@convex/analytics.ts`:
- Around line 45-51: The velocity calculation for completed issues is using the
creation timestamp (createdDate derived from issue._creationTime) instead of the
actual completion time, which causes completed issues to be bucketed on their
creation date rather than when they were actually finished. Add a completion
timestamp field to the issue object that is set when the issue status
transitions to "done" or "canceled", then modify the velocity calculation in the
analytics function to bucket completed issues by this completion timestamp
instead of by createdDate when incrementing velocityMap for the "completed"
count.

In `@convex/discord.ts`:
- Around line 116-124: The fetch call to args.webhookUrl in the Discord webhook
request lacks timeout protection, which can block execution if the endpoint is
unresponsive. Add a timeout mechanism using AbortController to abort the fetch
request after a reasonable duration (e.g., 5-10 seconds), and wrap the fetch
call in a try-catch block to handle both timeout and other potential errors.
Apply this same timeout pattern to both webhook fetch calls mentioned in the
comment.
- Around line 109-113: The postToDiscord and sendTestMessage functions accept
caller-supplied webhookUrl parameters without validating the target, creating an
SSRF vulnerability where any client can post to arbitrary URLs. Add URL
validation logic to both functions to enforce https:// protocol and restrict
target URLs to Discord domains only before executing the POST request.
Additionally, add authentication checks to these action functions to ensure only
authorized callers can invoke them. Apply identical URL validation and
authentication protections to the postToSlack and sendTestMessage functions in
convex/slack.ts.

In `@convex/github.ts`:
- Around line 156-159: The reviewerLoop finding logic uses overly broad
substring matching with toLowerCase().includes("pr") and
toLowerCase().includes("reviewer"), which can match unrelated loops for PR
events. Replace these loose substring heuristics with exact matching or a
deterministic identifier such as a specific template slug or exact normalized
name. Modify the condition in the find method to use strict equality checks
against a known, specific loop identifier instead of partial string matching.

In `@convex/http.ts`:
- Around line 118-134: The Slack webhook handler at the `/slack-webhook` POST
route is missing signature verification, which allows forged events to be
processed. Before extracting the payload and calling ctx.runAction with
handleSlackWebhook, verify the HMAC-SHA256 signature from the
`X-Slack-Request-Timestamp` and `X-Slack-Request-Signature` headers using the
SLACK_WEBHOOK_SIGNING_SECRET environment variable. Construct the signature base
string by concatenating the version, timestamp, and request body, compute the
HMAC, and compare it with the provided signature. If the signature is missing,
invalid, or the timestamp is too old (outside acceptable tolerance), reject the
request with a 401 response before proceeding with payload processing.

In `@docs/agent/task.md`:
- Around line 150-153: Resolve all 33 linting errors reported by `pnpm lint`
across the codebase before merging this PR. Address the following categories:
fix React Hook violations in loops/page.tsx (useQuery called conditionally at
line 49) and ai-agent-page.tsx, create-issue-dialog.tsx, local-explorer.tsx
(setState called directly in effects); escape unescaped JSX entities (quotes and
apostrophes) in analytics/page.tsx, command-provider.tsx, and ai-agent-page.tsx;
replace explicit `any` type annotations with proper types in
merge-queue/page.tsx, generative-ui.tsx, embeddings.ts and other files; and
remove all unused imports and variables identified across the codebase. Verify
that all checks pass by running `pnpm lint` before considering the PR ready for
merge.

In `@lib/plans.ts`:
- Around line 81-87: The clerkPlanId values in PRO_PLAN and ENTERPRISE_PLAN
objects must be verified against your Clerk dashboard to ensure they are valid,
active, and correctly mapped to their respective pricing tiers (Pro at $20/mo
and Enterprise at $99/mo). These are deployment-specific configuration values
obtained from Clerk billing setup, so before merging, log into your Clerk
dashboard, confirm both plan IDs exist and are active, verify they route to the
correct checkout flows and pricing, and update the clerkPlanId values in
PRO_PLAN and ENTERPRISE_PLAN if they do not match your Clerk configuration.

In `@vitest.config.ts`:
- Around line 6-8: The test environment configuration in vitest.config.ts has a
mismatch: the environment is set to "node" but the include pattern contains
"components/**/*.test.tsx" which are React component tests that require a
browser-like environment. Either remove the "components/**/*.test.tsx" pattern
from the include array if component tests should not run in this configuration,
or change the environment setting from "node" to "jsdom" to support React
component testing. If both backend (Convex) and frontend (React) tests need to
run, consider using Vitest's multiple workspace configurations to maintain
separate test environments for each.

---

Outside diff comments:
In `@app/`(app)/[orgSlug]/loops/page.tsx:
- Around line 31-49: The useQuery hook call for the runs variable is positioned
after the early return statement that checks aiAccess.hasAccess, causing it to
execute in inconsistent order across renders and violating React's rules of
hooks. Move the entire runs variable assignment using
useQuery(api.loops.listRuns) to the beginning of the component before the if
(!aiAccess.hasAccess) conditional early return, and update the skip condition in
the hook to handle both the case where there is no access and the case where
there is no selected loop by combining these conditions with logical operators.

---

Minor comments:
In `@app/`(app)/[orgSlug]/analytics/page.tsx:
- Line 39: The text string "Track your team's velocity and issue burndown."
contains an unescaped apostrophe in "team's" which violates the
react/no-unescaped-entities ESLint rule. Replace the apostrophe in "team's" with
the HTML entity `&apos;` (so it becomes "team&apos;s") to properly escape the
character and satisfy the linter requirement.
- Around line 63-66: In the tickFormatter function, replace the local timezone
methods getMonth() and getDate() with their UTC equivalents getUTCMonth() and
getUTCDate() to ensure consistent date labels across all timezones. The current
implementation interprets a UTC-parsed date in the user's local timezone,
causing date shifts in UTC-negative regions. Change both method calls in the
return statement to use the UTC variants to properly display the date without
timezone-based shifts.

In `@components/ai/ai-agent-page.tsx`:
- Line 167: The text on line 167 contains an unescaped apostrophe in "Let's"
which violates the react/no-unescaped-entities linting rule. Replace the single
quotation mark in "Let's" with an HTML entity escape sequence such as &apos; or
&`#39`; to resolve the linting error while maintaining the intended meaning of the
text.

In `@components/commands/command-provider.tsx`:
- Around line 149-160: The literal double quote characters in the JSX text
within the CommandEmpty and CommandItem components are unescaped, causing
react/no-unescaped-entities warnings. In both instances where you have Ask
OpenGrove AI: "{search}", replace the unescaped double quotes with HTML entity
equivalents using &quot; so the text renders as Ask OpenGrove AI:
&quot;{search}&quot; in both the CommandEmpty fallback text and the CommandItem
label.

In `@convex/agent/data.ts`:
- Around line 1078-1082: The map callback function in the orgLabels.map call
contains an explicit `any` type annotation for the label parameter which
violates the no-explicit-any linting rule. Remove the `: any` type annotation
from the label parameter in the arrow function (label: any) => and allow
TypeScript to infer the parameter type automatically from the orgLabels array.

In `@convex/agent/tools.ts`:
- Line 771: Remove the explicit `Promise<any>` return type annotation from the
execute method signatures to comply with the `@typescript-eslint/no-explicit-any`
rule. In the execute method at line 771 and its counterpart at line 803, replace
the return type annotation `Promise<any>` with either a specific return type
based on what the method actually returns, or remove the return type annotation
entirely to allow TypeScript to infer the type automatically.

In `@convex/http.ts`:
- Around line 128-130: The JSON parsing in the else branch (non-form branch)
lacks error handling for malformed payloads. Wrap the `await request.json()`
assignment in a try/catch block similar to the Discord error handling pattern,
and return a controlled 400 response when JSON parsing fails instead of allowing
the error to bubble up as a 500.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5cae0563-13f5-4569-a452-b9a6fb44d789

📥 Commits

Reviewing files that changed from the base of the PR and between 88f09a0 and cc7f09e.

⛔ Files ignored due to path filters (2)
  • convex/_generated/api.d.ts is excluded by !**/_generated/**
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (27)
  • app/(app)/[orgSlug]/ai/page.tsx
  • app/(app)/[orgSlug]/analytics/page.tsx
  • app/(app)/[orgSlug]/loops/page.tsx
  • app/(app)/[orgSlug]/settings/integrations/page.tsx
  • components/ai/ai-agent-page.tsx
  • components/commands/command-provider.tsx
  • components/issues/create-issue-dialog.tsx
  • components/shell/app-sidebar.tsx
  • convex/agent/automationsAgent.ts
  • convex/agent/data.ts
  • convex/agent/embeddings.ts
  • convex/agent/loopOrchestrator.ts
  • convex/agent/templates.ts
  • convex/agent/tools.ts
  • convex/agent/triage.ts
  • convex/analytics.ts
  • convex/data.test.ts
  • convex/discord.ts
  • convex/github.ts
  • convex/http.ts
  • convex/loops.ts
  • convex/mergeQueue.ts
  • convex/slack.ts
  • docs/agent/task.md
  • lib/plans.ts
  • package.json
  • vitest.config.ts

Comment on lines +4 to +9
export default function Page({
searchParams,
}: {
searchParams: { q?: string };
}) {
return <AiAgentPage initialQuery={searchParams.q} />;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Check Next.js version
cat package.json | grep -A 2 '"next"'

Repository: demobdev/open-grove

Length of output: 140


🏁 Script executed:

# Examine the actual file to see current implementation
cat -n app/\(app\)/\[orgSlug\]/ai/page.tsx | head -20

Repository: demobdev/open-grove

Length of output: 406


🏁 Script executed:

# Check if there are other page components to understand the pattern
fd -e 'tsx' -e 'ts' -path '*/app/*' -name 'page.tsx' | head -5 | xargs -I {} head -15 {}

Repository: demobdev/open-grove

Length of output: 232


🏁 Script executed:

# Find other page.tsx files to see the pattern used elsewhere
find app -name 'page.tsx' -type f | head -10 | xargs -I {} head -15 {}

Repository: demobdev/open-grove

Length of output: 5190


🏁 Script executed:

# Check Next.js type definitions for searchParams
find node_modules/next -name '*.d.ts' -type f | xargs grep -l "searchParams" | head -3

Repository: demobdev/open-grove

Length of output: 45


🌐 Web query:

Next.js 16.2.9 App Router page props searchParams contract async Promise

💡 Result:

In the Next.js App Router, the searchParams prop provided to page.js components is a Promise that must be unwrapped to access its values [1][2]. This change was introduced as part of the transition to asynchronous Dynamic APIs (starting in Next.js 15) to enable better rendering performance, such as allowing parts of a component to render before request-specific data is fully available [3][4]. ### Server Components In Server Components, which can be async, you should use await to unwrap the searchParams Promise [1][5]: tsx export default async function Page({ searchParams, }: { searchParams: Promise<{ [key: string]: string | string[] | undefined }> }) { const query = await searchParams; const filter = query.filter; return <h1>Filter: {filter}</h1>; } ### Client Components Because Client Components cannot be async, you must use the React use() hook to unwrap the searchParams Promise [1][6]. Note that this will cause the component to suspend, so it should be wrapped in a <Suspense> boundary [7]: tsx 'use client'; import { use, Suspense } from 'react'; function SearchViewer({ searchParams }: { searchParams: Promise<any> }) { const params = use(searchParams); return <div>{params.q}</div>; } export default function Page({ searchParams }: { searchParams: Promise<any> }) { return ( <Suspense fallback={<div>Loading...</div>}> <SearchViewer searchParams={searchParams} /> </Suspense> ); } ### Key Considerations * Dynamic Rendering: Accessing searchParams (by awaiting it or passing it to use()) opts the page into dynamic rendering [2][5]. * Type Safety: You can use the globally available PageProps helper to correctly type your page props based on your route structure [1][2]: tsx export default async function Page(props: PageProps<'/blog/[slug]'>) { const searchParams = await props.searchParams; // ... } * Migration: While synchronous access was supported temporarily during the transition to allow for easier migration, it is deprecated and triggers warnings [3][4]. Always prefer the await or use() patterns [1][8].

Citations:


Make the page component async and properly type searchParams as a Promise.

The current synchronous searchParams typing is deprecated in Next.js 16.2.9. Server page components must declare searchParams as Promise<...> and await it to comply with the App Router's asynchronous Dynamic API contract. Additionally, the code must handle q as potentially being a string array for repeated query parameters.

Proposed fix
-export default function Page({
-  searchParams,
-}: {
-  searchParams: { q?: string };
-}) {
-  return <AiAgentPage initialQuery={searchParams.q} />;
+export default async function Page({
+  searchParams,
+}: {
+  searchParams: Promise<{ q?: string | string[] }>;
+}) {
+  const { q } = await searchParams;
+  const initialQuery = Array.isArray(q) ? q[0] : q;
+  return <AiAgentPage initialQuery={initialQuery} />;
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`(app)/[orgSlug]/ai/page.tsx around lines 4 - 9, Make the Page component
async by adding the async keyword to the function declaration. Update the
searchParams type annotation to declare it as a Promise by wrapping the object
type in Promise (e.g., Promise<{ q?: string | string[] }>). Inside the component
body, await the searchParams variable before using it. When passing
searchParams.q to AiAgentPage's initialQuery prop, handle the case where q might
be a string array by extracting the first element (using Array.isArray check) to
ensure a single string value is passed, since repeated query parameters can
result in arrays.

Comment on lines +72 to +90
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="text-muted-foreground hover:text-foreground">
<Pause className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Pause Loop</TooltipContent>
</Tooltip>

<Tooltip>
<TooltipTrigger asChild>
<Button variant="ghost" size="icon" className="text-muted-foreground hover:text-rose-500 transition-colors">
<Trash className="size-4" />
</Button>
</TooltipTrigger>
<TooltipContent>Delete Loop</TooltipContent>
</Tooltip>
</TooltipProvider>

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Wire or disable no-op action buttons (Pause/Delete/Edit/More).

These controls render as actionable buttons but have no handlers. Users get clickable UI with no effect. Either connect the corresponding mutations/dialogs now, or disable/hide them until implemented.

Also applies to: 307-323

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/`(app)/[orgSlug]/loops/page.tsx around lines 72 - 90, The Pause button
and Delete button components in the TooltipTrigger elements are rendering as
interactive buttons with no onClick handlers or disabled states attached,
creating a non-functional UI. Either add onClick handlers to the Button
components that trigger the corresponding pause and delete mutations/dialogs for
the loop, or add the disabled attribute to disable them visually and
functionally until the actions are implemented. Apply the same fix to any Edit
or More action buttons elsewhere in the component that have the same issue.

Comment on lines +95 to +100
useEffect(() => {
if (initialQuery && !quotaExhausted) {
void send(initialQuery);
router.replace(pathname); // Strip the query param after consuming
}
}, [initialQuery, quotaExhausted, router, pathname]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Consume initialQuery exactly once and clear URL only after successful send.

At Line 97–Line 99, the effect dispatches and immediately calls router.replace(pathname). If send fails, the prompt is dropped from the URL; and without a one-time guard, replay/remount paths can trigger duplicate sends.

Proposed fix
+  const consumedInitialQuery = useRef<string | null>(null);
+
   useEffect(() => {
-    if (initialQuery && !quotaExhausted) {
-      void send(initialQuery);
-      router.replace(pathname); // Strip the query param after consuming
-    }
+    if (!initialQuery || quotaExhausted) return;
+    if (consumedInitialQuery.current === initialQuery) return;
+    consumedInitialQuery.current = initialQuery;
+
+    void (async () => {
+      try {
+        await send(initialQuery);
+        router.replace(pathname); // Strip only after successful consume
+      } catch {
+        consumedInitialQuery.current = null; // allow retry
+      }
+    })();
   }, [initialQuery, quotaExhausted, router, pathname]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (initialQuery && !quotaExhausted) {
void send(initialQuery);
router.replace(pathname); // Strip the query param after consuming
}
}, [initialQuery, quotaExhausted, router, pathname]);
const consumedInitialQuery = useRef<string | null>(null);
useEffect(() => {
if (!initialQuery || quotaExhausted) return;
if (consumedInitialQuery.current === initialQuery) return;
consumedInitialQuery.current = initialQuery;
void (async () => {
try {
await send(initialQuery);
router.replace(pathname); // Strip only after successful consume
} catch {
consumedInitialQuery.current = null; // allow retry
}
})();
}, [initialQuery, quotaExhausted, router, pathname]);
🧰 Tools
🪛 ESLint

[error] 97-97: Error: Calling setState synchronously within an effect can trigger cascading renders

Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:

  • Update external systems with the latest state from React.
  • Subscribe for updates from some external system, calling setState in a callback function when external state changes.

Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).

/home/jailuser/git/components/ai/ai-agent-page.tsx:97:12
95 | useEffect(() => {
96 | if (initialQuery && !quotaExhausted) {

97 | void send(initialQuery);
| ^^^^ Avoid calling setState() directly within an effect
98 | router.replace(pathname); // Strip the query param after consuming
99 | }
100 | }, [initialQuery, quotaExhausted, router, pathname]);

(react-hooks/set-state-in-effect)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/ai/ai-agent-page.tsx` around lines 95 - 100, The useEffect hook
with dependencies on initialQuery and quotaExhausted currently calls
send(initialQuery) and immediately calls router.replace(pathname) without
waiting for the send operation to complete or ensuring the query is consumed
only once. To fix this, add a ref or state variable to track whether
initialQuery has already been processed, check this flag before calling send,
and only call router.replace(pathname) after the send operation completes
successfully (using await or .then()). This ensures that if send fails, the URL
query parameter is preserved, and remounts or replays will not trigger duplicate
sends since the one-time guard prevents re-processing of the same initialQuery.

Source: Linters/SAST tools

Comment on lines +67 to +106
useEffect(() => {
if (!open) {
setDuplicates([]);
setAiSuggestion(null);
setIsAiLoading(false);
return;
}
const text = title + " " + description;
if (text.trim().length < 10) {
setDuplicates([]);
setAiSuggestion(null);
setIsAiLoading(false);
return;
}

setIsAiLoading(true);
const handler = setTimeout(async () => {
try {
const [dupRes, triageRes] = await Promise.all([
findDuplicates({ title, description: description.trim() || undefined }),
suggestTriage({ title, description: description.trim() || undefined }),
]);
if (dupRes.ok) {
setDuplicates(dupRes.duplicates);
}
if (triageRes.ok) {
setAiSuggestion({
priority: triageRes.priority,
labels: triageRes.labels,
reasoning: triageRes.reasoning,
});
}
} catch (err) {
console.error("AI actions failed", err);
} finally {
setIsAiLoading(false);
}
}, 800);
return () => clearTimeout(handler);
}, [title, description, open, findDuplicates, suggestTriage]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Prevent stale AI responses from overwriting newer dialog input.

Line 83–Line 103 does not guard against out-of-order async completion. Older requests can win the race and write stale duplicates/aiSuggestion (including after dialog close).

Proposed fix
+  const aiRequestSeq = useRef(0);

   useEffect(() => {
     if (!open) {
       setDuplicates([]);
       setAiSuggestion(null);
       setIsAiLoading(false);
       return;
     }
@@
-    setIsAiLoading(true);
+    const requestId = ++aiRequestSeq.current;
+    setIsAiLoading(true);
     const handler = setTimeout(async () => {
       try {
         const [dupRes, triageRes] = await Promise.all([
           findDuplicates({ title, description: description.trim() || undefined }),
           suggestTriage({ title, description: description.trim() || undefined }),
         ]);
+        if (aiRequestSeq.current !== requestId || !open) return;
-        if (dupRes.ok) {
-           setDuplicates(dupRes.duplicates);
-        }
-        if (triageRes.ok) {
-           setAiSuggestion({
+        setDuplicates(dupRes.ok ? dupRes.duplicates : []);
+        setAiSuggestion(
+          triageRes.ok
+            ? {
               priority: triageRes.priority,
               labels: triageRes.labels,
               reasoning: triageRes.reasoning,
-           });
-        }
+            }
+            : null
+        );
       } catch (err) {
         console.error("AI actions failed", err);
       } finally {
-        setIsAiLoading(false);
+        if (aiRequestSeq.current === requestId) {
+          setIsAiLoading(false);
+        }
       }
     }, 800);
     return () => clearTimeout(handler);
   }, [title, description, open, findDuplicates, suggestTriage]);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
useEffect(() => {
if (!open) {
setDuplicates([]);
setAiSuggestion(null);
setIsAiLoading(false);
return;
}
const text = title + " " + description;
if (text.trim().length < 10) {
setDuplicates([]);
setAiSuggestion(null);
setIsAiLoading(false);
return;
}
setIsAiLoading(true);
const handler = setTimeout(async () => {
try {
const [dupRes, triageRes] = await Promise.all([
findDuplicates({ title, description: description.trim() || undefined }),
suggestTriage({ title, description: description.trim() || undefined }),
]);
if (dupRes.ok) {
setDuplicates(dupRes.duplicates);
}
if (triageRes.ok) {
setAiSuggestion({
priority: triageRes.priority,
labels: triageRes.labels,
reasoning: triageRes.reasoning,
});
}
} catch (err) {
console.error("AI actions failed", err);
} finally {
setIsAiLoading(false);
}
}, 800);
return () => clearTimeout(handler);
}, [title, description, open, findDuplicates, suggestTriage]);
const aiRequestSeq = useRef(0);
useEffect(() => {
if (!open) {
setDuplicates([]);
setAiSuggestion(null);
setIsAiLoading(false);
return;
}
const text = title + " " + description;
if (text.trim().length < 10) {
setDuplicates([]);
setAiSuggestion(null);
setIsAiLoading(false);
return;
}
const requestId = ++aiRequestSeq.current;
setIsAiLoading(true);
const handler = setTimeout(async () => {
try {
const [dupRes, triageRes] = await Promise.all([
findDuplicates({ title, description: description.trim() || undefined }),
suggestTriage({ title, description: description.trim() || undefined }),
]);
if (aiRequestSeq.current !== requestId || !open) return;
setDuplicates(dupRes.ok ? dupRes.duplicates : []);
setAiSuggestion(
triageRes.ok
? {
priority: triageRes.priority,
labels: triageRes.labels,
reasoning: triageRes.reasoning,
}
: null
);
} catch (err) {
console.error("AI actions failed", err);
} finally {
if (aiRequestSeq.current === requestId) {
setIsAiLoading(false);
}
}
}, 800);
return () => clearTimeout(handler);
}, [title, description, open, findDuplicates, suggestTriage]);
🧰 Tools
🪛 ESLint

[error] 69-69: Error: Calling setState synchronously within an effect can trigger cascading renders

Effects are intended to synchronize state between React and external systems such as manually updating the DOM, state management libraries, or other platform APIs. In general, the body of an effect should do one or both of the following:

  • Update external systems with the latest state from React.
  • Subscribe for updates from some external system, calling setState in a callback function when external state changes.

Calling setState synchronously within an effect body causes cascading renders that can hurt performance, and is not recommended. (https://react.dev/learn/you-might-not-need-an-effect).

/home/jailuser/git/components/issues/create-issue-dialog.tsx:69:7
67 | useEffect(() => {
68 | if (!open) {

69 | setDuplicates([]);
| ^^^^^^^^^^^^^ Avoid calling setState() directly within an effect
70 | setAiSuggestion(null);
71 | setIsAiLoading(false);
72 | return;

(react-hooks/set-state-in-effect)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@components/issues/create-issue-dialog.tsx` around lines 67 - 106, The
useEffect hook in the create-issue-dialog component has a race condition where
older async requests from findDuplicates and suggestTriage can complete after
newer ones and overwrite the state with stale data. Add a request tracking
mechanism (such as a ref counter or request ID) to ensure only the most recent
request's results are applied to state. When a new request is initiated in the
handler setTimeout, increment a counter, and before updating state with
setDuplicates or setAiSuggestion, verify that the current request ID matches the
most recent one. This ensures stale responses from earlier requests are
discarded and the cleanup function cancels any pending operations when the
dialog closes or dependencies change.

Comment on lines +36 to 48
const boundTools = Object.fromEntries(
Object.entries(vectorTools).map(([key, t]: [string, any]) => [
key,
{ ...t, ctx: { ...ctx, orgId: args.orgId, requestUserId: automation.createdBy } }
])
);

const { text: result } = await generateText({
model: chatModel,
system: skill.content,
prompt: `Execute the task for this incoming payload event:\n\n${JSON.stringify(args.payload, null, 2)}\n\nExecution mode is: ${args.executionMode}`,
tools: boundTools,
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

In AI SDK v6 generateText, what is the required executecallback signature for tools, and does adding a customctx field affect invocation arguments?

💡 Result:

In AI SDK v6, the required tools execute callback signature for generateText is: execute: async (parameters, options) => result Where: - parameters are the arguments from the tool call. - options has type ToolExecutionOptions (metadata/options available to tool execution, e.g. toolCallId, messages, abortSignal, and experimental tool execution context fields). [1][2] Example from the docs (showing the first argument as the tool input object): execute: async ({ location }) => ({ location, temperature: ... }) [1] Does adding a custom ctx field affect invocation arguments? No—there is no documented ctx field that is automatically injected into tool execute via a generateText option, so adding your own ctx property will not change the arguments AI SDK passes to execute. Instead, AI SDK v6 provides experimental_context, which is explicitly documented as “Context that is passed into tool execution” and is made available inside the tool’s execute via the experimental_context property in the second options argument. [3][4] So, to pass custom values into tool execution, use experimental_context (and optionally contextSchema), not a custom top-level ctx field. [3][4]

Citations:


🏁 Script executed:

# First, let's check the automationsAgent.ts file structure and the specific lines
cd convex && find . -name "automationsAgent.ts" -o -name "*automationsAgent*" 2>/dev/null | head -20

Repository: demobdev/open-grove

Length of output: 91


🏁 Script executed:

# Also search for the loopOrchestrator reference to understand the comparison
cd convex && rg "loopOrchestrator" -l --type ts 2>/dev/null | head -20

Repository: demobdev/open-grove

Length of output: 133


🏁 Script executed:

# Search for vectorTools definition and how ctx is used in tools
cd convex && rg "vectorTools" -B2 -A5 --type ts 2>/dev/null | head -80

Repository: demobdev/open-grove

Length of output: 3017


🏁 Script executed:

# Read the tools.ts file to see how tools use ctx
cd convex && cat -n agent/tools.ts | head -150

Repository: demobdev/open-grove

Length of output: 5842


🏁 Script executed:

# Look at the full automationsAgent.ts to understand context
cd convex && wc -l agent/automationsAgent.ts && echo "---" && cat -n agent/automationsAgent.ts

Repository: demobdev/open-grove

Length of output: 4634


🏁 Script executed:

# Look at loopOrchestrator.ts pattern to compare
cd convex && cat -n agent/loopOrchestrator.ts | sed -n '1,100p'

Repository: demobdev/open-grove

Length of output: 4179


Tool execution binding is incompatible with AI SDK's tool contract.

Tools in vectorTools are designed for the @convex-dev/agent framework and expect the Convex context as the first parameter (ctx: VectorToolCtx). When passed to generateText, AI SDK v6 invokes tools with the signature execute(toolInputParameters, options), not execute(ctx, input). Adding a custom ctx property to the tool object will not change how AI SDK invokes the tool — it will still pass user input parameters first, causing tools to fail when trying to access context methods like ctx.runQuery().

Additionally, the webhook payload is directly serialized into the prompt without escaping, creating a prompt injection vector where untrusted data can steer tool execution under the automation creator's identity.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@convex/agent/automationsAgent.ts` around lines 36 - 48, The issue is that
vectorTools are designed for the `@convex-dev/agent` framework expecting ctx as
the first parameter, but AI SDK v6's generateText invokes tools with the
signature execute(toolInputParameters, options) where input comes first. Simply
adding a ctx property to the tool object will not work because AI SDK will still
pass parameters first. Additionally, the args.payload is directly serialized
into the prompt without escaping, creating a prompt injection vulnerability.
Create wrapper functions around the vectorTools that adapt them to AI SDK's tool
contract by accepting the tool input parameters and internally calling the
original tool with the properly bound context. Also escape or sanitize
args.payload before embedding it into the prompt string to prevent untrusted
data from steering tool execution.

Comment thread convex/github.ts
Comment on lines +156 to +159
const reviewerLoop = loops.find(l =>
l.isEnabled &&
(l.name.toLowerCase().includes("reviewer") || l.name.toLowerCase().includes("pr"))
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Use deterministic loop targeting for PR reviewer runs.

Line 158 matches any enabled loop name containing "pr", which can start unrelated loops for PR events. Use an exact identifier (e.g., template slug or exact normalized name) instead of broad substring heuristics.

🎯 Suggested fix
-const reviewerLoop = loops.find(l => 
-  l.isEnabled && 
-  (l.name.toLowerCase().includes("reviewer") || l.name.toLowerCase().includes("pr"))
-);
+const reviewerLoop = loops.find(
+  (l) => l.isEnabled && l.name.trim().toLowerCase() === "pr code reviewer"
+);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const reviewerLoop = loops.find(l =>
l.isEnabled &&
(l.name.toLowerCase().includes("reviewer") || l.name.toLowerCase().includes("pr"))
);
const reviewerLoop = loops.find(
(l) => l.isEnabled && l.name.trim().toLowerCase() === "pr code reviewer"
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@convex/github.ts` around lines 156 - 159, The reviewerLoop finding logic uses
overly broad substring matching with toLowerCase().includes("pr") and
toLowerCase().includes("reviewer"), which can match unrelated loops for PR
events. Replace these loose substring heuristics with exact matching or a
deterministic identifier such as a specific template slug or exact normalized
name. Modify the condition in the find method to use strict equality checks
against a known, specific loop identifier instead of partial string matching.

Comment thread convex/http.ts
Comment on lines +118 to +134
http.route({
path: "/slack-webhook",
method: "POST",
handler: httpAction(async (ctx, request) => {
const contentType = request.headers.get("content-type") || "";
let payload: any;

if (contentType.includes("application/x-www-form-urlencoded")) {
const text = await request.text();
payload = Object.fromEntries(new URLSearchParams(text).entries());
} else {
payload = await request.json();
}

// Slack requires us to respond quickly, but handleSlackWebhook returns the response directly.
return await ctx.runAction(internal.slack.handleSlackWebhook, { payload });
}),

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the full convex/http.ts file
cd /
find . -type f -name "http.ts" | grep convex

Repository: demobdev/open-grove

Length of output: 161


🏁 Script executed:

# Search for the webhook handlers in the codebase
rg "handleSlackWebhook|handleDiscordWebhook" -n

Repository: demobdev/open-grove

Length of output: 525


🏁 Script executed:

# Look for signature verification patterns in the codebase
rg "signature|verify|HMAC|Ed25519|secret" -i -n | head -100

Repository: demobdev/open-grove

Length of output: 36613


🏁 Script executed:

# Read the full convex/http.ts file to see both webhook implementations
cat -n ./convex/http.ts

Repository: demobdev/open-grove

Length of output: 13190


🏁 Script executed:

# Check for signature verification in slack.ts and discord.ts
rg "signature|verify|secret" -i -n -A 5 -B 5 ./convex/slack.ts ./convex/discord.ts

Repository: demobdev/open-grove

Length of output: 1768


Add signature verification to Slack and Discord webhook endpoints.

Both Slack and Discord webhooks forward unauthenticated payloads directly to internal actions, enabling forged events. Implement signature verification at the HTTP boundary:

  • Slack: Verify HMAC-SHA256 signature using SLACK_WEBHOOK_SIGNING_SECRET (similar to the existing GitHub implementation)
  • Discord: Implement Ed25519 verification for X-Signature-Ed25519 and X-Signature-Timestamp headers (placeholder already noted in code)

Reject requests with missing or invalid signatures with 401/403.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@convex/http.ts` around lines 118 - 134, The Slack webhook handler at the
`/slack-webhook` POST route is missing signature verification, which allows
forged events to be processed. Before extracting the payload and calling
ctx.runAction with handleSlackWebhook, verify the HMAC-SHA256 signature from the
`X-Slack-Request-Timestamp` and `X-Slack-Request-Signature` headers using the
SLACK_WEBHOOK_SIGNING_SECRET environment variable. Construct the signature base
string by concatenating the version, timestamp, and request body, compute the
HMAC, and compare it with the provided signature. If the signature is missing,
invalid, or the timestamp is too old (outside acceptable tolerance), reject the
request with a 401 response before proceeding with payload processing.

Comment thread docs/agent/task.md
Comment on lines +150 to +153
## Verification
- [x] Run `pnpm test` (reverted, replaced by static analysis).
- [x] Check `/analytics` renders real data.
- [ ] Verify `pnpm exec tsc --noEmit` and `pnpm lint` pass.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Simulate pre-merge verification
pnpm exec tsc --noEmit && echo "✓ TypeScript check passed" || echo "✗ TypeScript check failed"
pnpm lint && echo "✓ Linting check passed" || echo "✗ Linting check failed"

Repository: demobdev/open-grove

Length of output: 15949


Linting checks are currently failing and must be resolved before merge.

TypeScript check passes (tsc --noEmit ✓), but pnpm lint fails with 33 errors:

  • React Hook violations: useQuery called conditionally (loops/page.tsx:49), setState called directly in effects (ai-agent-page.tsx:97, create-issue-dialog.tsx:69, local-explorer.tsx:23)
  • Unescaped entities: JSX content with unescaped quotes and apostrophes (analytics/page.tsx:39, command-provider.tsx:149/160, ai-agent-page.tsx:167)
  • Explicit any types: 15+ instances requiring type annotations (merge-queue/page.tsx, generative-ui.tsx, embeddings.ts, and others)
  • Unused imports/variables: 18 warnings across multiple files

Fix these linting errors before merging.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/agent/task.md` around lines 150 - 153, Resolve all 33 linting errors
reported by `pnpm lint` across the codebase before merging this PR. Address the
following categories: fix React Hook violations in loops/page.tsx (useQuery
called conditionally at line 49) and ai-agent-page.tsx, create-issue-dialog.tsx,
local-explorer.tsx (setState called directly in effects); escape unescaped JSX
entities (quotes and apostrophes) in analytics/page.tsx, command-provider.tsx,
and ai-agent-page.tsx; replace explicit `any` type annotations with proper types
in merge-queue/page.tsx, generative-ui.tsx, embeddings.ts and other files; and
remove all unused imports and variables identified across the codebase. Verify
that all checks pass by running `pnpm lint` before considering the PR ready for
merge.

Comment thread lib/plans.ts
Comment on lines 81 to 87
export const PRO_PLAN: PlanDefinition = {
plan: "pro",
slug: "pro",
clerkPlanId: "cplan_3FHNqGnQLUJ3xTkT6EKHjXRBQ48",
clerkPlanId: "cplan_3F1zOlRdECmGJjGmWIwzRyjeK5O",
name: "Pro",
tagline: "For growing teams that want AI superpowers and no limits.",
monthlyPrice: 20,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, read the plans.ts file to see the full context
cat -n lib/plans.ts | head -120

Repository: demobdev/open-grove

Length of output: 4734


🏁 Script executed:

# Search for references to the new plan IDs to see where they're used
rg "cplan_3F1zOlRdECmGJjGmWIwzRyjeK5O|cplan_3F1zOpyzIlH2xrZiCaIc2DqFOOL" --type ts --type tsx --type js --type json

Repository: demobdev/open-grove

Length of output: 91


🏁 Script executed:

# Look for any old Clerk plan IDs that may have been replaced
rg "cplan_" --type ts --type tsx --type json -B 2 -A 2

Repository: demobdev/open-grove

Length of output: 91


🏁 Script executed:

# Check git history for changes to this file if available
git log --oneline -20 lib/plans.ts 2>/dev/null || echo "Git history not available"

Repository: demobdev/open-grove

Length of output: 118


🏁 Script executed:

# Search for migration logic or deprecation notices related to Clerk
rg "migrate|deprecat|clerk.*plan|billing.*migration" --type ts --type tsx -i -B 2 -A 2

Repository: demobdev/open-grove

Length of output: 91


🏁 Script executed:

# Search for where these clerkPlanIds are used in the codebase
rg "clerkPlanId" -B 3 -A 3 --type ts

Repository: demobdev/open-grove

Length of output: 4067


🏁 Script executed:

# Look for Clerk configuration files or environment setup
find . -type f \( -name ".env*" -o -name "*clerk*" -o -name "clerk.config.*" \) 2>/dev/null | head -20

Repository: demobdev/open-grove

Length of output: 3941


🏁 Script executed:

# Search for any validation, tests, or documentation about plan IDs
rg "PRO_PLAN|ENTERPRISE_PLAN|FREE_PLAN" -B 2 -A 2 --type ts | head -100

Repository: demobdev/open-grove

Length of output: 3995


🏁 Script executed:

# Check if there's any git history of changes to plan IDs
git log -p --follow lib/plans.ts 2>/dev/null | grep -A 5 -B 5 "cplan_" | head -50 || echo "Git history unavailable"

Repository: demobdev/open-grove

Length of output: 45


🏁 Script executed:

# Check if there are any test files or documentation that might reference these plan IDs
find . -type f \( -name "*.test.*" -o -name "*.spec.*" -o -name "README*" \) -not -path "*/node_modules/*" 2>/dev/null | head -20

Repository: demobdev/open-grove

Length of output: 148


🏁 Script executed:

# Search for any comments or documentation about Clerk plan ID setup
rg "clerk.*plan.*id|billing.*setup|plan.*id.*must|plan.*id.*verify" -i --type ts -B 2 -A 2

Repository: demobdev/open-grove

Length of output: 3009


🏁 Script executed:

# Check for any billing-related configuration or constants
rg "PLAN|BILLING|CHECKOUT" --type ts --type json -l | grep -v node_modules | head -15

Repository: demobdev/open-grove

Length of output: 341


🏁 Script executed:

# Check the billing components to see how they handle plan IDs
cat -n components/billing/upgrade-options.tsx | head -80

Repository: demobdev/open-grove

Length of output: 2781


🏁 Script executed:

# Look at CheckoutButton component to understand plan ID validation
find . -name "*CheckoutButton*" -not -path "*/node_modules/*" 2>/dev/null

Repository: demobdev/open-grove

Length of output: 45


🏁 Script executed:

# Search for any comments about plan ID migration or old IDs
rg "old.*plan|previous.*plan|migrate.*plan|deprecat.*plan" -i --type ts -B 2 -A 2

Repository: demobdev/open-grove

Length of output: 1924


🏁 Script executed:

# Check if there's any environment configuration or secrets management that might reference plan IDs
find . -type f \( -name ".env*" -o -name "*.env.*" -o -name "*secret*" -o -name "*config*" \) -not -path "*/node_modules/*" 2>/dev/null | head -10

Repository: demobdev/open-grove

Length of output: 266


🏁 Script executed:

# Look for any deployment configuration or CI/CD that might document plan ID changes
find . -type f \( -name "*.yml" -o -name "*.yaml" -o -name "Makefile" -o -name "*.sh" \) -not -path "*/node_modules/*" | head -20

Repository: demobdev/open-grove

Length of output: 102


🏁 Script executed:

# Check if there are any comments in the billing components about plan ID validation
cat -n components/billing/upgrade-options.tsx | tail -40

Repository: demobdev/open-grove

Length of output: 1469


🏁 Script executed:

# Check if there are any tests related to billing or plans
cat -n components/dashboard/dashboard-overview.test.tsx | head -50

Repository: demobdev/open-grove

Length of output: 1473


🏁 Script executed:

# Search for any documentation about Clerk setup or billing
cat -n README.md | grep -i "clerk\|billing\|plan" -B 2 -A 2

Repository: demobdev/open-grove

Length of output: 20613


Confirm these Clerk plan IDs are valid and correctly configured before merging.

The clerkPlanId values for PRO_PLAN and ENTERPRISE_PLAN route users to specific checkout flows in Clerk. Invalid or swapped IDs would cause checkout failures or users being charged at the wrong tier.

Before approving:

  1. Verify both plan IDs exist and are active in your Clerk dashboard
  2. Confirm they map to the correct pricing tiers (Pro: $20/mo base, Enterprise: $99/mo flat)

These IDs are deployment-specific and must be obtained from your Clerk billing setup; the README (line 277) confirms this is a manual configuration step per environment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@lib/plans.ts` around lines 81 - 87, The clerkPlanId values in PRO_PLAN and
ENTERPRISE_PLAN objects must be verified against your Clerk dashboard to ensure
they are valid, active, and correctly mapped to their respective pricing tiers
(Pro at $20/mo and Enterprise at $99/mo). These are deployment-specific
configuration values obtained from Clerk billing setup, so before merging, log
into your Clerk dashboard, confirm both plan IDs exist and are active, verify
they route to the correct checkout flows and pricing, and update the clerkPlanId
values in PRO_PLAN and ENTERPRISE_PLAN if they do not match your Clerk
configuration.

Comment thread vitest.config.ts
Comment on lines 6 to +8
test: {
globals: true,
environment: "jsdom",
setupFiles: ["./vitest.setup.ts"],
include: ["**/*.test.ts", "**/*.test.tsx"],
alias: {
"@": path.resolve(__dirname, "./"),
},
environment: "node",
include: ["convex/**/*.test.ts", "components/**/*.test.tsx"],

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether any component test files exist in the repository
fd -e test.tsx -e test.ts components/

Repository: demobdev/open-grove

Length of output: 413


🏁 Script executed:

#!/bin/bash
# First, let's find the vitest.config.ts file and read it
fd -e vitest.config.ts

Repository: demobdev/open-grove

Length of output: 45


🏁 Script executed:

#!/bin/bash
# Search for test files in components directory with corrected fd syntax
fd --full-path 'components/.*\.test\.tsx?$'

Repository: demobdev/open-grove

Length of output: 112


🏁 Script executed:

#!/bin/bash
# List what's in the components directory if it exists
ls -la components/ 2>/dev/null || echo "components/ directory does not exist"

Repository: demobdev/open-grove

Length of output: 1331


🏁 Script executed:

#!/bin/bash
# Read vitest.config.ts to see the actual configuration
cat vitest.config.ts

Repository: demobdev/open-grove

Length of output: 329


Incompatible test environment configuration: React components cannot run in Node.

The test environment is set to "node" but the include pattern specifies "components/**/*.test.tsx". React component tests require a browser-like environment (e.g., "jsdom"); they will fail at runtime in the Node environment. A component test file (components/dashboard/dashboard-overview.test.tsx) exists in the codebase and is matched by this glob.

Either remove the "components/**/*.test.tsx" pattern if these tests are not meant to run, or use "jsdom" environment and maintain separate test configurations for backend (Convex) and frontend (React) tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vitest.config.ts` around lines 6 - 8, The test environment configuration in
vitest.config.ts has a mismatch: the environment is set to "node" but the
include pattern contains "components/**/*.test.tsx" which are React component
tests that require a browser-like environment. Either remove the
"components/**/*.test.tsx" pattern from the include array if component tests
should not run in this configuration, or change the environment setting from
"node" to "jsdom" to support React component testing. If both backend (Convex)
and frontend (React) tests need to run, consider using Vitest's multiple
workspace configurations to maintain separate test environments for each.

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