Skip to content

feat: Add OpenClaw agent integration with Slack webhooks#3266

Open
buildwithmoses wants to merge 1 commit intotriggerdotdev:mainfrom
buildwithmoses:feat/openclaw-agent-fixes
Open

feat: Add OpenClaw agent integration with Slack webhooks#3266
buildwithmoses wants to merge 1 commit intotriggerdotdev:mainfrom
buildwithmoses:feat/openclaw-agent-fixes

Conversation

@buildwithmoses
Copy link

Summary

Build Phase 1 MVP for AI agent platform:

  • Users create agents through setup form (/agents/setup)
  • Agents stored in database with configuration (model, platform, tools)
  • Slack webhook receives messages and routes to containers on VPS
  • Status dashboard shows agent health and execution history
  • Database models: AgentConfig, AgentExecution, AgentHealthCheck

Key Features

  • Agent Setup: Configure model, messaging platform, tools, Slack integration
  • Webhook Handler: Receives Slack events, routes to OpenClaw containers at 178.128.150.129:{port}
  • Provisioning: Assigns container ports (8001+), stores metadata
  • Status Dashboard: Real-time visibility into agent health and recent executions
  • Health Monitoring: Tracks container status and response times

Known Limitations

  • Docker provisioning not yet implemented (currently stores port assignments only)
  • Actual container startup will require SSH to VPS + docker run commands
  • Need to verify OpenClaw /api/message endpoint format

Files Changed

  • Database: Added AgentConfig, AgentExecution, AgentHealthCheck models
  • Routes: agents.setup.tsx, agents.$agentId.status.tsx, api.agents.provision.ts, webhooks.slack.ts
  • Fixes: VPS IP routing, removed axios dependency, proper fetch API usage

- Fix Slack webhook to route to VPS IP (178.128.150.129) instead of localhost
- Replace axios with fetch API (not in dependencies)
- Update provisioning endpoint status to 'provisioning' instead of 'healthy' until Docker is actually implemented
- Extract container response data from fetch properly
@changeset-bot
Copy link

changeset-bot bot commented Mar 25, 2026

⚠️ No Changeset found

Latest commit: 6c5cba9

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 25, 2026

Walkthrough

This pull request implements a comprehensive rebranding from "Trigger.dev" to "AirTrigger" across the entire application, alongside significant new agent management features. The changes include product name replacements in user-facing text, documentation links, and email communications; domain URL updates from trigger.dev to airtrigger.dev; visual styling updates including scrollbar color changes and color palette adjustments; SVG logo redesigns; and new database schema for agent configuration, execution tracking, and health checks. New Remix route handlers implement agent setup, provisioning, status tracking, and Slack webhook integration for agent message routing. Configuration updates span Tailwind themes, CLI constants, JWT issuer/audience values, and email sender addresses. A database migration establishes the agent management schema with appropriate indexes and foreign key relationships.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description provides comprehensive context about the Phase 1 MVP implementation, key features, known limitations, and files changed. However, it does not follow the required template structure with the specified checklist, testing section, changelog section, or screenshots section. Update the description to follow the template format: include the checklist with proper checkboxes, add a Testing section describing test steps, add a Changelog section with summary, and optionally add Screenshots section.
Docstring Coverage ⚠️ Warning Docstring coverage is 2.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat: Add OpenClaw agent integration with Slack webhooks' accurately summarizes the main changes: new agent integration features with Slack webhook support. It follows conventional commit format and is clear and specific.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch feat/openclaw-agent-fixes

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.

Copy link
Contributor

@devin-ai-integration devin-ai-integration bot left a comment

Choose a reason for hiding this comment

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

Devin Review found 13 potential issues.

View 3 additional findings in Devin Review.

Open in Devin Review

Comment on lines +11 to +14
export const action = async ({ request }: ActionFunctionArgs) => {
if (request.method !== "POST") {
return json({ error: "Method not allowed" }, { status: 405 });
}
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 Unauthenticated /api/agents/provision endpoint allows arbitrary database writes

The new api.agents.provision.ts route has no authentication whatsoever. Any unauthenticated HTTP POST to /api/agents/provision with an agentId can modify AgentConfig records (setting container names/ports) and create AgentHealthCheck entries. Every other mutation endpoint in the webapp requires authentication via requireUserId, requireUser, or API key validation.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +10 to +16
export const action = async ({ request }: ActionFunctionArgs) => {
if (request.method !== "POST") {
return json({ error: "Method not allowed" }, { status: 405 });
}

try {
const event = await request.json() as any;
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 Slack webhook endpoint lacks signature verification, enabling spoofed events

The new webhooks.slack.ts does not verify the x-slack-signature or x-slack-request-timestamp headers. Without verifying the HMAC signature using the Slack signing secret, any attacker can send forged webhook payloads to this endpoint, causing it to route arbitrary messages to agent containers, write execution records to the database, and potentially mark agents as unhealthy via the error handling at webhooks.slack.ts:117-129.

Prompt for agents
In apps/webapp/app/routes/webhooks.slack.ts, add Slack request signature verification before processing any events. After parsing the request body, verify the x-slack-signature header against the request body using the Slack signing secret (which should be added as an env var). Reject requests with invalid or missing signatures with a 401 response. See Slack's documentation on verifying requests: https://api.slack.com/authentication/verifying-requests-from-slack
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +71 to +75
const provisionResponse = await fetch("http://localhost:3000/api/agents/provision", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ agentId: agentConfig.id }),
});
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 Provisioning fetch uses hardcoded localhost:3000 instead of the webapp's actual origin

In agents.setup.tsx:71, the agent setup action calls fetch("http://localhost:3000/api/agents/provision", ...). The webapp runs on port 3030 in development (per CONTRIBUTING.md and CLAUDE.md) and on a different origin in production. This means the provisioning call will always fail in both dev and production. The error is silently caught at line 83, so the agent is created in the database but never actually provisioned.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +33 to +40
const lastAgent = await prisma.agentConfig.findFirst({
where: {
containerPort: { not: null },
},
orderBy: { containerPort: "desc" },
});

const nextPort = (lastAgent?.containerPort || 8000) + 1;
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 Port allocation race condition: concurrent provisions can assign the same port

In api.agents.provision.ts:33-40, the next available container port is determined by querying the max existing port and incrementing by 1. This is not atomic — if two provision requests execute concurrently, both will read the same lastAgent.containerPort and compute the same nextPort, resulting in a port collision. The subsequent prisma.agentConfig.update calls will both succeed, leaving two agents configured for the same port.

Prompt for agents
In apps/webapp/app/routes/api.agents.provision.ts, replace the non-atomic port allocation (lines 33-40) with a transaction-based approach. Either use a Prisma interactive transaction with a SELECT FOR UPDATE or use a dedicated sequence/counter in the database. The current findFirst + increment pattern has a TOCTOU race condition.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +650 to +651
.setIssuer("https://id.airtrigger.dev")
.setAudience("https://api.airtrigger.dev")
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 JWT issuer/audience changed, breaking all existing tokens

The JWT issuer was changed from https://id.trigger.dev to https://id.airtrigger.dev and audience from https://api.trigger.dev to https://api.airtrigger.dev in three locations (apiAuth.server.ts:650-651, 674-675, 747-748). The jwtVerify call at line 673 checks both issuer and audience — any JWT token issued before this deployment will fail verification because the issuer/audience won't match, immediately invalidating all active sessions using JWT auth. There is no migration path or backwards-compatible verification.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 58 to 75
const charcoal = {
100: "#E8E9EC",
200: "#D7D9DD",
300: "#B5B8C0",
400: "#878C99",
500: "#5F6570",
550: "#4D525B",
600: "#3B3E45",
650: "#2C3034",
700: "#272A2E",
750: "#212327",
775: "#1C1E21",
800: "#1A1B1F",
850: "#15171A",
900: "#121317",
950: "#0D0E12",
1000: "#0B0C0F",
100: "#F8F9FA",
200: "#F1F3F5",
300: "#E9ECEF",
400: "#868E96",
500: "#495057",
550: "#343A40",
600: "#DEE2E6",
650: "#E9ECEF",
700: "#F1F3F5",
750: "#F8F9FA",
775: "#FAFBFC",
800: "#FFFFFF",
850: "#FFFFFF",
900: "#FAFBFC",
950: "#F8F9FA",
1000: "#F1F3F5",
};
Copy link
Contributor

Choose a reason for hiding this comment

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

🚩 Tailwind charcoal palette inversion changes semantic meaning of existing classes

The tailwind.config.js completely redefines the charcoal color palette from dark values (e.g., charcoal-800: #1A1B1F) to light values (e.g., charcoal-800: #FFFFFF). This means every existing use of charcoal-* classes across the entire codebase will render with completely different colors. For example, bg-charcoal-900 was previously a very dark background (#121317) and is now #FAFBFC (near-white). While some classes were explicitly updated (e.g., scrollbar-thumb-charcoal-600scrollbar-thumb-gray-300), hundreds of other charcoal-* references throughout components were NOT updated. Elements like bg-charcoal-900/80 (used in button overlays at TSQLEditor.tsx:287), border-charcoal-800, bg-charcoal-750 (hover states in menus), and text-charcoal-500 will all look radically different. This is either intentional (relying on the palette swap to do a global theme change) or will cause widespread visual regression. A reviewer should verify the visual output across the full app.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}

// Route message to OpenClaw container (on VPS)
const containerUrl = `http://178.128.150.129:${agent.containerPort}`;
Copy link
Contributor

Choose a reason for hiding this comment

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

🚩 Hardcoded VPS IP address in webhook handler

The Slack webhook handler at webhooks.slack.ts:58 uses a hardcoded IP http://178.128.150.129:${agent.containerPort} to route messages to OpenClaw containers. This couples the code to a specific server, makes it impossible to run in different environments (dev, staging, production), and would need a code change to update the IP. This should be an environment variable. Not reported as a bug since the entire provisioning flow is marked as TODO/placeholder, but it's worth flagging.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +2609 to +2612
slackWorkspaceId String?

/// Webhook token for Slack
slackWebhookToken String?
Copy link
Contributor

Choose a reason for hiding this comment

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

🚩 Slack webhook token stored in plaintext in the database

The AgentConfig model stores slackWebhookToken as a plain String? in the Prisma schema (schema.prisma:2612). Other sensitive tokens in this codebase use SecretReference with encrypted storage (e.g., MFA secrets use SecretStore via SecretReference). Storing Slack webhook tokens in plaintext means anyone with database read access can retrieve them and send messages to users' Slack channels. This should use the existing SecretReference/SecretStore pattern.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines 147 to 151
trailingIconClassName="text-text-dimmed"
inactiveIconColor="text-sun-500"
activeIconColor="text-sun-500"
to="https://trigger.dev/changelog"
to="#"
data-action="changelog"
Copy link
Contributor

Choose a reason for hiding this comment

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

🚩 Help menu links replaced with # placeholders

In HelpAndFeedbackPopover.tsx, the Status page, Suggest a Feature, Changelog, Discord, and Book a Call links were all changed from real URLs to to="#" while keeping target="_blank". These will open blank new tabs pointing to #. This appears intentional as part of the rebrand (removing Trigger.dev-specific links before new ones are ready), but it degrades the user experience significantly. All five links now do nothing useful.

(Refers to lines 128-151)

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment on lines +18 to +19
from: env.FROM_EMAIL ?? "team@email.airtrigger.dev",
replyTo: env.REPLY_TO_EMAIL ?? "help@email.airtrigger.dev",
Copy link
Contributor

Choose a reason for hiding this comment

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

🚩 Email default addresses changed to airtrigger.dev domain without DNS/MX verification

In email.server.ts:18-31, the default email addresses were changed from *@email.trigger.dev and *@alerts.trigger.dev to *@email.airtrigger.dev and *@alerts.airtrigger.dev. If the airtrigger.dev domain doesn't have proper SPF, DKIM, and DMARC records configured for these subdomains, emails will fail to send or land in spam. The priority-support email was also changed to support@airtrigger.dev in HelpAndFeedbackPopover.tsx:196. These are all fallback defaults (only used when env vars aren't set), but they'll affect any deployment that relies on defaults.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 14

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 (2)
apps/webapp/app/services/apiAuth.server.ts (1)

648-656: ⚠️ Potential issue | 🔴 Critical

Critical: JWT issuer/audience mismatch between packages/core SDK and apps/webapp breaks compatibility.

The change updates issuer/audience to https://id.airtrigger.dev and https://api.airtrigger.dev in apps/webapp/app/services/apiAuth.server.ts, but packages/core/src/v3/jwt.ts still uses the old values (https://id.trigger.dev and https://api.trigger.dev). This creates a breaking incompatibility:

  1. Tokens issued by the SDK (via packages/core) will be rejected by the updated webapp when validated
  2. The error handling in validateJWTTokenAndRenew only catches JWTExpired, not JWTClaimValidationFailed, so claim validation failures will not be gracefully handled
  3. This affects all deployed user tasks and the platform simultaneously

Required fixes:

  • Update JWT_ISSUER and JWT_AUDIENCE constants in packages/core/src/v3/jwt.ts to match the new values
  • Add error handling for JWTClaimValidationFailed in the webapp's catch block, or implement dual-validation to accept both old and new issuer/audience during a migration period
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/services/apiAuth.server.ts` around lines 648 - 656, Update
the mismatched issuer/audience constants and the token validation error
handling: change JWT_ISSUER and JWT_AUDIENCE in packages/core/src/v3/jwt.ts to
"https://id.airtrigger.dev" and "https://api.airtrigger.dev" respectively so
tokens issued by the SDK match the webapp, and in the webapp's
validateJWTTokenAndRenew function add handling for JWTClaimValidationFailed (or
implement dual-validation logic inside the same function to accept both the old
and new issuer/audience during migration) so claim validation failures are
caught and handled the same way as JWTExpired.
apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsx (1)

115-153: ⚠️ Potential issue | 🟠 Major

Placeholder links create broken UX.

Multiple help menu links now point to # with target="_blank", which opens empty new tabs:

  • Documentation (Line 115)
  • Status (Line 128)
  • Suggest a feature (Line 139)
  • Changelog (Line 150)
  • Discord (Line 218)
  • Book a 15 min call (Line 229)

Consider either:

  1. Removing these menu items until proper URLs are available
  2. Disabling them with a visual indicator
  3. Adding the actual AirTrigger URLs before merging

Also applies to: 218-231

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsx` around
lines 115 - 153, The help menu uses SideMenuItem entries with placeholder links
(to="#" plus target="_blank") which open empty tabs; update the SideMenuItem
instances (e.g., the ones named "Documentation", "Status", "Suggest a feature",
"Changelog", "Discord", "Book a 15 min call") to either (A) remove the menu
items entirely, (B) replace the placeholder hrefs with the real AirTrigger URLs,
or (C) make them non-navigable disabled items by removing target="_blank",
setting to={undefined} or to="#" without target, adding aria-disabled and a
visual "disabled" style/tooltip; pick one approach and apply consistently to all
listed SideMenuItem entries so they no longer open blank tabs.
🟡 Minor comments (7)
apps/webapp/app/v3/vercel/vercelProjectIntegrationSchema.ts-100-101 (1)

100-101: ⚠️ Potential issue | 🟡 Minor

Fix article: "a AirTrigger" → "an AirTrigger".

Since "AirTrigger" begins with a vowel sound, the correct article is "an" rather than "a".

📝 Suggested fix
 /**
- * Maps a AirTrigger environment type to its Vercel target identifier(s).
+ * Maps an AirTrigger environment type to its Vercel target identifier(s).
  * Returns null for STAGING when no custom environment is configured.
  */
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/v3/vercel/vercelProjectIntegrationSchema.ts` around lines 100
- 101, Update the JSDoc comment that reads "Maps a AirTrigger environment type
to its Vercel target identifier(s)." to use the correct indefinite article:
change "a AirTrigger" to "an AirTrigger" in the comment above the mapping for
VerceI project integration (vercelProjectIntegrationSchema.ts) so the doc reads
"Maps an AirTrigger environment type to its Vercel target identifier(s)."
apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx-242-242 (1)

242-242: ⚠️ Potential issue | 🟡 Minor

Fix class token typo in editor container.
Line 242 has rounded-smbg-charcoal-900, which is parsed as one invalid class, so expected rounding/background styles won’t apply.

Suggested fix
-          <div className="rounded-smbg-charcoal-900 mb-3 h-full min-h-40 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-gray-300">
+          <div className="rounded-sm bg-charcoal-900 mb-3 h-full min-h-40 overflow-y-auto scrollbar-thin scrollbar-track-transparent scrollbar-thumb-gray-300">
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx` at line 242, In
ReplayRunDialog locate the editor container div whose className includes the
token "rounded-smbg-charcoal-900" and split that combined token into two valid
Tailwind classes by replacing it with "rounded-sm bg-charcoal-900" (preserving
the rest of the className string and spacing); update the className in the div
inside the ReplayRunDialog component so rounding and background styles apply
correctly.
internal-packages/emails/emails/components/Footer.tsx-10-10 (1)

10-10: ⚠️ Potential issue | 🟡 Minor

Minor copy polish: add spacing after ©.

Line 10 currently renders as “©AirTrigger”; add a space for standard formatting.

✏️ Proposed copy fix
-        ©AirTrigger, 1111B S Governors Ave STE 6433, Dover, DE 19904 |{" "}
+        © AirTrigger, 1111B S Governors Ave STE 6433, Dover, DE 19904 |{" "}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal-packages/emails/emails/components/Footer.tsx` at line 10, In the
Footer component (Footer.tsx) update the copyright string that currently renders
"©AirTrigger, 1111B S Governors Ave STE 6433, Dover, DE 19904 |{" "}" to include
a space after the © symbol (i.e., change "©AirTrigger" to "© AirTrigger") so the
displayed text follows standard formatting; locate the literal in the JSX and
insert the space directly in that string.
apps/webapp/app/routes/api.v1.orgs.$organizationSlug.projects.$projectParam.vercel.projects.ts-17-17 (1)

17-17: ⚠️ Potential issue | 🟡 Minor

Fix article in endpoint documentation string.

Line 17 should use “an AirTrigger project” instead of “a AirTrigger project” for correctness/readability.

✏️ Proposed copy fix
- * API endpoint to retrieve connected Vercel projects for a AirTrigger project.
+ * API endpoint to retrieve connected Vercel projects for an AirTrigger project.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/webapp/app/routes/api.v1.orgs`.$organizationSlug.projects.$projectParam.vercel.projects.ts
at line 17, Update the endpoint documentation string that currently reads "API
endpoint to retrieve connected Vercel projects for a AirTrigger project." to use
the correct article "an" — change it to "API endpoint to retrieve connected
Vercel projects for an AirTrigger project." Locate and edit the
docstring/comment at the top of
apps/webapp/app/routes/api.v1.orgs.$organizationSlug.projects.$projectParam.vercel.projects.ts
(the line containing the current sentence) and replace "a AirTrigger" with "an
AirTrigger".
apps/webapp/app/root.tsx-40-46 (1)

40-46: ⚠️ Potential issue | 🟡 Minor

Robots meta logic may not work as intended.

The typeof window === "undefined" check will always be true during server-side rendering, causing all pages to initially render with noindex, nofollow. The hostname check only applies client-side after hydration. If the intent is to allow indexing only on cloud.airtrigger.dev, consider moving this logic to the loader and passing the result via loader data, or use a different approach that works during SSR.

💡 Suggested approach using loader data
 export const meta: MetaFunction = ({ data }) => {
   const typedData = data as UseDataFunctionReturn<typeof loader>;
   return [
     { title: typedData?.appEnv ? `AirTrigger${appEnvTitleTag(typedData.appEnv)}` : "AirTrigger" },
     {
       name: "viewport",
       content: "width=1024, initial-scale=1",
     },
     {
       name: "robots",
-      content:
-        typeof window === "undefined" || window.location.hostname !== "cloud.airtrigger.dev"
-          ? "noindex, nofollow"
-          : "index, follow",
+      content: typedData?.shouldIndex ? "index, follow" : "noindex, nofollow",
     },
   ];
 };

Then compute shouldIndex in the loader based on env.APP_ORIGIN or a dedicated env var.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/root.tsx` around lines 40 - 46, The robots meta currently
uses typeof window which is true during SSR and causes incorrect "noindex"
output; instead compute a boolean like shouldIndex in the route loader (use
server-side config/env such as env.APP_ORIGIN or a dedicated env var to
determine if origin is cloud.airtrigger.dev) and return it as part of loader
data, then read that loader data in the root component and set the meta entry
(name: "robots", content: shouldIndex ? "index, follow" : "noindex, nofollow").
Update the loader function and the meta-generation logic to reference the
loader-provided flag (e.g., loader returns { shouldIndex } and meta reads
loaderData.shouldIndex) so the decision is correct during SSR.
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx-776-786 (1)

776-786: ⚠️ Potential issue | 🟡 Minor

Video title doesn't match video content.

The dialog is titled "AirTrigger walkthrough" but the embedded YouTube video (YH_4c0K7fGM) appears to be the original Trigger.dev walkthrough. This creates a branding inconsistency for users.

Consider either:

  1. Updating to an AirTrigger-branded video when available
  2. Adding a note that this is the original platform walkthrough
  3. Keeping the original "Trigger.dev" title until new content is ready
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx
around lines 776 - 786, The DialogTitle "AirTrigger walkthrough" is inconsistent
with the embedded YouTube video id "YH_4c0K7fGM" in the iframe (which appears to
be the original Trigger.dev walkthrough); update the UI to match branding by
either replacing the iframe src with the AirTrigger-branded video URL when
available, or change the DialogTitle/text to indicate this is the Trigger.dev
walkthrough (or add a short note below DialogTitle), making the change around
the DialogTitle component and the iframe src attribute to ensure the title and
embedded video content are consistent.
apps/webapp/app/routes/agents.$agentId.status.tsx-178-178 (1)

178-178: ⚠️ Potential issue | 🟡 Minor

Handle missing health-check latency in the table.

responseTimeMs is nullable, and the provisioning flow creates an initial health record without it. This cell will render nullms/undefinedms for those rows.

💡 Suggested fix
-                    <TableCell>{check.responseTimeMs}ms</TableCell>
+                    <TableCell>
+                      {check.responseTimeMs != null ? `${check.responseTimeMs}ms` : "—"}
+                    </TableCell>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/routes/agents`.$agentId.status.tsx at line 178, TableCell
currently renders check.responseTimeMs directly which can be null/undefined and
yields "nullms"/"undefinedms"; update the render inside the TableCell that
displays check.responseTimeMs to guard for missing values (e.g., if
check.responseTimeMs is null/undefined show a placeholder like "—" or "N/A",
otherwise render the value with "ms"). Locate the TableCell that references
check.responseTimeMs and replace the direct interpolation with a conditional
expression that outputs a human-friendly placeholder when responseTimeMs is
absent.
🧹 Nitpick comments (4)
apps/webapp/app/services/mfa/multiFactorAuthentication.server.ts (1)

139-139: Consider using proper case "AirTrigger" for the TOTP issuer label.

The issuer label appears in users' authenticator apps (e.g., Google Authenticator, Authy). Using "AirTrigger" instead of "airtrigger" would be more consistent with the product branding mentioned in the PR and easier for users to recognize.

✨ Suggested change for consistent branding
-    const otpAuthUrl = createOTP(secret).url("airtrigger", user.email);
+    const otpAuthUrl = createOTP(secret).url("AirTrigger", user.email);

Apply at both line 139 and line 185.

Also applies to: 185-185

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/services/mfa/multiFactorAuthentication.server.ts` at line
139, Update the TOTP issuer label from "airtrigger" to the proper-cased
"AirTrigger" wherever the OTP URL is generated: change the call that builds the
auth URL (createOTP(secret).url("airtrigger", user.email)) to use "AirTrigger"
instead; ensure the same update is applied to both occurrences (the
createOTP(...).url(...) invocation at the site currently using "airtrigger" and
the other identical call later in the file).
apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx (1)

940-940: Scrollbar color changes look correct, but note the mismatch with PR objectives.

The scrollbar thumb color updates from charcoal-600 to gray-300 are syntactically correct and consistent across both locations. However, the PR objectives describe "OpenClaw agent integration with Slack webhooks" and Docker provisioning features, while this file only contains visual styling changes.

This suggests either the PR description is incorrect, or these styling changes were unintentionally bundled with unrelated agent feature work. The AI summary mentions "comprehensive rebranding" which better aligns with these changes.

Also applies to: 1036-1036

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
at line 940, Summary: PR description mentions agent/webhook and Docker features
but this diff only changes a scrollbar thumb color (className "min-w-[20rem]
overflow-y-auto p-2 scrollbar-thin scrollbar-track-transparent
scrollbar-thumb-gray-300"); either update the PR description to reflect
UI/rebranding work or remove these styling edits from this feature PR. Fix:
locate the JSX using the className string above (and the duplicate occurrence of
the same class elsewhere) and either (a) move the styling changes into a
separate branch/PR dedicated to rebranding/visual tweaks and revert them here,
or (b) amend the PR title/description to include the rebranding/visual changes
so the PR accurately represents the changes.
apps/webapp/app/v3/marqs/index.server.ts (1)

2624-2624: Consider aligning the error text with the env abstraction.

The runtime check uses env.REDIS_HOST / env.REDIS_PORT, so the message could avoid naming process.env directly to reduce debugging confusion.

Suggested wording tweak
-      "Could not initialize AirTrigger because process.env.REDIS_HOST and process.env.REDIS_PORT are required to be set."
+      "Could not initialize AirTrigger because REDIS_HOST and REDIS_PORT are required to be set."
As per coding guidelines: "Access all environment variables through the `env` export of `env.server.ts` instead of directly accessing `process.env` in the Trigger.dev webapp".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/v3/marqs/index.server.ts` at line 2624, Update the error
string that currently mentions "process.env.REDIS_HOST and
process.env.REDIS_PORT" to reference the env abstraction instead (use
env.REDIS_HOST and env.REDIS_PORT) so the message aligns with the code that
checks env.REDIS_HOST / env.REDIS_PORT; locate the AirTrigger initialization
(symbol: AirTrigger) in index.server.ts and replace the literal process.env
wording in the thrown error with a message that mentions env.REDIS_HOST and
env.REDIS_PORT (or a neutral phrase like "required environment variables") to
follow the project's env.server.ts access guideline.
apps/webapp/app/routes/agents.$agentId.status.tsx (1)

28-44: Scope the lookup to the signed-in user.

This loads any agent by id and only applies the ownership check after the row plus related histories are already fetched. Querying with both id and userId keeps the authorization boundary in the database call.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/routes/agents`.$agentId.status.tsx around lines 28 - 44, The
prisma lookup currently fetches agentConfig by id then checks ownership
afterward; change the query in prisma.agentConfig.findUnique to include the
signed-in userId in the where clause (e.g., where: { id: agentId, userId:
user.id }) so the DB enforces the authorization boundary while keeping the
existing includes (executions, healthChecks) and ordering/take options.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/webapp/app/components/integrations/VercelOnboardingModal.tsx`:
- Around line 1083-1085: The copy in VercelOnboardingModal (the Paragraph
elements that currently state "This allows automatic deployments and build
synchronization" and the nearby sentence at lines cited) overstates the Vercel
integration; update the text in the Paragraph(s) inside the
VercelOnboardingModal component to say that the integration links deployment
records and context between AirTrigger and Vercel, and that builds and git
metadata are handled by the GitHub app, removing any mention of automatic
deployments or build synchronization; keep phrasing concise and user-facing so
it clarifies responsibilities (Vercel = deployments/context linking, GitHub app
= builds/git metadata).

In `@apps/webapp/app/features.server.ts`:
- Around line 7-13: The isManagedCloud function currently reads
process.env.CLOUD_ENV directly; update the file to import the env export from
env.server.ts (e.g., import { env } from "env.server" or your repo’s env module)
and replace process.env.CLOUD_ENV with env.CLOUD_ENV in the isManagedCloud
return expression so all environment access uses the env export; ensure the
import name matches the exported symbol and adjust any references accordingly.

In `@apps/webapp/app/routes/agents.setup.tsx`:
- Around line 69-75: The code is calling the app via a hard-coded
"http://localhost:3000" when POSTing { agentId: agentConfig.id } to
"/api/agents/provision" (provisionResponse/fetch), which breaks in preview/prod
or behind proxies; instead, move the provisioning logic out of the HTTP handler
into a shared server function (e.g., export a server-side
provisionAgent(agentId) helper) and invoke that helper directly from
agents.setup.tsx on the server, or derive the base URL from the incoming
request/environment and use that (e.g., use request.url or an env var) so you
don't hard-code localhost. Ensure you call the shared function with
agentConfig.id rather than issuing a local HTTP fetch to localhost.
- Around line 221-229: The placeholder for the Slack webhook input is misleading
(shows a bot token like "xoxb-...") but the value is later used to build an
incoming webhook URL in webhooks.slack.ts; update the input with id/name
slackWebhookToken (and its label) to use a webhook token/path placeholder such
as "T0XXXXXXXX/B0XXXXXXXX/XXXXXXXXXXXXXXXXXXXX" or the full path segment format
so callers know to paste the incoming webhook token (the part appended to
https://hooks.slack.com/services/) rather than a bot token.

In `@apps/webapp/app/routes/api.agents.provision.ts`:
- Around line 32-62: The current port allocation reads the max containerPort
(prisma.agentConfig.findFirst) and then updates the agent
(prisma.agentConfig.update) which is racy; change to an atomic allocation: add a
DB uniqueness constraint/index on containerPort and perform the allocation
inside a transaction (or use a DB sequence/raw SQL SELECT MAX(...) FOR UPDATE)
to compute nextPort and immediately persist it in the same transaction when
creating/updating the agent record, and handle unique-constraint violations by
retrying the allocation loop a few times before failing; update the code paths
using nextPort, containerName, and the agentConfig update to use this
transactional/atomic allocation flow.
- Around line 22-31: The provisioning route currently reads and updates agent
configs by agentId only (prisma.agentConfig.findUnique and the provisioning
update path), allowing any caller to provision others' agents; require and
validate an authenticated/internal caller (e.g., via a verified session or
internal token), extract the caller's ownerId (or teamId), and scope both reads
and writes to that owner: change the read to a scoped lookup (e.g., findFirst /
findUnique with where: { id: agentId, ownerId: currentOwnerId }) or use
updateMany for the write with where: { id: agentId, ownerId: currentOwnerId } so
the update only applies if ownership matches, and return 404 if no matching
record; ensure the same ownership check is applied in the update path referenced
around lines 55-62.
- Around line 16-20: Move the call to request.json() inside the handler's try
block and replace the type assertion with a zod validation: create a zod schema
like z.object({ agentId: z.string().min(1) }), parse the body using schema.parse
or safeParse (e.g., schema.safeParse(body)), and only destructure/use agentId
after validation; on validation failure return json({ error: "...validation
message..." }, { status: 400 }) so malformed or missing agentId returns a 400
instead of a 500. Ensure you reference the request.json() result for validation
and remove the unsafe "as { agentId: string }" assertion.

In `@apps/webapp/app/routes/webhooks.slack.ts`:
- Around line 39-45: The Slack agent lookup using prisma.agentConfig.findFirst({
where: { slackWorkspaceId, messagingPlatform: "slack", status: "healthy" } }) is
non-deterministic when multiple healthy agents exist; make routing deterministic
by adding an explicit ordering or tie-breaker to the query (e.g., orderBy a
stable column like id or createdAt) or by selecting a specific agent identifier
criterion, so update the query in webhooks.slack.ts (the
prisma.agentConfig.findFirst call) to include an orderBy (or otherwise
deterministically pick from findMany) to ensure consistent routing.
- Around line 31-36: The current logger.info call in webhooks.slack.ts logs the
Slack message body (text?.substring(0, 100)); change this to avoid logging
message content: keep workspaceId, channel, userId, and replace the text field
with a non-sensitive metric such as messageLength (e.g., text ? text.length : 0)
or a boolean hasText flag; update the logger.info invocation where workspaceId,
channel, userId, and text are used to instead emit messageLength/hasText to
prevent PII/secrets exposure.
- Around line 60-77: Add an AbortController-based timeout to the fetch call (use
AbortController/AbortSignal and setTimeout to abort after X ms) and check
containerResponse.ok immediately after fetch; if !ok, log the status and throw
or return so you don't call containerResponse.json() or proceed. Only parse
containerResponse.json() and derive agentResponse when containerResponse.ok is
true, otherwise skip the subsequent execution recording and Slack reply logic
(the variables containerResponse, containerResponse.json, and agentResponse are
the key places to update).
- Around line 23-30: Add a guard at the start of the Slack message handler in
webhooks.slack.ts to skip processing bot/postback messages and messages created
by our own bot to prevent recursion: check slackEvent.subtype and
slackEvent.bot_id and return early for values like "bot_message" or any
non-empty subtype, and also compare slackEvent.user (userId) against the app's
bot user id (from config/ENV or stored workspace bot id) and return if they
match; keep this check before using workspaceId/channel/text/userId and before
the code that posts replies (the response posting block around the current reply
logic) so self-generated replies are not re-processed.
- Around line 15-20: This webhook currently parses request.json() and trusts the
payload; instead first read the raw request body and verify the Slack signature
using X-Slack-Request-Signature and X-Slack-Request-Timestamp before any JSON
parsing or action, rejecting requests with invalid or stale signatures; after
successful verification, parse the JSON and validate the event shape with zod
(replace the unsafe "as any" usage around request.json()) and ensure handlers
that trigger container calls (the code path around the call at line ~61), mark
agents unhealthy (around line ~128), or write to the DB (lines ~80–89 and
~130–136) only run for validated, non-bot events—explicitly filter out events
that are from bots or the app itself to prevent recursion.

In
`@internal-packages/database/prisma/migrations/20260325122458_add_openclaw_agents/migration.sql`:
- Around line 1-44: The migration includes unrelated DDL that alters existing
objects; remove all non-agent changes by deleting the DROP INDEX statements for
"SecretStore_key_idx", "TaskRun_runtimeEnvironmentId_createdAt_idx",
"TaskRun_runtimeEnvironmentId_id_idx", the ALTER TABLE ... DROP DEFAULT lines
for "FeatureFlag"."updatedAt" and "IntegrationDeployment"."updatedAt", and the
ALTER/ADD CONSTRAINT and DROP INDEX blocks for junction tables
"_BackgroundWorkerToBackgroundWorkerFile", "_BackgroundWorkerToTaskQueue",
"_TaskRunToTaskRunTag", "_WaitpointRunConnections", and "_completedWaitpoints"
(i.e., any lines creating primary keys or dropping the corresponding
_*_AB_unique indexes). Keep only the three new agent table CREATE statements
plus their indexes and foreign keys; ensure no changes remain to SecretStore,
TaskRun, FeatureFlag, IntegrationDeployment, or the listed junction tables (also
remove the equivalent lines around 105-112).

In `@packages/cli-v3/src/consts.ts`:
- Around line 1-4: Remove or verify the unused CONFIG_FILES constant (export
const CONFIG_FILES) — if it serves no purpose delete it; then update all
c12.loadConfig / watchConfig calls (the loadConfig/name usages) to use name:
"airtrigger" instead of "trigger" (look for functions calling c12.loadConfig and
watchConfig), replace hardcoded "trigger.config.ts" occurrences (e.g., init
command that creates the config and build helpers referenced in
packageModules.ts and bundle.ts) with "airtrigger.config.ts", and finally run
the changeset step (pnpm run changeset:add) to record the rebranding change.

---

Outside diff comments:
In `@apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsx`:
- Around line 115-153: The help menu uses SideMenuItem entries with placeholder
links (to="#" plus target="_blank") which open empty tabs; update the
SideMenuItem instances (e.g., the ones named "Documentation", "Status", "Suggest
a feature", "Changelog", "Discord", "Book a 15 min call") to either (A) remove
the menu items entirely, (B) replace the placeholder hrefs with the real
AirTrigger URLs, or (C) make them non-navigable disabled items by removing
target="_blank", setting to={undefined} or to="#" without target, adding
aria-disabled and a visual "disabled" style/tooltip; pick one approach and apply
consistently to all listed SideMenuItem entries so they no longer open blank
tabs.

In `@apps/webapp/app/services/apiAuth.server.ts`:
- Around line 648-656: Update the mismatched issuer/audience constants and the
token validation error handling: change JWT_ISSUER and JWT_AUDIENCE in
packages/core/src/v3/jwt.ts to "https://id.airtrigger.dev" and
"https://api.airtrigger.dev" respectively so tokens issued by the SDK match the
webapp, and in the webapp's validateJWTTokenAndRenew function add handling for
JWTClaimValidationFailed (or implement dual-validation logic inside the same
function to accept both the old and new issuer/audience during migration) so
claim validation failures are caught and handled the same way as JWTExpired.

---

Minor comments:
In `@apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx`:
- Line 242: In ReplayRunDialog locate the editor container div whose className
includes the token "rounded-smbg-charcoal-900" and split that combined token
into two valid Tailwind classes by replacing it with "rounded-sm
bg-charcoal-900" (preserving the rest of the className string and spacing);
update the className in the div inside the ReplayRunDialog component so rounding
and background styles apply correctly.

In `@apps/webapp/app/root.tsx`:
- Around line 40-46: The robots meta currently uses typeof window which is true
during SSR and causes incorrect "noindex" output; instead compute a boolean like
shouldIndex in the route loader (use server-side config/env such as
env.APP_ORIGIN or a dedicated env var to determine if origin is
cloud.airtrigger.dev) and return it as part of loader data, then read that
loader data in the root component and set the meta entry (name: "robots",
content: shouldIndex ? "index, follow" : "noindex, nofollow"). Update the loader
function and the meta-generation logic to reference the loader-provided flag
(e.g., loader returns { shouldIndex } and meta reads loaderData.shouldIndex) so
the decision is correct during SSR.

In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx:
- Around line 776-786: The DialogTitle "AirTrigger walkthrough" is inconsistent
with the embedded YouTube video id "YH_4c0K7fGM" in the iframe (which appears to
be the original Trigger.dev walkthrough); update the UI to match branding by
either replacing the iframe src with the AirTrigger-branded video URL when
available, or change the DialogTitle/text to indicate this is the Trigger.dev
walkthrough (or add a short note below DialogTitle), making the change around
the DialogTitle component and the iframe src attribute to ensure the title and
embedded video content are consistent.

In `@apps/webapp/app/routes/agents`.$agentId.status.tsx:
- Line 178: TableCell currently renders check.responseTimeMs directly which can
be null/undefined and yields "nullms"/"undefinedms"; update the render inside
the TableCell that displays check.responseTimeMs to guard for missing values
(e.g., if check.responseTimeMs is null/undefined show a placeholder like "—" or
"N/A", otherwise render the value with "ms"). Locate the TableCell that
references check.responseTimeMs and replace the direct interpolation with a
conditional expression that outputs a human-friendly placeholder when
responseTimeMs is absent.

In
`@apps/webapp/app/routes/api.v1.orgs`.$organizationSlug.projects.$projectParam.vercel.projects.ts:
- Line 17: Update the endpoint documentation string that currently reads "API
endpoint to retrieve connected Vercel projects for a AirTrigger project." to use
the correct article "an" — change it to "API endpoint to retrieve connected
Vercel projects for an AirTrigger project." Locate and edit the
docstring/comment at the top of
apps/webapp/app/routes/api.v1.orgs.$organizationSlug.projects.$projectParam.vercel.projects.ts
(the line containing the current sentence) and replace "a AirTrigger" with "an
AirTrigger".

In `@apps/webapp/app/v3/vercel/vercelProjectIntegrationSchema.ts`:
- Around line 100-101: Update the JSDoc comment that reads "Maps a AirTrigger
environment type to its Vercel target identifier(s)." to use the correct
indefinite article: change "a AirTrigger" to "an AirTrigger" in the comment
above the mapping for VerceI project integration
(vercelProjectIntegrationSchema.ts) so the doc reads "Maps an AirTrigger
environment type to its Vercel target identifier(s)."

In `@internal-packages/emails/emails/components/Footer.tsx`:
- Line 10: In the Footer component (Footer.tsx) update the copyright string that
currently renders "©AirTrigger, 1111B S Governors Ave STE 6433, Dover, DE 19904
|{" "}" to include a space after the © symbol (i.e., change "©AirTrigger" to "©
AirTrigger") so the displayed text follows standard formatting; locate the
literal in the JSX and insert the space directly in that string.

---

Nitpick comments:
In
`@apps/webapp/app/routes/_app.orgs`.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx:
- Line 940: Summary: PR description mentions agent/webhook and Docker features
but this diff only changes a scrollbar thumb color (className "min-w-[20rem]
overflow-y-auto p-2 scrollbar-thin scrollbar-track-transparent
scrollbar-thumb-gray-300"); either update the PR description to reflect
UI/rebranding work or remove these styling edits from this feature PR. Fix:
locate the JSX using the className string above (and the duplicate occurrence of
the same class elsewhere) and either (a) move the styling changes into a
separate branch/PR dedicated to rebranding/visual tweaks and revert them here,
or (b) amend the PR title/description to include the rebranding/visual changes
so the PR accurately represents the changes.

In `@apps/webapp/app/routes/agents`.$agentId.status.tsx:
- Around line 28-44: The prisma lookup currently fetches agentConfig by id then
checks ownership afterward; change the query in prisma.agentConfig.findUnique to
include the signed-in userId in the where clause (e.g., where: { id: agentId,
userId: user.id }) so the DB enforces the authorization boundary while keeping
the existing includes (executions, healthChecks) and ordering/take options.

In `@apps/webapp/app/services/mfa/multiFactorAuthentication.server.ts`:
- Line 139: Update the TOTP issuer label from "airtrigger" to the proper-cased
"AirTrigger" wherever the OTP URL is generated: change the call that builds the
auth URL (createOTP(secret).url("airtrigger", user.email)) to use "AirTrigger"
instead; ensure the same update is applied to both occurrences (the
createOTP(...).url(...) invocation at the site currently using "airtrigger" and
the other identical call later in the file).

In `@apps/webapp/app/v3/marqs/index.server.ts`:
- Line 2624: Update the error string that currently mentions
"process.env.REDIS_HOST and process.env.REDIS_PORT" to reference the env
abstraction instead (use env.REDIS_HOST and env.REDIS_PORT) so the message
aligns with the code that checks env.REDIS_HOST / env.REDIS_PORT; locate the
AirTrigger initialization (symbol: AirTrigger) in index.server.ts and replace
the literal process.env wording in the thrown error with a message that mentions
env.REDIS_HOST and env.REDIS_PORT (or a neutral phrase like "required
environment variables") to follow the project's env.server.ts access guideline.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 2580bfaa-f2a6-4606-bf47-d98b192a51d8

📥 Commits

Reviewing files that changed from the base of the PR and between c00dae0 and 6c5cba9.

📒 Files selected for processing (118)
  • apps/webapp/app/bootstrap.ts
  • apps/webapp/app/components/AskAI.tsx
  • apps/webapp/app/components/BlankStatePanels.tsx
  • apps/webapp/app/components/DevPresence.tsx
  • apps/webapp/app/components/ErrorDisplay.tsx
  • apps/webapp/app/components/Feedback.tsx
  • apps/webapp/app/components/LoginPageLayout.tsx
  • apps/webapp/app/components/LogoIcon.tsx
  • apps/webapp/app/components/LogoType.tsx
  • apps/webapp/app/components/code/AIQueryInput.tsx
  • apps/webapp/app/components/code/CodeBlock.tsx
  • apps/webapp/app/components/code/TSQLEditor.tsx
  • apps/webapp/app/components/code/TSQLResultsTable.tsx
  • apps/webapp/app/components/integrations/VercelBuildSettings.tsx
  • apps/webapp/app/components/integrations/VercelOnboardingModal.tsx
  • apps/webapp/app/components/layout/AppLayout.tsx
  • apps/webapp/app/components/logs/LogDetailView.tsx
  • apps/webapp/app/components/logs/LogsTable.tsx
  • apps/webapp/app/components/navigation/AccountSideMenu.tsx
  • apps/webapp/app/components/navigation/EnvironmentSelector.tsx
  • apps/webapp/app/components/navigation/HelpAndFeedbackPopover.tsx
  • apps/webapp/app/components/navigation/OrganizationSettingsSideMenu.tsx
  • apps/webapp/app/components/navigation/SideMenu.tsx
  • apps/webapp/app/components/navigation/SideMenuHeader.tsx
  • apps/webapp/app/components/onboarding/TechnologyPicker.tsx
  • apps/webapp/app/components/primitives/Select.tsx
  • apps/webapp/app/components/primitives/Sheet.tsx
  • apps/webapp/app/components/primitives/Table.tsx
  • apps/webapp/app/components/primitives/TreeView/TreeView.tsx
  • apps/webapp/app/components/primitives/charts/ChartLegendCompound.tsx
  • apps/webapp/app/components/primitives/charts/ChartZoom.tsx
  • apps/webapp/app/components/runs/v3/ReplayRunDialog.tsx
  • apps/webapp/app/components/scheduled/timezones.tsx
  • apps/webapp/app/features.server.ts
  • apps/webapp/app/presenters/v3/VercelSettingsPresenter.server.ts
  • apps/webapp/app/root.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts.new/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.alerts/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.apikeys/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches.$batchParam/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.batches/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions.$bulkActionParam/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.bulk-actions/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.concurrency/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments.$deploymentParam/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.deployments/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables.new/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.environment-variables/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors.$fingerprint/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.errors._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.limits/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.logs/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/QueryHelpSidebar.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.query/QueryHistoryPopover.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.queues/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.regions/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.$scheduleParam/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.settings/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test.tasks.$taskParam/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.test/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens.$waitpointParam/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.tokens/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings._index/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing-alerts/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.billing/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.team/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug.settings.usage/route.tsx
  • apps/webapp/app/routes/_app.orgs.$organizationSlug_.projects.new/route.tsx
  • apps/webapp/app/routes/_app.timezones/route.tsx
  • apps/webapp/app/routes/account._index/route.tsx
  • apps/webapp/app/routes/account.security/route.tsx
  • apps/webapp/app/routes/account.tokens/route.tsx
  • apps/webapp/app/routes/agents.$agentId.status.tsx
  • apps/webapp/app/routes/agents.setup.tsx
  • apps/webapp/app/routes/api.agents.provision.ts
  • apps/webapp/app/routes/api.v1.artifacts.ts
  • apps/webapp/app/routes/api.v1.orgs.$organizationSlug.projects.$projectParam.vercel.projects.ts
  • apps/webapp/app/routes/confirm-basic-details.tsx
  • apps/webapp/app/routes/login._index/route.tsx
  • apps/webapp/app/routes/login.magic/route.tsx
  • apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.spans.$spanParam/route.tsx
  • apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.$runParam.streams.$streamKey/route.tsx
  • apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.runs.bulkaction.tsx
  • apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.schedules.new/route.tsx
  • apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.vercel.tsx
  • apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.env.$envParam.waitpoints.$waitpointFriendlyId.complete/route.tsx
  • apps/webapp/app/routes/resources.orgs.$organizationSlug.projects.$projectParam.schedules.new.natural-language.tsx
  • apps/webapp/app/routes/resources.orgs.$organizationSlug.select-plan.tsx
  • apps/webapp/app/routes/storybook.popover/route.tsx
  • apps/webapp/app/routes/storybook/route.tsx
  • apps/webapp/app/routes/webhooks.slack.ts
  • apps/webapp/app/services/apiAuth.server.ts
  • apps/webapp/app/services/email.server.ts
  • apps/webapp/app/services/mfa/multiFactorAuthentication.server.ts
  • apps/webapp/app/services/platform.v3.server.ts
  • apps/webapp/app/tailwind.css
  • apps/webapp/app/v3/marqs/index.server.ts
  • apps/webapp/app/v3/vercel/vercelProjectIntegrationSchema.ts
  • apps/webapp/remix.config.js
  • apps/webapp/tailwind.config.js
  • internal-packages/database/prisma/migrations/20260325122458_add_openclaw_agents/migration.sql
  • internal-packages/database/prisma/schema.prisma
  • internal-packages/emails/emails/alert-attempt-failure.tsx
  • internal-packages/emails/emails/alert-run-failure.tsx
  • internal-packages/emails/emails/bulk-action-complete.tsx
  • internal-packages/emails/emails/components/Footer.tsx
  • internal-packages/emails/emails/deployment-failure.tsx
  • internal-packages/emails/emails/deployment-success.tsx
  • internal-packages/emails/emails/invite.tsx
  • internal-packages/emails/emails/magic-link.tsx
  • internal-packages/emails/emails/mfa-disabled.tsx
  • internal-packages/emails/emails/mfa-enabled.tsx
  • internal-packages/emails/emails/welcome.tsx
  • packages/cli-v3/src/consts.ts

Comment on lines +1083 to 1085
To fully integrate with Vercel, AirTrigger needs access to your source code.
This allows automatic deployments and build synchronization.
</Paragraph>
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Copy overstates what the Vercel integration does.

Line 1084 and Line 1090 currently imply this integration drives automatic deployments/build sync. That can mislead users; this flow should describe linking deployments/context, while builds are handled by Vercel/GitHub.

✏️ Suggested copy adjustment
- To fully integrate with Vercel, AirTrigger needs access to your source code.
- This allows automatic deployments and build synchronization.
+ To complete this integration, connect your GitHub repository to AirTrigger.
+ This links deployment context across AirTrigger and Vercel.

- Connecting your GitHub repository enables AirTrigger to read your source code
- and automatically create deployments when you push changes to Vercel.
+ Connecting your GitHub repository lets AirTrigger read repository metadata
+ and associate deployments with the correct project/environment context.

Based on learnings, in the Vercel integration feature the GitHub app is responsible for builds and git metadata, while Vercel integration is for linking deployments between platforms.

Also applies to: 1089-1090

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/components/integrations/VercelOnboardingModal.tsx` around
lines 1083 - 1085, The copy in VercelOnboardingModal (the Paragraph elements
that currently state "This allows automatic deployments and build
synchronization" and the nearby sentence at lines cited) overstates the Vercel
integration; update the text in the Paragraph(s) inside the
VercelOnboardingModal component to say that the integration links deployment
records and context between AirTrigger and Vercel, and that builds and git
metadata are handled by the GitHub app, removing any mention of automatic
deployments or build synchronization; keep phrasing concise and user-facing so
it clarifies responsibilities (Vercel = deployments/context linking, GitHub app
= builds/git metadata).

Comment on lines 7 to 13
function isManagedCloud(host: string): boolean {
return (
host === "cloud.trigger.dev" ||
host === "test-cloud.trigger.dev" ||
host === "internal.trigger.dev" ||
host === "cloud.airtrigger.dev" ||
host === "test-cloud.airtrigger.dev" ||
host === "internal.airtrigger.dev" ||
process.env.CLOUD_ENV === "development"
);
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Use env instead of direct process.env access in webapp server code.

Line 12 should read from env.server.ts to follow the webapp env-access convention.

✅ Suggested fix
 import { requestUrl } from "./utils/requestUrl.server";
+import { env } from "./env.server";
@@
 function isManagedCloud(host: string): boolean {
   return (
     host === "cloud.airtrigger.dev" ||
     host === "test-cloud.airtrigger.dev" ||
     host === "internal.airtrigger.dev" ||
-    process.env.CLOUD_ENV === "development"
+    env.CLOUD_ENV === "development"
   );
 }

As per coding guidelines, "Access all environment variables through the env export of env.server.ts instead of directly accessing process.env in the Trigger.dev webapp".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/features.server.ts` around lines 7 - 13, The isManagedCloud
function currently reads process.env.CLOUD_ENV directly; update the file to
import the env export from env.server.ts (e.g., import { env } from "env.server"
or your repo’s env module) and replace process.env.CLOUD_ENV with env.CLOUD_ENV
in the isManagedCloud return expression so all environment access uses the env
export; ensure the import name matches the exported symbol and adjust any
references accordingly.

Comment on lines +69 to +75
// Trigger provisioning endpoint to spin up container
try {
const provisionResponse = await fetch("http://localhost:3000/api/agents/provision", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ agentId: agentConfig.id }),
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't self-call the app through http://localhost:3000.

This only works when the webapp is running on that exact host/port. In preview/prod or behind a proxy, agent creation can succeed while provisioning silently fails. Extract provisioning into a shared server function or derive the base URL from the current request/environment.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/routes/agents.setup.tsx` around lines 69 - 75, The code is
calling the app via a hard-coded "http://localhost:3000" when POSTing { agentId:
agentConfig.id } to "/api/agents/provision" (provisionResponse/fetch), which
breaks in preview/prod or behind proxies; instead, move the provisioning logic
out of the HTTP handler into a shared server function (e.g., export a
server-side provisionAgent(agentId) helper) and invoke that helper directly from
agents.setup.tsx on the server, or derive the base URL from the incoming
request/environment and use that (e.g., use request.url or an env var) so you
don't hard-code localhost. Ensure you call the shared function with
agentConfig.id rather than issuing a local HTTP fetch to localhost.

Comment on lines +221 to +229
<label htmlFor="slackWebhookToken" className="block text-sm font-medium mb-2">
Slack Webhook Token (optional)
</label>
<input
type="password"
id="slackWebhookToken"
name="slackWebhookToken"
placeholder="xoxb-..."
className="w-full px-3 py-2 border rounded-md"
Copy link
Contributor

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
# First, let's view the file and surrounding context
wc -l "apps/webapp/app/routes/agents.setup.tsx"

Repository: triggerdotdev/trigger.dev

Length of output: 112


🏁 Script executed:

#!/bin/bash
# View the specific section and surrounding context
sed -n '210,250p' "apps/webapp/app/routes/agents.setup.tsx"

Repository: triggerdotdev/trigger.dev

Length of output: 1181


🏁 Script executed:

#!/bin/bash
# Search for where slackWebhookToken is used in the file
rg "slackWebhookToken" "apps/webapp/app/routes/agents.setup.tsx" -A 3 -B 3

Repository: triggerdotdev/trigger.dev

Length of output: 1272


🏁 Script executed:

#!/bin/bash
# Search for slackWebhookToken usage across entire codebase
rg "slackWebhookToken" --type ts --type tsx -A 2 -B 2

Repository: triggerdotdev/trigger.dev

Length of output: 96


🏁 Script executed:

#!/bin/bash
# Search for slackWebhookToken usage across entire codebase
rg "slackWebhookToken" -A 2 -B 2

Repository: triggerdotdev/trigger.dev

Length of output: 3480


Fix the Slack webhook token placeholder and clarify the expected format.

The form placeholder shows xoxb-... (a bot token format), but the code interpolates the field into https://hooks.slack.com/services/${agent.slackWebhookToken} in webhooks.slack.ts. The field needs the webhook token path (e.g., T0XXXXXXXX/B0XXXXXXXX/XXXXXXXXXXXXXXXXXXXX), not a bot token. Update the placeholder to match the expected incoming webhook token format.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/routes/agents.setup.tsx` around lines 221 - 229, The
placeholder for the Slack webhook input is misleading (shows a bot token like
"xoxb-...") but the value is later used to build an incoming webhook URL in
webhooks.slack.ts; update the input with id/name slackWebhookToken (and its
label) to use a webhook token/path placeholder such as
"T0XXXXXXXX/B0XXXXXXXX/XXXXXXXXXXXXXXXXXXXX" or the full path segment format so
callers know to paste the incoming webhook token (the part appended to
https://hooks.slack.com/services/) rather than a bot token.

Comment on lines +16 to +20
const { agentId } = await request.json() as { agentId: string };

if (!agentId) {
return json({ error: "agentId is required" }, { status: 400 });
}
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Validate the JSON body before destructuring it.

await request.json() at Line 16 runs outside the try, and the type assertion does not guarantee that agentId is actually a string. Malformed bodies will turn into 500s instead of a clean 400.

As per coding guidelines, "Use zod for validation in packages/core and apps/webapp".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/routes/api.agents.provision.ts` around lines 16 - 20, Move
the call to request.json() inside the handler's try block and replace the type
assertion with a zod validation: create a zod schema like z.object({ agentId:
z.string().min(1) }), parse the body using schema.parse or safeParse (e.g.,
schema.safeParse(body)), and only destructure/use agentId after validation; on
validation failure return json({ error: "...validation message..." }, { status:
400 }) so malformed or missing agentId returns a 400 instead of a 500. Ensure
you reference the request.json() result for validation and remove the unsafe "as
{ agentId: string }" assertion.

Comment on lines +31 to +36
logger.info("Received Slack message", {
workspaceId,
channel,
userId,
text: text?.substring(0, 100),
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't log Slack message bodies here.

Even truncated to 100 characters, chat content can include customer secrets or PII. Log workspace/channel metadata and message length instead of the message text.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/routes/webhooks.slack.ts` around lines 31 - 36, The current
logger.info call in webhooks.slack.ts logs the Slack message body
(text?.substring(0, 100)); change this to avoid logging message content: keep
workspaceId, channel, userId, and replace the text field with a non-sensitive
metric such as messageLength (e.g., text ? text.length : 0) or a boolean hasText
flag; update the logger.info invocation where workspaceId, channel, userId, and
text are used to instead emit messageLength/hasText to prevent PII/secrets
exposure.

Comment on lines +39 to +45
const agent = await prisma.agentConfig.findFirst({
where: {
slackWorkspaceId: workspaceId,
messagingPlatform: "slack",
status: "healthy",
},
});
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Make Slack routing deterministic.

This lookup keys only on slackWorkspaceId, and the schema only adds a non-unique index for that column. Once a workspace has multiple healthy Slack agents, findFirst() can route messages to an arbitrary agent.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/routes/webhooks.slack.ts` around lines 39 - 45, The Slack
agent lookup using prisma.agentConfig.findFirst({ where: { slackWorkspaceId,
messagingPlatform: "slack", status: "healthy" } }) is non-deterministic when
multiple healthy agents exist; make routing deterministic by adding an explicit
ordering or tie-breaker to the query (e.g., orderBy a stable column like id or
createdAt) or by selecting a specific agent identifier criterion, so update the
query in webhooks.slack.ts (the prisma.agentConfig.findFirst call) to include an
orderBy (or otherwise deterministically pick from findMany) to ensure consistent
routing.

Comment on lines +60 to +77
try {
const containerResponse = await fetch(`${containerUrl}/api/message`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
text,
userId,
channel,
metadata: {
slackUserId: userId,
slackChannel: channel,
timestamp: new Date().toISOString(),
},
}),
});

const containerData = await containerResponse.json();
const agentResponse = containerData?.response || "I couldn't process that";
Copy link
Contributor

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:

cat -n apps/webapp/app/routes/webhooks.slack.ts | head -100

Repository: triggerdotdev/trigger.dev

Length of output: 4018


🏁 Script executed:

cat -n apps/webapp/app/routes/webhooks.slack.ts | tail -30

Repository: triggerdotdev/trigger.dev

Length of output: 1151


Add timeout and validate container response status before processing.

The container fetch at line 61 lacks a timeout and doesn't check containerResponse.ok. A non-2xx response will still attempt JSON parsing, and if successful, the code proceeds as if the request succeeded—recording an execution at line 80 and replying to Slack at line 92-100 with potentially invalid data.

Add an AbortSignal timeout to the fetch options and validate containerResponse.ok before parsing JSON or proceeding with execution recording and Slack replies.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/webapp/app/routes/webhooks.slack.ts` around lines 60 - 77, Add an
AbortController-based timeout to the fetch call (use AbortController/AbortSignal
and setTimeout to abort after X ms) and check containerResponse.ok immediately
after fetch; if !ok, log the status and throw or return so you don't call
containerResponse.json() or proceed. Only parse containerResponse.json() and
derive agentResponse when containerResponse.ok is true, otherwise skip the
subsequent execution recording and Slack reply logic (the variables
containerResponse, containerResponse.json, and agentResponse are the key places
to update).

Comment on lines +1 to +44
-- DropIndex
DROP INDEX "public"."SecretStore_key_idx";

-- DropIndex
DROP INDEX "public"."TaskRun_runtimeEnvironmentId_createdAt_idx";

-- DropIndex
DROP INDEX "public"."TaskRun_runtimeEnvironmentId_id_idx";

-- AlterTable
ALTER TABLE "public"."FeatureFlag" ALTER COLUMN "updatedAt" DROP DEFAULT;

-- AlterTable
ALTER TABLE "public"."IntegrationDeployment" ALTER COLUMN "updatedAt" DROP DEFAULT;

-- AlterTable
ALTER TABLE "public"."_BackgroundWorkerToBackgroundWorkerFile" ADD CONSTRAINT "_BackgroundWorkerToBackgroundWorkerFile_AB_pkey" PRIMARY KEY ("A", "B");

-- DropIndex
DROP INDEX "public"."_BackgroundWorkerToBackgroundWorkerFile_AB_unique";

-- AlterTable
ALTER TABLE "public"."_BackgroundWorkerToTaskQueue" ADD CONSTRAINT "_BackgroundWorkerToTaskQueue_AB_pkey" PRIMARY KEY ("A", "B");

-- DropIndex
DROP INDEX "public"."_BackgroundWorkerToTaskQueue_AB_unique";

-- AlterTable
ALTER TABLE "public"."_TaskRunToTaskRunTag" ADD CONSTRAINT "_TaskRunToTaskRunTag_AB_pkey" PRIMARY KEY ("A", "B");

-- DropIndex
DROP INDEX "public"."_TaskRunToTaskRunTag_AB_unique";

-- AlterTable
ALTER TABLE "public"."_WaitpointRunConnections" ADD CONSTRAINT "_WaitpointRunConnections_AB_pkey" PRIMARY KEY ("A", "B");

-- DropIndex
DROP INDEX "public"."_WaitpointRunConnections_AB_unique";

-- AlterTable
ALTER TABLE "public"."_completedWaitpoints" ADD CONSTRAINT "_completedWaitpoints_AB_pkey" PRIMARY KEY ("A", "B");

-- DropIndex
DROP INDEX "public"."_completedWaitpoints_AB_unique";
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Remove the unrelated generated DDL from this migration.

This feature only needs the three new agent tables plus their indexes and foreign keys. Lines 1-44 and 105-112 also rewrite existing SecretStore, TaskRun, FeatureFlag, IntegrationDeployment, and junction-table objects, which increases rollout risk on production data for no agent-specific benefit.

As per coding guidelines, "Clean up generated Prisma migrations by removing extraneous lines for junction tables (_BackgroundWorkerToBackgroundWorkerFile, _BackgroundWorkerToTaskQueue, _TaskRunToTaskRunTag, _WaitpointRunConnections, _completedWaitpoints) and indexes (SecretStore_key_idx, various TaskRun indexes) unless explicitly added".

Also applies to: 105-112

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@internal-packages/database/prisma/migrations/20260325122458_add_openclaw_agents/migration.sql`
around lines 1 - 44, The migration includes unrelated DDL that alters existing
objects; remove all non-agent changes by deleting the DROP INDEX statements for
"SecretStore_key_idx", "TaskRun_runtimeEnvironmentId_createdAt_idx",
"TaskRun_runtimeEnvironmentId_id_idx", the ALTER TABLE ... DROP DEFAULT lines
for "FeatureFlag"."updatedAt" and "IntegrationDeployment"."updatedAt", and the
ALTER/ADD CONSTRAINT and DROP INDEX blocks for junction tables
"_BackgroundWorkerToBackgroundWorkerFile", "_BackgroundWorkerToTaskQueue",
"_TaskRunToTaskRunTag", "_WaitpointRunConnections", and "_completedWaitpoints"
(i.e., any lines creating primary keys or dropping the corresponding
_*_AB_unique indexes). Keep only the three new agent table CREATE statements
plus their indexes and foreign keys; ensure no changes remain to SecretStore,
TaskRun, FeatureFlag, IntegrationDeployment, or the listed junction tables (also
remove the equivalent lines around 105-112).

Comment on lines +1 to +4
export const COMMAND_NAME = "airtrigger";
export const CLOUD_WEB_URL = "https://cloud.airtrigger.dev";
export const CLOUD_API_URL = "https://api.airtrigger.dev";
export const CONFIG_FILES = ["airtrigger.config.ts", "airtrigger.config.js", "airtrigger.config.mjs"];
Copy link
Contributor

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
# Check if a changeset was added for this package
fd -t f '.md' .changeset 2>/dev/null | xargs -I{} grep -l 'cli-v3' {} 2>/dev/null || echo "No changeset found for cli-v3"

Repository: triggerdotdev/trigger.dev

Length of output: 98


🏁 Script executed:

# Check if there are references to these constants elsewhere
rg "COMMAND_NAME|CLOUD_WEB_URL|CLOUD_API_URL|CONFIG_FILES" packages/cli-v3/src --type ts -B2 -A2

Repository: triggerdotdev/trigger.dev

Length of output: 7198


🏁 Script executed:

# Search for "airtrigger" in the codebase to understand the rebranding scope
rg "airtrigger" --type ts --type js --type json -l | head -20

Repository: triggerdotdev/trigger.dev

Length of output: 564


🏁 Script executed:

# Check if there are any migration guides or docs about the rebranding
fd -t f "\.md$" packages/cli-v3 -o fd -t f "\.md$" docs -o fd -t f "\.md$" . -path "*/migration*" -o -path "*/CHANGELOG*" 2>/dev/null | xargs grep -l "airtrigger\|trigger.config\|rebranding" 2>/dev/null | head -10

Repository: triggerdotdev/trigger.dev

Length of output: 51


🏁 Script executed:

# Look for recent changes or documentation about config file migration
rg "trigger\.config\|airtrigger\.config" --type ts --type md -B2 -A2 | head -50

Repository: triggerdotdev/trigger.dev

Length of output: 51


🏁 Script executed:

# Search for where CONFIG_FILES is actually used
rg "CONFIG_FILES" packages/cli-v3 --type ts -B3 -A3

Repository: triggerdotdev/trigger.dev

Length of output: 453


🏁 Script executed:

# Check if there's config file loading logic that references these patterns
rg "\.config\.(ts|js|mjs)" packages/cli-v3/src --type ts -B2 -A2 | head -40

Repository: triggerdotdev/trigger.dev

Length of output: 3085


🏁 Script executed:

# Look for migration or version bump information
git log --oneline -20 packages/cli-v3/src/consts.ts 2>/dev/null || echo "git log not available"

Repository: triggerdotdev/trigger.dev

Length of output: 130


🏁 Script executed:

# Check if there are any tests for config file discovery
fd -t f ".*test.*" packages/cli-v3/src | xargs rg "CONFIG_FILES|config.*file" 2>/dev/null | head -20

Repository: triggerdotdev/trigger.dev

Length of output: 51


🏁 Script executed:

# Search more broadly for where CONFIG_FILES is used (including imports)
rg "CONFIG_FILES" packages/cli-v3 --type ts -r '$0'

Repository: triggerdotdev/trigger.dev

Length of output: 201


🏁 Script executed:

# Check for config file discovery logic
rg "findConfig|loadConfig|searchConfig" packages/cli-v3/src --type ts -B2 -A5

Repository: triggerdotdev/trigger.dev

Length of output: 17840


🏁 Script executed:

# Look for file discovery patterns that might use the CONFIG_FILES constant
rg "\.config\.\(ts\|js\|mjs\)|glob.*config" packages/cli-v3/src --type ts -B2 -A2 | head -50

Repository: triggerdotdev/trigger.dev

Length of output: 51


🏁 Script executed:

# Check the build system for config file discovery
rg "trigger\.config|airtrigger\.config" packages/cli-v3/src/build --type ts -B2 -A2 | head -40

Repository: triggerdotdev/trigger.dev

Length of output: 819


🏁 Script executed:

# Look at the loadTriggerConfig or similar functions
ast-grep --pattern 'function $_ {
  $$$
  CONFIG_FILES
  $$$
}'

Repository: triggerdotdev/trigger.dev

Length of output: 51


🏁 Script executed:

# Check how c12.loadConfig works with the "trigger" name - look at the config.ts more closely
cat -n packages/cli-v3/src/config.ts | head -80

Repository: triggerdotdev/trigger.dev

Length of output: 3020


🏁 Script executed:

# Check if there are any build errors or issues with hardcoded trigger.config.ts
rg "trigger\.config\.ts" packages/cli-v3/src/build --type ts -B3 -A3

Repository: triggerdotdev/trigger.dev

Length of output: 1103


🏁 Script executed:

# Look for the c12 library to understand how it resolves config files
fd -t f "package.json" packages/cli-v3 | xargs grep -l "c12"

Repository: triggerdotdev/trigger.dev

Length of output: 97


🏁 Script executed:

# Check what name is used in the loadConfig call and if it matches CONFIG_FILES
cat -n packages/cli-v3/src/config.ts | grep -A 20 "loadConfig"

Repository: triggerdotdev/trigger.dev

Length of output: 912


🏁 Script executed:

# Check the c12 library documentation or how it resolves config files based on the "name" parameter
cat -n packages/cli-v3/package.json | grep -A2 -B2 "c12"

Repository: triggerdotdev/trigger.dev

Length of output: 228


🏁 Script executed:

# Verify the mcp/config.ts and other files that reference trigger.config to understand the full scope
rg "trigger\.config" packages/cli-v3/src --type ts -l

Repository: triggerdotdev/trigger.dev

Length of output: 387


🏁 Script executed:

# Check if there are any tests that verify config file discovery
fd -t f ".*test.*" packages/cli-v3 | xargs rg "trigger\.config|loadConfig" 2>/dev/null | head -20

Repository: triggerdotdev/trigger.dev

Length of output: 327


🏁 Script executed:

# Look at the init command to see what config file it creates
cat -n packages/cli-v3/src/commands/init.ts | grep -A 20 -B 5 "createFileFromTemplate\|config file"

Repository: triggerdotdev/trigger.dev

Length of output: 7718


🏁 Script executed:

# Check c12 library to understand how the name parameter works
npm info c12 2>/dev/null | grep -A 5 "description" || echo "npm not available"

Repository: triggerdotdev/trigger.dev

Length of output: 86


🏁 Script executed:

# Verify that CONFIG_FILES is truly unused by searching for any dynamic usage
rg "CONFIG_FILES" packages/ --type ts --type js

Repository: triggerdotdev/trigger.dev

Length of output: 201


🏁 Script executed:

# Check if the c12 name parameter needs to be updated too
rg "name:.*trigger" packages/cli-v3/src --type ts -B2 -A2

Repository: triggerdotdev/trigger.dev

Length of output: 3224


🏁 Script executed:

# Look at the complete init command to see if it references the new airtrigger names anywhere
grep -n "airtrigger" packages/cli-v3/src/commands/init.ts

Repository: triggerdotdev/trigger.dev

Length of output: 51


🏁 Script executed:

# Verify if build system needs airtrigger updates
grep -n "airtrigger" packages/cli-v3/src/build/*.ts

Repository: triggerdotdev/trigger.dev

Length of output: 51


🏁 Script executed:

# Check if the c12 name parameter was supposed to be changed to "airtrigger" but wasn't
git diff HEAD~1 packages/cli-v3/src/config.ts 2>/dev/null | head -50 || echo "git diff not available"

Repository: triggerdotdev/trigger.dev

Length of output: 51


🏁 Script executed:

# Verify the critical inconsistency: c12 still uses name="trigger" but CONFIG_FILES says "airtrigger"
echo "=== c12 config name ===" && rg 'c12\.loadConfig.*name.*trigger' packages/cli-v3/src/config.ts -A2 && echo "=== CONFIG_FILES ===" && rg 'CONFIG_FILES.*airtrigger' packages/cli-v3/src/consts.ts

Repository: triggerdotdev/trigger.dev

Length of output: 92


🏁 Script executed:

# Check if there are any migration docs or README updates about the rename
fd -t f "README*" packages/cli-v3 -o -path "*MIGRATION*" packages/cli-v3 2>/dev/null | xargs cat 2>/dev/null | grep -i "airtrigger\|config\|rename" | head -10

Repository: triggerdotdev/trigger.dev

Length of output: 51


🏁 Script executed:

# Verify all the hardcoded references to trigger.config that need updating
rg '"trigger\.config\.' packages/cli-v3/src --type ts -n | head -15

Repository: triggerdotdev/trigger.dev

Length of output: 329


CONFIG_FILES constant is unused, and the rebranding to "airtrigger" is incomplete.

The CONFIG_FILES constant defined here is dead code—it's never imported or used anywhere. The actual config file discovery is handled by the c12 library, which uses the name parameter (still set to "trigger" in packages/cli-v3/src/config.ts lines 41 and 71), not this constant.

Additionally, the rebranding to "airtrigger" is inconsistent across the codebase:

  • c12.loadConfig still uses name: "trigger"
  • The init command still creates trigger.config.ts (line 593)
  • Build system has hardcoded "trigger.config.ts" checks (packageModules.ts:236, bundle.ts:272)
  • MCP config references the "trigger" name

For proper rebranding:

  1. Remove the unused CONFIG_FILES constant or verify its intended purpose
  2. Update c12.loadConfig name from "trigger" to "airtrigger" in both loadConfig and watchConfig calls
  3. Update hardcoded "trigger.config.ts" strings throughout the codebase to use "airtrigger.config.ts"
  4. Add the required changeset via pnpm run changeset:add
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/cli-v3/src/consts.ts` around lines 1 - 4, Remove or verify the
unused CONFIG_FILES constant (export const CONFIG_FILES) — if it serves no
purpose delete it; then update all c12.loadConfig / watchConfig calls (the
loadConfig/name usages) to use name: "airtrigger" instead of "trigger" (look for
functions calling c12.loadConfig and watchConfig), replace hardcoded
"trigger.config.ts" occurrences (e.g., init command that creates the config and
build helpers referenced in packageModules.ts and bundle.ts) with
"airtrigger.config.ts", and finally run the changeset step (pnpm run
changeset:add) to record the rebranding change.

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.

2 participants