Backend ai#220
Conversation
- Update 'ai' package version to 6.0.146 for improved functionality - Introduce optional backend support in project creation and message handling - Add 'setHasBackendForUser' mutation to manage backend status for projects - Extend message creation to include optional messageId for better tracking - Update API definitions to include new schema proposals and backend features
📝 WalkthroughWalkthroughAdds an automated Convex backend bootstrapping flow: schema-proposal and backend-generation agents, new prompts, schemaProposals table and related mutations, Convex project/fragment backend metadata, and integration into the Inngest orchestration and client request paths. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant Client
participant Inngest
participant OpenRouter
participant Convex
User->>Client: Submit request (includes messageId)
Client->>Inngest: POST /api/agent/run (projectId, value, messageId)
Inngest->>Inngest: wantsConvexBackend(prompt) check
alt needs backend
Inngest->>OpenRouter: runSchemaProposalAgent(prompt, plan)
OpenRouter-->>Inngest: <schema_proposal>...</schema_proposal>
Inngest->>Inngest: parseSchemaProposal -> parsedTables
Inngest->>Convex: create schemaProposals record
Inngest->>OpenRouter: runBackendImplementerAgent(prompt, schemaProposal)
OpenRouter-->>Inngest: generated files (<zapdev_file ...>)
Inngest->>Inngest: parseGeneratedFiles -> backendPreload
Inngest->>Convex: mark proposal implemented
Inngest->>Convex: setHasBackendForUser(hasBackend=true)
end
Inngest->>Convex: createFragmentForUser(..., hasBackend?, backendFiles?)
Inngest-->>Client: respond with result and backend metadata
Client-->>User: display results
Estimated code review effort🎯 4 (Complex) | ⏱️ ~50 minutes Possibly related PRs
Suggested labels
Poem
🚥 Pre-merge checks | ✅ 1 | ❌ 2❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
|
@cubic-dev-ai review |
@Jackson57279 I have started the AI code review. It will take a few minutes to complete. |
There was a problem hiding this comment.
Actionable comments posted: 10
🧹 Nitpick comments (3)
src/agents/wants-backend.ts (1)
1-3: Backend intent regex is likely too broad and may over-trigger.Line 2 includes generic terms (
queries,mutations,schema) that can appear in non-backend requests, which may cause unnecessary backend bootstrapping. Consider splitting into “strong backend signals” vs “weak signals” and requiring at least one strong match.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/agents/wants-backend.ts` around lines 1 - 3, The BACKEND_INTENT regex is too broad; replace it with two regexes (e.g., STRONG_BACKEND_INTENT and WEAK_BACKEND_INTENT) and change the matching logic to require at least one strong signal to trigger backend bootstrapping (do not trigger on weak-only matches). Update the declaration that currently defines BACKEND_INTENT to instead define the two regex constants (moving strong anchors like "database", "backend", "authentication", "postgres", "supabase", "prisma" into STRONG_BACKEND_INTENT and generic terms like "queries", "mutations", "schema" into WEAK_BACKEND_INTENT) and modify every usage site that references BACKEND_INTENT (search for BACKEND_INTENT in this file/agent logic) to perform a check like: if (STRONG_BACKEND_INTENT.test(text)) { bootstrap } else { do not bootstrap } (or include additional conditional logic only if you want to allow weak signals combined with other heuristics).convex/schema.ts (1)
132-133:backendFilesshould be schema-validated more strictly thanv.any().Using
v.any()here weakens data guarantees and makes malformed payloads easy to persist. Prefer a constrained shape (for example, a filename→content record) to protect reads and future migrations.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@convex/schema.ts` around lines 132 - 133, The backendFiles field uses v.any() which is too permissive; replace v.optional(v.any()) with a constrained schema (e.g., v.optional(v.record(v.string(), v.string())) or v.optional(v.dict(v.string(), v.string())) depending on your validator API) to enforce a filename→content shape, and update any code that reads backendFiles accordingly; keep hasBackend as v.optional(v.boolean()) but ensure backendFiles validation matches the expected structure used by functions that access backendFiles.convex/schemaProposals.ts (1)
59-64: Consider:approvedAtis set on implementation even if never explicitly approved.Line 61 sets
approvedAt: row.approvedAt ?? now, meaning if a schema was never approved but is directly marked as implemented, it gets anapprovedAttimestamp. This may be intentional (auto-approve on implement) but could mask the approval workflow if auditing requires explicit approval before implementation.If explicit approval is required:
+ if (!row.approvedAt) { + throw new Error("Schema must be approved before implementation"); + } await ctx.db.patch(args.schemaProposalId, { status: "IMPLEMENTED", - approvedAt: row.approvedAt ?? now, + approvedAt: row.approvedAt, implementedAt: now, updatedAt: now, });Otherwise, if auto-approve is desired, consider adding a comment clarifying the intent.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@convex/schemaProposals.ts` around lines 59 - 64, The patch sets approvedAt to row.approvedAt ?? now when marking a proposal IMPLEMENTED, which implicitly auto-approves proposals that were never explicitly approved; decide the intended behavior and implement it: if explicit approval is required, change the update logic (for the ctx.db.patch call using args.schemaProposalId and fields approvedAt/status/implementedAt/updatedAt) to NOT set approvedAt when row.approvedAt is absent and instead reject or surface an error, or else leave approvedAt null; if auto-approve is intended, add a clarifying comment next to the approvedAt assignment explaining that implementing also sets approvedAt to now when missing to indicate auto-approval.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In @.cursor/hooks/state/continual-learning.json:
- Around line 1-8: Add a gitignore rule to exclude the IDE/state files under
.cursor/hooks/state/ by appending an entry like `.cursor/hooks/state/` to
.gitignore; ensure you only ignore the state directory (not other .cursor files
like rules or configuration) and include a short comment explaining it's to
prevent committing auto-generated IDE state files so reviewers understand the
intent.
In `@convex/projects.ts`:
- Around line 475-490: The mutation setHasBackendForUser currently trusts the
caller-supplied userId arg for authorization which is spoofable; update the
handler to call requireAuth(ctx) (or otherwise read ctx.auth.userId) and use the
authenticated user id to authorize (compare ctx.auth.userId to project.userId)
instead of args.userId, and remove or ignore the userId arg in args; also ensure
you still fetch the project via ctx.db.get(args.projectId) and throw
Unauthorized if the authenticated user isn't the owner before patching
hasBackend/updatedAt.
In `@src/agents/backend-agent.ts`:
- Around line 100-104: The loop that consumes model output (while ((match =
fileRegex.exec(text)) ...) trusts match[1] as a file path and writes it into
files[path]; instead validate and normalize each path before accepting: reject
absolute paths or any path containing "..", null bytes, or path separators that
escape the intended workspace, enforce an allowlist/prefix (e.g., must start
with "convex/"), and normalize (using a path normalization helper like
validateAndNormalizePath or path.posix.normalize) to collapse any "../" before
adding to files; if validation fails, skip the entry and log or surface an error
instead of writing to files.
In `@src/agents/schema-proposal-agent.ts`:
- Around line 50-57: The current validation in schema-proposal-agent.ts only
checks for the opening "<schema_proposal>" in the variable text which allows
malformed or truncated model output to be treated as successful; update the
validation in the handler that returns { schemaProposal, success, error } to
ensure both the opening "<schema_proposal>" and the corresponding closing
"</schema_proposal>" exist and that the content between them can be extracted
(e.g., locate start/end indices or use a simple regex to parse the block) before
setting success=true and returning schemaProposal; if parsing fails or the
closing tag is missing, return success=false and an explanatory error.
- Around line 121-123: The current regex in fieldsSection.matchAll uses (\w+)
which only captures word tokens and drops complex Convex validator expressions;
update the pattern to capture the entire validator expression (e.g. /-
`([^`]+)`: ([^\n]+)/g or /- `([^`]+)`: (.+)/g) and then trim the captured group
before pushing into fields so validators like v.id("users") or
v.optional(v.string()) are preserved (change the match in the
fieldsSection.matchAll call and use fields.push(`${fieldMatch[1]}:
${fieldMatch[2].trim()}`) in the same block that handles fieldMatches).
In `@src/app/api/agent/run/route.ts`:
- Around line 40-43: The messageId validation checks messageId.trim() but
forwards the original untrimmed messageId; update the assignment for the
messageId property so it sends the trimmed value (e.g., use messageId.trim())
when typeof messageId === "string" and trimmed length > 0. Modify the object
construction in the route handler where messageId is set to ensure the trimmed
string is forwarded (reference the messageId variable and the messageId:
property in this file). Ensure you still set undefined when the trimmed value is
empty.
In `@src/inngest/functions.ts`:
- Around line 284-296: The code unsafely casts event.data.messageId to
Id<"messages"> when resolving triggerMessageId; instead validate the incoming ID
before casting (or avoid casting) to prevent opaque failures: update the
resolve-trigger-message block (referencing triggerMessageId and
event.data.messageId) to check the ID format/shape used by Convex (or run a
lightweight lookup via getConvexClient + api.messages.listForUser) and only
return a typed Id<"messages"> when the value is valid, otherwise return null;
alternatively, keep the value as string | null and let the downstream mutation
(createForUser / ownership checks) surface a clear "Message not found" error.
- Around line 338-350: The current finalize-backend-bootstrap step runs two
sequential mutations
(convex.mutation(api.schemaProposals.markImplementedForUser, ...) and
convex.mutation(api.projects.setHasBackendForUser, ...)) which can leave the
system inconsistent if one succeeds and the other fails; change this by
splitting the work into two independent Inngest steps (e.g.,
finalize-mark-implemented and finalize-set-has-backend) so each mutation has its
own retry semantics, or alternatively make the operations atomic/idempotent in
the backend (use a transaction or an idempotent setter on
api.projects.setHasBackendForUser and check/update schemaProposal state to be
safe on retries). Ensure the new steps reference the same identifiers (userId,
schemaProposalId/schemaProposalRowId, projectId) and that needsBackendBootstrap
continues to consult project.hasBackend so workflow resumes correctly after
partial failures.
In `@src/prompts/backend/convex-backend.ts`:
- Around line 194-199: The prompt in src/prompts/backend/convex-backend.ts uses
createOrUpdateFiles + a <task_summary> block which conflicts with the parser in
src/agents/backend-agent.ts that expects <zapdev_file path="..."> blocks; update
the prompt to emit each file wrapped in <zapdev_file path="..."> with file
contents (one block per file) and move the summary outside or into a separate,
parser-recognized tag so the backend-agent.ts parser can extract files correctly
instead of producing empty parsed files.
- Around line 42-45: The prompt examples incorrectly show ctx.db.delete(id)
without the required table name; update the guidance so all CRUD examples
consistently use the correct signatures: ctx.db.insert("table", data),
ctx.db.patch("table", id, updates), ctx.db.replace("table", id, data), and
ctx.db.delete("table", id). Specifically change the ctx.db.delete example to
include the "table" parameter to match the other examples and avoid generating
invalid code.
---
Nitpick comments:
In `@convex/schema.ts`:
- Around line 132-133: The backendFiles field uses v.any() which is too
permissive; replace v.optional(v.any()) with a constrained schema (e.g.,
v.optional(v.record(v.string(), v.string())) or v.optional(v.dict(v.string(),
v.string())) depending on your validator API) to enforce a filename→content
shape, and update any code that reads backendFiles accordingly; keep hasBackend
as v.optional(v.boolean()) but ensure backendFiles validation matches the
expected structure used by functions that access backendFiles.
In `@convex/schemaProposals.ts`:
- Around line 59-64: The patch sets approvedAt to row.approvedAt ?? now when
marking a proposal IMPLEMENTED, which implicitly auto-approves proposals that
were never explicitly approved; decide the intended behavior and implement it:
if explicit approval is required, change the update logic (for the ctx.db.patch
call using args.schemaProposalId and fields
approvedAt/status/implementedAt/updatedAt) to NOT set approvedAt when
row.approvedAt is absent and instead reject or surface an error, or else leave
approvedAt null; if auto-approve is intended, add a clarifying comment next to
the approvedAt assignment explaining that implementing also sets approvedAt to
now when missing to indicate auto-approval.
In `@src/agents/wants-backend.ts`:
- Around line 1-3: The BACKEND_INTENT regex is too broad; replace it with two
regexes (e.g., STRONG_BACKEND_INTENT and WEAK_BACKEND_INTENT) and change the
matching logic to require at least one strong signal to trigger backend
bootstrapping (do not trigger on weak-only matches). Update the declaration that
currently defines BACKEND_INTENT to instead define the two regex constants
(moving strong anchors like "database", "backend", "authentication", "postgres",
"supabase", "prisma" into STRONG_BACKEND_INTENT and generic terms like
"queries", "mutations", "schema" into WEAK_BACKEND_INTENT) and modify every
usage site that references BACKEND_INTENT (search for BACKEND_INTENT in this
file/agent logic) to perform a check like: if (STRONG_BACKEND_INTENT.test(text))
{ bootstrap } else { do not bootstrap } (or include additional conditional logic
only if you want to allow weak signals combined with other heuristics).
🪄 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
Run ID: 4bfbc700-dbbb-4723-bed4-4a579d01cbea
⛔ Files ignored due to path filters (2)
bun.lockis excluded by!**/*.lockconvex/_generated/api.d.tsis excluded by!**/_generated/**
📒 Files selected for processing (19)
.cursor/hooks/state/continual-learning.jsonconvex/messages.tsconvex/projects.tsconvex/schema.tsconvex/schemaProposals.tspackage.jsonsrc/agents/backend-agent.tssrc/agents/index.tssrc/agents/schema-proposal-agent.tssrc/agents/wants-backend.tssrc/app/api/agent/run/route.tssrc/inngest/functions.tssrc/modules/home/ui/components/project-form.tsxsrc/modules/projects/ui/components/message-form.tsxsrc/prompt.tssrc/prompts/backend/convex-backend.tssrc/prompts/backend/fullstack.tssrc/prompts/backend/index.tssrc/prompts/backend/schema-proposal.ts
| { | ||
| "version": 1, | ||
| "lastRunAtMs": 0, | ||
| "turnsSinceLastRun": 1, | ||
| "lastTranscriptMtimeMs": null, | ||
| "lastProcessedGenerationId": "02f2727d-db77-45c6-bb2d-4163883b764f", | ||
| "trialStartedAtMs": null | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
git rm .cursor/hooks/state/continual-learning.jsonRepository: Zapdev-labs/zapdev
Length of output: 110
🏁 Script executed:
#!/bin/bash
# Description: Check .gitignore for .cursor/ entry and find other .cursor files
echo "=== Checking .gitignore for .cursor/ entry ==="
if [ -f .gitignore ]; then
rg -n '\.cursor' .gitignore || echo "No .cursor entry found in .gitignore"
else
echo ".gitignore file not found"
fi
echo -e "\n=== Finding all .cursor files in the repository ==="
fd -H -t f . .cursor 2>/dev/null || echo "No .cursor files found"Repository: Zapdev-labs/zapdev
Length of output: 321
Add .cursor/hooks/state/ to .gitignore to prevent committing IDE state files.
The continual-learning.json file has been removed from the PR. However, to prevent similar auto-generated IDE state files from being accidentally committed in the future, add the following to .gitignore:
+# Cursor IDE state files
+.cursor/hooks/state/Note: The other .cursor/ files (rules and configuration) appear to be intentional and should remain in the repository. Only the state files in .cursor/hooks/state/ should be excluded.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In @.cursor/hooks/state/continual-learning.json around lines 1 - 8, Add a
gitignore rule to exclude the IDE/state files under .cursor/hooks/state/ by
appending an entry like `.cursor/hooks/state/` to .gitignore; ensure you only
ignore the state directory (not other .cursor files like rules or configuration)
and include a short comment explaining it's to prevent committing auto-generated
IDE state files so reviewers understand the intent.
| export const setHasBackendForUser = mutation({ | ||
| args: { | ||
| userId: v.string(), | ||
| projectId: v.id("projects"), | ||
| hasBackend: v.boolean(), | ||
| }, | ||
| handler: async (ctx, args) => { | ||
| const project = await ctx.db.get(args.projectId); | ||
| if (!project || project.userId !== args.userId) { | ||
| throw new Error("Unauthorized"); | ||
| } | ||
| await ctx.db.patch(args.projectId, { | ||
| hasBackend: args.hasBackend, | ||
| updatedAt: Date.now(), | ||
| }); | ||
| }, |
There was a problem hiding this comment.
Authorization is bypassable because identity is caller-controlled.
Line 477 takes userId from input, and Lines 483-484 trust it for auth without requireAuth(ctx). This permits spoofing ownership checks.
Suggested fix (bind auth to session, not args)
export const setHasBackendForUser = mutation({
args: {
- userId: v.string(),
projectId: v.id("projects"),
hasBackend: v.boolean(),
},
handler: async (ctx, args) => {
+ const userId = await requireAuth(ctx);
const project = await ctx.db.get(args.projectId);
- if (!project || project.userId !== args.userId) {
+ if (!project || project.userId !== userId) {
throw new Error("Unauthorized");
}
await ctx.db.patch(args.projectId, {
hasBackend: args.hasBackend,
updatedAt: Date.now(),
});
},
});📝 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.
| export const setHasBackendForUser = mutation({ | |
| args: { | |
| userId: v.string(), | |
| projectId: v.id("projects"), | |
| hasBackend: v.boolean(), | |
| }, | |
| handler: async (ctx, args) => { | |
| const project = await ctx.db.get(args.projectId); | |
| if (!project || project.userId !== args.userId) { | |
| throw new Error("Unauthorized"); | |
| } | |
| await ctx.db.patch(args.projectId, { | |
| hasBackend: args.hasBackend, | |
| updatedAt: Date.now(), | |
| }); | |
| }, | |
| export const setHasBackendForUser = mutation({ | |
| args: { | |
| projectId: v.id("projects"), | |
| hasBackend: v.boolean(), | |
| }, | |
| handler: async (ctx, args) => { | |
| const userId = await requireAuth(ctx); | |
| const project = await ctx.db.get(args.projectId); | |
| if (!project || project.userId !== userId) { | |
| throw new Error("Unauthorized"); | |
| } | |
| await ctx.db.patch(args.projectId, { | |
| hasBackend: args.hasBackend, | |
| updatedAt: Date.now(), | |
| }); | |
| }, | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@convex/projects.ts` around lines 475 - 490, The mutation setHasBackendForUser
currently trusts the caller-supplied userId arg for authorization which is
spoofable; update the handler to call requireAuth(ctx) (or otherwise read
ctx.auth.userId) and use the authenticated user id to authorize (compare
ctx.auth.userId to project.userId) instead of args.userId, and remove or ignore
the userId arg in args; also ensure you still fetch the project via
ctx.db.get(args.projectId) and throw Unauthorized if the authenticated user
isn't the owner before patching hasBackend/updatedAt.
| while ((match = fileRegex.exec(text)) !== null) { | ||
| const path = match[1]; | ||
| const content = match[2].trim(); | ||
| files[path] = content; | ||
| } |
There was a problem hiding this comment.
Validate generated file paths before accepting them.
path from model output is trusted as-is. A prompt-injected response can emit unexpected targets (../, hidden config paths, etc.). Restrict to an allowlist/prefix (for example convex/) and normalize/reject unsafe paths before adding them to files.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/agents/backend-agent.ts` around lines 100 - 104, The loop that consumes
model output (while ((match = fileRegex.exec(text)) ...) trusts match[1] as a
file path and writes it into files[path]; instead validate and normalize each
path before accepting: reject absolute paths or any path containing "..", null
bytes, or path separators that escape the intended workspace, enforce an
allowlist/prefix (e.g., must start with "convex/"), and normalize (using a path
normalization helper like validateAndNormalizePath or path.posix.normalize) to
collapse any "../" before adding to files; if validation fails, skip the entry
and log or surface an error instead of writing to files.
| if (!text.includes("<schema_proposal>")) { | ||
| console.error("[SCHEMA PROPOSAL] Invalid response - missing schema_proposal tag"); | ||
| return { | ||
| schemaProposal: text, | ||
| success: false, | ||
| error: "Invalid schema proposal format", | ||
| }; | ||
| } |
There was a problem hiding this comment.
Success validation is too weak for malformed/truncated model output.
Checking only the opening tag can return success: true even when the closing tag/content is missing. Validate both opening and closing tags (or parse block first) before marking success.
Suggested guard
- if (!text.includes("<schema_proposal>")) {
+ const hasProposalBlock = /<schema_proposal>[\s\S]*<\/schema_proposal>/.test(text);
+ if (!hasProposalBlock) {📝 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.
| if (!text.includes("<schema_proposal>")) { | |
| console.error("[SCHEMA PROPOSAL] Invalid response - missing schema_proposal tag"); | |
| return { | |
| schemaProposal: text, | |
| success: false, | |
| error: "Invalid schema proposal format", | |
| }; | |
| } | |
| const hasProposalBlock = /<schema_proposal>[\s\S]*<\/schema_proposal>/.test(text); | |
| if (!hasProposalBlock) { | |
| console.error("[SCHEMA PROPOSAL] Invalid response - missing schema_proposal tag"); | |
| return { | |
| schemaProposal: text, | |
| success: false, | |
| error: "Invalid schema proposal format", | |
| }; | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/agents/schema-proposal-agent.ts` around lines 50 - 57, The current
validation in schema-proposal-agent.ts only checks for the opening
"<schema_proposal>" in the variable text which allows malformed or truncated
model output to be treated as successful; update the validation in the handler
that returns { schemaProposal, success, error } to ensure both the opening
"<schema_proposal>" and the corresponding closing "</schema_proposal>" exist and
that the content between them can be extracted (e.g., locate start/end indices
or use a simple regex to parse the block) before setting success=true and
returning schemaProposal; if parsing fails or the closing tag is missing, return
success=false and an explanatory error.
| const fieldMatches = fieldsSection.matchAll(/- `([^`]+)`: (\w+)/g); | ||
| for (const fieldMatch of fieldMatches) { | ||
| fields.push(`${fieldMatch[1]}: ${fieldMatch[2]}`); |
There was a problem hiding this comment.
Field parsing regex drops real Convex validator types.
(\w+) only captures word tokens, so validators like v.id("users") or v.optional(v.string()) are truncated. This degrades parsedTables.fields quality and downstream proposal metadata.
Suggested regex hardening
- const fieldMatches = fieldsSection.matchAll(/- `([^`]+)`: (\w+)/g);
+ const fieldMatches = fieldsSection.matchAll(/- `([^`]+)`: ([^-\\n]+?)(?:\\s-\\s.*)?$/gm);📝 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.
| const fieldMatches = fieldsSection.matchAll(/- `([^`]+)`: (\w+)/g); | |
| for (const fieldMatch of fieldMatches) { | |
| fields.push(`${fieldMatch[1]}: ${fieldMatch[2]}`); | |
| const fieldMatches = fieldsSection.matchAll(/- `([^`]+)`: ([^-\n]+?)(?:\s-\s.*)?$/gm); | |
| for (const fieldMatch of fieldMatches) { | |
| fields.push(`${fieldMatch[1]}: ${fieldMatch[2]}`); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/agents/schema-proposal-agent.ts` around lines 121 - 123, The current
regex in fieldsSection.matchAll uses (\w+) which only captures word tokens and
drops complex Convex validator expressions; update the pattern to capture the
entire validator expression (e.g. /- `([^`]+)`: ([^\n]+)/g or /- `([^`]+)`:
(.+)/g) and then trim the captured group before pushing into fields so
validators like v.id("users") or v.optional(v.string()) are preserved (change
the match in the fieldsSection.matchAll call and use
fields.push(`${fieldMatch[1]}: ${fieldMatch[2].trim()}`) in the same block that
handles fieldMatches).
| messageId: | ||
| typeof messageId === "string" && messageId.trim().length > 0 | ||
| ? messageId | ||
| : undefined, |
There was a problem hiding this comment.
Trimmed validation is not applied to the value being sent.
Line 41 validates messageId.trim() but Line 42 forwards untrimmed messageId. This can leak whitespace into downstream lookups.
Suggested fix
messageId:
typeof messageId === "string" && messageId.trim().length > 0
- ? messageId
+ ? messageId.trim()
: undefined,📝 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.
| messageId: | |
| typeof messageId === "string" && messageId.trim().length > 0 | |
| ? messageId | |
| : undefined, | |
| messageId: | |
| typeof messageId === "string" && messageId.trim().length > 0 | |
| ? messageId.trim() | |
| : undefined, |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/app/api/agent/run/route.ts` around lines 40 - 43, The messageId
validation checks messageId.trim() but forwards the original untrimmed
messageId; update the assignment for the messageId property so it sends the
trimmed value (e.g., use messageId.trim()) when typeof messageId === "string"
and trimmed length > 0. Modify the object construction in the route handler
where messageId is set to ensure the trimmed string is forwarded (reference the
messageId variable and the messageId: property in this file). Ensure you still
set undefined when the trimmed value is empty.
| const triggerMessageId = await step.run("resolve-trigger-message", async () => { | ||
| const explicit = event.data.messageId as string | undefined; | ||
| if (explicit && explicit.length > 0) { | ||
| return explicit as Id<"messages">; | ||
| } | ||
| const convex = getConvexClient(); | ||
| const messages = await convex.query(api.messages.listForUser, { | ||
| userId, | ||
| projectId, | ||
| }); | ||
| const lastUser = [...messages].reverse().find((m) => m.role === "USER"); | ||
| return lastUser?._id ?? null; | ||
| }); |
There was a problem hiding this comment.
Unsafe cast of messageId to Id<"messages">.
The event.data.messageId string is cast directly to Id<"messages"> after only checking that it has length > 0. If the string is not a valid Convex document ID, the subsequent mutation at line 311 will fail with an opaque error. Consider validating the ID format or letting the mutation's ownership check (line 29-31 in schemaProposals.ts) handle it gracefully.
🛡️ Proposed defensive check
const explicit = event.data.messageId as string | undefined;
- if (explicit && explicit.length > 0) {
+ if (explicit && explicit.length > 0 && explicit.startsWith("k")) {
+ // Basic Convex ID format check - IDs typically start with specific prefixes
return explicit as Id<"messages">;
}Note: Alternatively, rely on the existing message ownership validation in createForUser to throw "Message not found" for invalid IDs.
📝 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.
| const triggerMessageId = await step.run("resolve-trigger-message", async () => { | |
| const explicit = event.data.messageId as string | undefined; | |
| if (explicit && explicit.length > 0) { | |
| return explicit as Id<"messages">; | |
| } | |
| const convex = getConvexClient(); | |
| const messages = await convex.query(api.messages.listForUser, { | |
| userId, | |
| projectId, | |
| }); | |
| const lastUser = [...messages].reverse().find((m) => m.role === "USER"); | |
| return lastUser?._id ?? null; | |
| }); | |
| const triggerMessageId = await step.run("resolve-trigger-message", async () => { | |
| const explicit = event.data.messageId as string | undefined; | |
| if (explicit && explicit.length > 0 && explicit.startsWith("k")) { | |
| // Basic Convex ID format check - IDs typically start with specific prefixes | |
| return explicit as Id<"messages">; | |
| } | |
| const convex = getConvexClient(); | |
| const messages = await convex.query(api.messages.listForUser, { | |
| userId, | |
| projectId, | |
| }); | |
| const lastUser = [...messages].reverse().find((m) => m.role === "USER"); | |
| return lastUser?._id ?? null; | |
| }); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/inngest/functions.ts` around lines 284 - 296, The code unsafely casts
event.data.messageId to Id<"messages"> when resolving triggerMessageId; instead
validate the incoming ID before casting (or avoid casting) to prevent opaque
failures: update the resolve-trigger-message block (referencing triggerMessageId
and event.data.messageId) to check the ID format/shape used by Convex (or run a
lightweight lookup via getConvexClient + api.messages.listForUser) and only
return a typed Id<"messages"> when the value is valid, otherwise return null;
alternatively, keep the value as string | null and let the downstream mutation
(createForUser / ownership checks) surface a clear "Message not found" error.
| await step.run("finalize-backend-bootstrap", async () => { | ||
| const convex = getConvexClient(); | ||
| await convex.mutation(api.schemaProposals.markImplementedForUser, { | ||
| userId, | ||
| schemaProposalId: schemaProposalRowId, | ||
| }); | ||
| await convex.mutation(api.projects.setHasBackendForUser, { | ||
| userId, | ||
| projectId, | ||
| hasBackend: true, | ||
| }); | ||
| }); | ||
| } |
There was a problem hiding this comment.
Non-atomic mutations may leave inconsistent state.
The finalize-backend-bootstrap step runs two mutations sequentially. If markImplementedForUser succeeds but setHasBackendForUser fails (e.g., network error, rate limit), the schema proposal is marked IMPLEMENTED while project.hasBackend remains false. On retry, the entire bootstrap flow would be skipped (since needsBackendBootstrap checks !project.hasBackend), leaving the schema proposal orphaned as "implemented" without actual project update.
💡 Suggested approach: split into separate steps or add idempotency
- await step.run("finalize-backend-bootstrap", async () => {
- const convex = getConvexClient();
- await convex.mutation(api.schemaProposals.markImplementedForUser, {
- userId,
- schemaProposalId: schemaProposalRowId,
- });
- await convex.mutation(api.projects.setHasBackendForUser, {
- userId,
- projectId,
- hasBackend: true,
- });
- });
+ await step.run("mark-schema-implemented", async () => {
+ const convex = getConvexClient();
+ await convex.mutation(api.schemaProposals.markImplementedForUser, {
+ userId,
+ schemaProposalId: schemaProposalRowId,
+ });
+ });
+ await step.run("set-project-has-backend", async () => {
+ const convex = getConvexClient();
+ await convex.mutation(api.projects.setHasBackendForUser, {
+ userId,
+ projectId,
+ hasBackend: true,
+ });
+ });By splitting into separate Inngest steps, each mutation gets independent retry semantics and the workflow can resume from the correct point after partial failures.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/inngest/functions.ts` around lines 338 - 350, The current
finalize-backend-bootstrap step runs two sequential mutations
(convex.mutation(api.schemaProposals.markImplementedForUser, ...) and
convex.mutation(api.projects.setHasBackendForUser, ...)) which can leave the
system inconsistent if one succeeds and the other fails; change this by
splitting the work into two independent Inngest steps (e.g.,
finalize-mark-implemented and finalize-set-has-backend) so each mutation has its
own retry semantics, or alternatively make the operations atomic/idempotent in
the backend (use a transaction or an idempotent setter on
api.projects.setHasBackendForUser and check/update schemaProposal state to be
safe on retries). Ensure the new steps reference the same identifiers (userId,
schemaProposalId/schemaProposalRowId, projectId) and that needsBackendBootstrap
continues to consult project.hasBackend so workflow resumes correctly after
partial failures.
| - Use ctx.db.insert("table", data) to create | ||
| - Use ctx.db.patch("table", id, updates) for partial updates | ||
| - Use ctx.db.replace("table", id, data) for full replacement | ||
| - Use ctx.db.delete(id) for deletion |
There was a problem hiding this comment.
🧩 Analysis chain
🌐 Web query:
What are the current Convex method signatures for ctx.db.patch, ctx.db.replace, and ctx.db.delete inside mutations?
💡 Result:
The current Convex method signatures for ctx.db inside mutations (GenericDatabaseWriter) are: ctx.db.patch(table: TableName, id: GenericId, value: PatchValue<DocumentByName<DataModel, TableName>>): Promise - Patches an existing document with a shallow merge. Adds new fields, overwrites existing, removes fields set to undefined. Throws if document does not exist. ctx.db.replace(table: TableName, id: GenericId, value: DocumentByName<DataModel, TableName>): Promise (inferred) - Replaces the entire document content, overwriting old value completely. Fields not included are removed. ctx.db.delete(table: TableName, id: GenericId): Promise - Deletes the document by ID. Throws if document does not exist. Note: Legacy overloads without table name (ctx.db.patch(id, value), etc.) exist but are deprecated since convex 1.31.0 (Dec 2025). Current best practice requires explicit table name first. All operations are async and transactional within mutations.
Citations:
- 1: https://docs.convex.dev/database/writing-data
- 2: https://docs.convex.dev/api/interfaces/server.GenericDatabaseWriter
- 3: https://docs.convex.dev/api/interfaces/server.GenericDatabaseWriter.md
ctx.db.delete(id) call is missing the required table parameter.
The delete method shown in the prompt omits the table name as the first argument. The correct signature is ctx.db.delete("table", id). The current guidance will generate invalid code that either fails or relies on deprecated behavior.
The patch and replace method signatures shown are correct.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/prompts/backend/convex-backend.ts` around lines 42 - 45, The prompt
examples incorrectly show ctx.db.delete(id) without the required table name;
update the guidance so all CRUD examples consistently use the correct
signatures: ctx.db.insert("table", data), ctx.db.patch("table", id, updates),
ctx.db.replace("table", id, data), and ctx.db.delete("table", id). Specifically
change the ctx.db.delete example to include the "table" parameter to match the
other examples and avoid generating invalid code.
| Use createOrUpdateFiles to write all files. After all files are created, output: | ||
|
|
||
| <task_summary> | ||
| Generated Convex backend with [N] tables: [table names]. Created [N] query functions, [N] mutation functions, and [N] actions. All functions include proper authentication, authorization, and error handling. | ||
| </task_summary> | ||
| `; |
There was a problem hiding this comment.
Output contract here conflicts with the backend parser contract.
This prompt asks for createOrUpdateFiles + <task_summary>, but src/agents/backend-agent.ts parses <zapdev_file path="..."> blocks. This mismatch can yield empty parsed files and false failure paths.
Suggested prompt contract alignment
-Use createOrUpdateFiles to write all files. After all files are created, output:
+Output all generated files using explicit XML-style file blocks:
+
+<zapdev_file path="convex/schema.ts">
+...full file contents...
+</zapdev_file>
+
+Repeat for every generated file, then output:
<task_summary>
Generated Convex backend with [N] tables: [table names]. Created [N] query functions, [N] mutation functions, and [N] actions. All functions include proper authentication, authorization, and error handling.
</task_summary>📝 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.
| Use createOrUpdateFiles to write all files. After all files are created, output: | |
| <task_summary> | |
| Generated Convex backend with [N] tables: [table names]. Created [N] query functions, [N] mutation functions, and [N] actions. All functions include proper authentication, authorization, and error handling. | |
| </task_summary> | |
| `; | |
| Output all generated files using explicit XML-style file blocks: | |
| <zapdev_file path="convex/schema.ts"> | |
| ...full file contents... | |
| </zapdev_file> | |
| Repeat for every generated file, then output: | |
| <task_summary> | |
| Generated Convex backend with [N] tables: [table names]. Created [N] query functions, [N] mutation functions, and [N] actions. All functions include proper authentication, authorization, and error handling. | |
| </task_summary> | |
| `; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/prompts/backend/convex-backend.ts` around lines 194 - 199, The prompt in
src/prompts/backend/convex-backend.ts uses createOrUpdateFiles + a
<task_summary> block which conflicts with the parser in
src/agents/backend-agent.ts that expects <zapdev_file path="..."> blocks; update
the prompt to emit each file wrapped in <zapdev_file path="..."> with file
contents (one block per file) and move the summary outside or into a separate,
parser-recognized tag so the backend-agent.ts parser can extract files correctly
instead of producing empty parsed files.
There was a problem hiding this comment.
16 issues found across 21 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="src/prompts/backend/convex-backend.ts">
<violation number="1" location="src/prompts/backend/convex-backend.ts:43">
P1: Incorrect Convex API signatures: `ctx.db.patch` and `ctx.db.replace` do not take a table name. Only `ctx.db.insert` does. These should be `ctx.db.patch(id, updates)` and `ctx.db.replace(id, data)`. The prompt's own authorization example on line ~93 correctly uses `ctx.db.patch(args.id, args.updates)` without a table name, contradicting these guidelines. This will cause the AI to generate broken Convex code.</violation>
<violation number="2" location="src/prompts/backend/convex-backend.ts:144">
P2: Inconsistent internal function path: `internal.tasks.sendReminder` references a `tasks` module, but `sendReminder` is defined in root-level `actions.ts` per the file structure and code comments. The correct path would be `internal.actions.sendReminder`. This mismatch will mislead the AI into generating incorrect scheduler references.</violation>
<violation number="3" location="src/prompts/backend/convex-backend.ts:194">
P1: The output instruction here tells the model to `Use createOrUpdateFiles to write all files`, but the parser in `backend-agent.ts` (`parseGeneratedFiles`) expects `<zapdev_file path="...">` XML blocks. This contract mismatch will cause the parser to extract zero files, triggering the `"No files were generated"` failure path.</violation>
</file>
<file name="src/agents/schema-proposal-agent.ts">
<violation number="1" location="src/agents/schema-proposal-agent.ts:50">
P2: This only checks for the opening `<schema_proposal>` tag. A truncated model response (e.g., hitting `maxOutputTokens`) could contain the opening tag but lack the closing tag or complete content, yet still be marked `success: true`. Validate both opening and closing tags to avoid propagating malformed proposals downstream.</violation>
<violation number="2" location="src/agents/schema-proposal-agent.ts:121">
P2: The `(\w+)` capture group only matches word characters, so Convex validator types like `v.id("users")` or `v.optional(v.string())` will be truncated to just `v`. This degrades the parsed field metadata stored in `parsedTables.fields`.</violation>
</file>
<file name="src/agents/backend-agent.ts">
<violation number="1" location="src/agents/backend-agent.ts:101">
P1: File paths parsed from AI output are not validated or sanitized. Since the user prompt influences the AI response, a crafted prompt could induce the model to emit paths containing `..` segments or absolute paths, enabling writes outside the intended `convex/` directory. Add path sanitization to reject traversal sequences and enforce an allowlist prefix.</violation>
</file>
<file name="src/prompts/backend/schema-proposal.ts">
<violation number="1" location="src/prompts/backend/schema-proposal.ts:53">
P2: `v.optional()` makes a field omittable (undefined), not nullable. Convex uses `v.null()` for null values. Labeling this as "nullable fields" will cause the AI to generate schemas that treat optional and nullable as interchangeable, leading to runtime mismatches when code expects `null` but gets `undefined`.</violation>
<violation number="2" location="src/prompts/backend/schema-proposal.ts:124">
P2: The relationship `users → tasks (assigned)` is labeled as "Many-to-many" but the schema uses `assignedTo: v.optional(v.id("users"))` which only allows one assigned user per task — this is a many-to-one (or one-to-many from the user side). The example will teach the AI incorrect relationship terminology.</violation>
</file>
<file name="convex/projects.ts">
<violation number="1" location="convex/projects.ts:475">
P1: This mutation is only called from Inngest but is exposed as a public `mutation`, allowing any unauthenticated client to set `hasBackend` on any project by supplying the owner's userId. Use `internalMutation` instead, which restricts access to server-side callers only.
Note: the existing `createForUser` mutation has the same problem, but this new code shouldn't perpetuate it.</violation>
<violation number="2" location="convex/projects.ts:477">
P1: Authorization check is bypassable — `userId` comes from the caller's args, not from `ctx.auth.getUserIdentity()`. Any client can call this public mutation with an arbitrary `userId` to pass the ownership check. Derive `userId` from the authenticated session instead.</violation>
</file>
<file name="src/inngest/functions.ts">
<violation number="1" location="src/inngest/functions.ts:338">
P2: These two mutations run sequentially in a single Inngest step. If `markImplementedForUser` succeeds but `setHasBackendForUser` fails, a retry will re-execute `markImplementedForUser` on an already-IMPLEMENTED row (which may or may not be idempotent). Split these into separate Inngest steps so each gets independent retry semantics and the workflow can resume from the correct point after partial failure.</violation>
<violation number="2" location="src/inngest/functions.ts:357">
P1: `useFullstackPrompt` is set to `true` based on user intent (`wantsConvexBackend`) even when the backend bootstrap failed or was skipped. This causes the code agent to receive the `FULLSTACK_PROMPT` (which instructs full Convex backend generation) without any schema proposal or pre-loaded backend files, producing Convex code that isn't tracked by the bootstrap pipeline and leaving `project.hasBackend` as `false` — triggering a duplicate bootstrap attempt on the next request.</violation>
</file>
<file name="convex/schemaProposals.ts">
<violation number="1" location="convex/schemaProposals.ts:54">
P2: Missing status guard allows a `REJECTED` proposal to be marked as `IMPLEMENTED`. The `approvedAt ?? now` fallback even backfills the approval timestamp, masking the invalid transition. Add a check that `row.status` is `"APPROVED"` (or at least not `"REJECTED"`) before proceeding.</violation>
</file>
<file name="src/prompts/backend/fullstack.ts">
<violation number="1" location="src/prompts/backend/fullstack.ts:11">
P2: The prompt hardcodes `Next.js 15.3.3` but the project uses `next@16.2.1`. Generated code will target the wrong major version, which may produce incompatible patterns or miss new APIs.</violation>
<violation number="2" location="src/prompts/backend/fullstack.ts:132">
P1: `userId: v.id("users")` expects a Convex document ID, but the query (line 151) and mutation (line 175) examples both assign `identity.subject` to it — a plain JWT subject string. This will fail Convex's runtime type validation. Either change the schema field to `v.string()`, or add a user-lookup step in the query/mutation to resolve the auth subject to a Convex `_id`.</violation>
<violation number="3" location="src/prompts/backend/fullstack.ts:197">
P2: The layout example creates a new `ConvexReactClient` inline instead of importing the one from `lib/convex.ts` (defined earlier in the prompt). This results in two separate client instances in the generated code.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review, or fix all with cubic.
|
|
||
| ## Output | ||
|
|
||
| Use createOrUpdateFiles to write all files. After all files are created, output: |
There was a problem hiding this comment.
P1: The output instruction here tells the model to Use createOrUpdateFiles to write all files, but the parser in backend-agent.ts (parseGeneratedFiles) expects <zapdev_file path="..."> XML blocks. This contract mismatch will cause the parser to extract zero files, triggering the "No files were generated" failure path.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/prompts/backend/convex-backend.ts, line 194:
<comment>The output instruction here tells the model to `Use createOrUpdateFiles to write all files`, but the parser in `backend-agent.ts` (`parseGeneratedFiles`) expects `<zapdev_file path="...">` XML blocks. This contract mismatch will cause the parser to extract zero files, triggering the `"No files were generated"` failure path.</comment>
<file context>
@@ -0,0 +1,199 @@
+
+## Output
+
+Use createOrUpdateFiles to write all files. After all files are created, output:
+
+<task_summary>
</file context>
| let match; | ||
|
|
||
| while ((match = fileRegex.exec(text)) !== null) { | ||
| const path = match[1]; |
There was a problem hiding this comment.
P1: File paths parsed from AI output are not validated or sanitized. Since the user prompt influences the AI response, a crafted prompt could induce the model to emit paths containing .. segments or absolute paths, enabling writes outside the intended convex/ directory. Add path sanitization to reject traversal sequences and enforce an allowlist prefix.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/agents/backend-agent.ts, line 101:
<comment>File paths parsed from AI output are not validated or sanitized. Since the user prompt influences the AI response, a crafted prompt could induce the model to emit paths containing `..` segments or absolute paths, enabling writes outside the intended `convex/` directory. Add path sanitization to reject traversal sequences and enforce an allowlist prefix.</comment>
<file context>
@@ -0,0 +1,107 @@
+ let match;
+
+ while ((match = fileRegex.exec(text)) !== null) {
+ const path = match[1];
+ const content = match[2].trim();
+ files[path] = content;
</file context>
| }, | ||
| }); | ||
|
|
||
| export const setHasBackendForUser = mutation({ |
There was a problem hiding this comment.
P1: This mutation is only called from Inngest but is exposed as a public mutation, allowing any unauthenticated client to set hasBackend on any project by supplying the owner's userId. Use internalMutation instead, which restricts access to server-side callers only.
Note: the existing createForUser mutation has the same problem, but this new code shouldn't perpetuate it.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At convex/projects.ts, line 475:
<comment>This mutation is only called from Inngest but is exposed as a public `mutation`, allowing any unauthenticated client to set `hasBackend` on any project by supplying the owner's userId. Use `internalMutation` instead, which restricts access to server-side callers only.
Note: the existing `createForUser` mutation has the same problem, but this new code shouldn't perpetuate it.</comment>
<file context>
@@ -472,6 +472,24 @@ export const createForUser = mutation({
},
});
+export const setHasBackendForUser = mutation({
+ args: {
+ userId: v.string(),
</file context>
|
|
||
| const useFullstackPrompt = | ||
| Boolean(project?.hasBackend) || | ||
| wantsConvexBackend(userPrompt) || |
There was a problem hiding this comment.
P1: useFullstackPrompt is set to true based on user intent (wantsConvexBackend) even when the backend bootstrap failed or was skipped. This causes the code agent to receive the FULLSTACK_PROMPT (which instructs full Convex backend generation) without any schema proposal or pre-loaded backend files, producing Convex code that isn't tracked by the bootstrap pipeline and leaving project.hasBackend as false — triggering a duplicate bootstrap attempt on the next request.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/inngest/functions.ts, line 357:
<comment>`useFullstackPrompt` is set to `true` based on user intent (`wantsConvexBackend`) even when the backend bootstrap failed or was skipped. This causes the code agent to receive the `FULLSTACK_PROMPT` (which instructs full Convex backend generation) without any schema proposal or pre-loaded backend files, producing Convex code that isn't tracked by the bootstrap pipeline and leaving `project.hasBackend` as `false` — triggering a duplicate bootstrap attempt on the next request.</comment>
<file context>
@@ -255,8 +266,99 @@ export const codeAgentFunction = inngest.createFunction(
+
+ const useFullstackPrompt =
+ Boolean(project?.hasBackend) ||
+ wantsConvexBackend(userPrompt) ||
+ Object.keys(backendPreload).length > 0;
+
</file context>
| wantsConvexBackend(userPrompt) || | |
| (needsBackendBootstrap && Object.keys(backendPreload).length > 0) || |
| - Use ctx.db.patch("table", id, updates) for partial updates | ||
| - Use ctx.db.replace("table", id, data) for full replacement |
There was a problem hiding this comment.
P1: Incorrect Convex API signatures: ctx.db.patch and ctx.db.replace do not take a table name. Only ctx.db.insert does. These should be ctx.db.patch(id, updates) and ctx.db.replace(id, data). The prompt's own authorization example on line ~93 correctly uses ctx.db.patch(args.id, args.updates) without a table name, contradicting these guidelines. This will cause the AI to generate broken Convex code.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/prompts/backend/convex-backend.ts, line 43:
<comment>Incorrect Convex API signatures: `ctx.db.patch` and `ctx.db.replace` do not take a table name. Only `ctx.db.insert` does. These should be `ctx.db.patch(id, updates)` and `ctx.db.replace(id, data)`. The prompt's own authorization example on line ~93 correctly uses `ctx.db.patch(args.id, args.updates)` without a table name, contradicting these guidelines. This will cause the AI to generate broken Convex code.</comment>
<file context>
@@ -0,0 +1,199 @@
+
+### Mutation Guidelines
+- Use ctx.db.insert("table", data) to create
+- Use ctx.db.patch("table", id, updates) for partial updates
+- Use ctx.db.replace("table", id, data) for full replacement
+- Use ctx.db.delete(id) for deletion
</file context>
| - Use ctx.db.patch("table", id, updates) for partial updates | |
| - Use ctx.db.replace("table", id, data) for full replacement | |
| - Use ctx.db.patch(id, updates) for partial updates | |
| - Use ctx.db.replace(id, data) for full replacement |
|
|
||
| if (backendResult.success && Object.keys(backendResult.files).length > 0) { | ||
| backendPreload = backendResult.files; | ||
| await step.run("finalize-backend-bootstrap", async () => { |
There was a problem hiding this comment.
P2: These two mutations run sequentially in a single Inngest step. If markImplementedForUser succeeds but setHasBackendForUser fails, a retry will re-execute markImplementedForUser on an already-IMPLEMENTED row (which may or may not be idempotent). Split these into separate Inngest steps so each gets independent retry semantics and the workflow can resume from the correct point after partial failure.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/inngest/functions.ts, line 338:
<comment>These two mutations run sequentially in a single Inngest step. If `markImplementedForUser` succeeds but `setHasBackendForUser` fails, a retry will re-execute `markImplementedForUser` on an already-IMPLEMENTED row (which may or may not be idempotent). Split these into separate Inngest steps so each gets independent retry semantics and the workflow can resume from the correct point after partial failure.</comment>
<file context>
@@ -255,8 +266,99 @@ export const codeAgentFunction = inngest.createFunction(
+
+ if (backendResult.success && Object.keys(backendResult.files).length > 0) {
+ backendPreload = backendResult.files;
+ await step.run("finalize-backend-bootstrap", async () => {
+ const convex = getConvexClient();
+ await convex.mutation(api.schemaProposals.markImplementedForUser, {
</file context>
| schemaProposalId: v.id("schemaProposals"), | ||
| }, | ||
| handler: async (ctx, args) => { | ||
| const row = await ctx.db.get(args.schemaProposalId); |
There was a problem hiding this comment.
P2: Missing status guard allows a REJECTED proposal to be marked as IMPLEMENTED. The approvedAt ?? now fallback even backfills the approval timestamp, masking the invalid transition. Add a check that row.status is "APPROVED" (or at least not "REJECTED") before proceeding.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At convex/schemaProposals.ts, line 54:
<comment>Missing status guard allows a `REJECTED` proposal to be marked as `IMPLEMENTED`. The `approvedAt ?? now` fallback even backfills the approval timestamp, masking the invalid transition. Add a check that `row.status` is `"APPROVED"` (or at least not `"REJECTED"`) before proceeding.</comment>
<file context>
@@ -0,0 +1,66 @@
+ schemaProposalId: v.id("schemaProposals"),
+ },
+ handler: async (ctx, args) => {
+ const row = await ctx.db.get(args.schemaProposalId);
+ if (!row || row.userId !== args.userId) {
+ throw new Error("Unauthorized");
</file context>
| }); | ||
|
|
||
| // Schedule a reminder | ||
| await ctx.scheduler.runAt(args.dueDate - 3600000, internal.tasks.sendReminder, { |
There was a problem hiding this comment.
P2: Inconsistent internal function path: internal.tasks.sendReminder references a tasks module, but sendReminder is defined in root-level actions.ts per the file structure and code comments. The correct path would be internal.actions.sendReminder. This mismatch will mislead the AI into generating incorrect scheduler references.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/prompts/backend/convex-backend.ts, line 144:
<comment>Inconsistent internal function path: `internal.tasks.sendReminder` references a `tasks` module, but `sendReminder` is defined in root-level `actions.ts` per the file structure and code comments. The correct path would be `internal.actions.sendReminder`. This mismatch will mislead the AI into generating incorrect scheduler references.</comment>
<file context>
@@ -0,0 +1,199 @@
+ });
+
+ // Schedule a reminder
+ await ctx.scheduler.runAt(args.dueDate - 3600000, internal.tasks.sendReminder, {
+ taskId,
+ });
</file context>
| import { ConvexProvider, ConvexReactClient } from "convex/react"; | ||
| import { api } from "@/convex/_generated/api"; | ||
|
|
||
| const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!); |
There was a problem hiding this comment.
P2: The layout example creates a new ConvexReactClient inline instead of importing the one from lib/convex.ts (defined earlier in the prompt). This results in two separate client instances in the generated code.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/prompts/backend/fullstack.ts, line 197:
<comment>The layout example creates a new `ConvexReactClient` inline instead of importing the one from `lib/convex.ts` (defined earlier in the prompt). This results in two separate client instances in the generated code.</comment>
<file context>
@@ -0,0 +1,284 @@
+import { ConvexProvider, ConvexReactClient } from "convex/react";
+import { api } from "@/convex/_generated/api";
+
+const convex = new ConvexReactClient(process.env.NEXT_PUBLIC_CONVEX_URL!);
+
+export default function RootLayout({ children }: { children: React.ReactNode }) {
</file context>
| ## Tech Stack | ||
|
|
||
| ### Frontend | ||
| - Next.js 15.3.3 with App Router |
There was a problem hiding this comment.
P2: The prompt hardcodes Next.js 15.3.3 but the project uses next@16.2.1. Generated code will target the wrong major version, which may produce incompatible patterns or miss new APIs.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/prompts/backend/fullstack.ts, line 11:
<comment>The prompt hardcodes `Next.js 15.3.3` but the project uses `next@16.2.1`. Generated code will target the wrong major version, which may produce incompatible patterns or miss new APIs.</comment>
<file context>
@@ -0,0 +1,284 @@
+## Tech Stack
+
+### Frontend
+- Next.js 15.3.3 with App Router
+- React 19
+- TypeScript (strict)
</file context>
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (1)
src/inngest/functions.ts (1)
280-281: Broad regex inwantsConvexBackendmay trigger expensive bootstrapping on incidental keyword mentions.The
wantsConvexBackendfunction (persrc/agents/wants-backend.ts) uses regex matching for keywords like "database", "login", "queries". A prompt like "debug my database connection timeout" would trigger the full bootstrapping flow (schema proposal agent + backend implementer + Convex mutations).Consider either:
- Tightening the regex to require more explicit backend intent
- Adding a lightweight LLM classifier for ambiguous cases
- Documenting this behavior as intentional for broad coverage
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/inngest/functions.ts` around lines 280 - 281, The current broad regex in wantsConvexBackend causes incidental keywords to trigger needsBackendBootstrap (the Boolean using wantsConvexBackend in the declaration of needsBackendBootstrap), so update the wantsConvexBackend function to reduce false positives: either tighten the regex to require stronger intent phrases (e.g., match whole-word phrases like "build backend", "create database schema", "implement login/auth", or require verbs + backend nouns) or implement a lightweight LLM/heuristic secondary check for ambiguous matches and return true only when both checks indicate backend intent; ensure the needsBackendBootstrap line continues to call wantsConvexBackend(userPrompt) but relies on the revised stricter logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/inngest/functions.ts`:
- Around line 407-408: The object literal sets the `system` property twice
(first as `useFullstackPrompt ? FULLSTACK_PROMPT : PROMPT`, then immediately as
`PROMPT`), which overwrites the conditional value; remove the duplicate
unconditional `system: PROMPT` so the code uses the conditional `system` value
based on `useFullstackPrompt` (look for the object containing `system`,
`useFullstackPrompt`, `FULLSTACK_PROMPT`, and `PROMPT` in this file and keep
only the conditional `system` entry).
- Around line 303-334: The flow currently persists a schema proposal with status
"PENDING" and immediately calls runBackendImplementerAgent even when
parseSchemaProposal may return no tables; update the logic in the block handling
schemaProposalResult.success so that after calling parseSchemaProposal you
validate parsed.tables.length > 0 (and/or parsed.relationships as appropriate)
before invoking runBackendImplementerAgent, and if validation fails do not call
runBackendImplementerAgent—either set a different proposal status or require
explicit approval; reference parseSchemaProposal, schemaProposalResult, the
persisted proposal step ("persist-schema-proposal") and
runBackendImplementerAgent when making the change.
---
Nitpick comments:
In `@src/inngest/functions.ts`:
- Around line 280-281: The current broad regex in wantsConvexBackend causes
incidental keywords to trigger needsBackendBootstrap (the Boolean using
wantsConvexBackend in the declaration of needsBackendBootstrap), so update the
wantsConvexBackend function to reduce false positives: either tighten the regex
to require stronger intent phrases (e.g., match whole-word phrases like "build
backend", "create database schema", "implement login/auth", or require verbs +
backend nouns) or implement a lightweight LLM/heuristic secondary check for
ambiguous matches and return true only when both checks indicate backend intent;
ensure the needsBackendBootstrap line continues to call
wantsConvexBackend(userPrompt) but relies on the revised stricter logic.
🪄 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
Run ID: 79ea7413-e7ad-43d7-a463-9c798b9a96d9
📒 Files selected for processing (5)
convex/messages.tsconvex/projects.tssrc/inngest/functions.tssrc/modules/home/ui/components/project-form.tsxsrc/modules/projects/ui/components/message-form.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- src/modules/projects/ui/components/message-form.tsx
| if (schemaProposalResult.success) { | ||
| schemaProposalText = schemaProposalResult.schemaProposal; | ||
| const parsed = parseSchemaProposal(schemaProposalResult.schemaProposal); | ||
| const schemaProposalRowId = await step.run("persist-schema-proposal", async () => { | ||
| const convex = getConvexClient(); | ||
| return await convex.mutation(api.schemaProposals.createForUser, { | ||
| userId, | ||
| projectId, | ||
| messageId: triggerMessageId, | ||
| proposal: schemaProposalResult.schemaProposal, | ||
| parsedTables: | ||
| parsed.tables.length > 0 | ||
| ? parsed.tables.map((t) => ({ | ||
| name: t.name, | ||
| purpose: t.purpose, | ||
| fields: t.fields, | ||
| indexes: t.indexes, | ||
| })) | ||
| : undefined, | ||
| parsedRelationships: | ||
| parsed.relationships.length > 0 ? parsed.relationships : undefined, | ||
| status: "PENDING", | ||
| }); | ||
| }); | ||
|
|
||
| const backendResult = await step.run("backend-implementer", () => | ||
| runBackendImplementerAgent( | ||
| userPrompt, | ||
| schemaProposalResult.schemaProposal, | ||
| plan || undefined | ||
| ) | ||
| ); |
There was a problem hiding this comment.
Schema proposal proceeds to backend generation without validation or approval.
The flow runs runBackendImplementerAgent immediately after creating the schema proposal with status: "PENDING" (line 324), without any user approval step. Additionally, parseSchemaProposal silently returns empty arrays if the LLM output is malformed (per src/agents/schema-proposal-agent.ts:100), meaning invalid proposals still trigger backend generation.
This contradicts the <convex_context> message at line 382 which states "Schema design (approved)". Consider either:
- Adding validation that
parsed.tables.length > 0before proceeding - Removing the "(approved)" label if auto-approval is intentional
🛡️ Optional: Add validation before backend generation
if (schemaProposalResult.success) {
schemaProposalText = schemaProposalResult.schemaProposal;
const parsed = parseSchemaProposal(schemaProposalResult.schemaProposal);
+
+ if (parsed.tables.length === 0) {
+ console.warn("[BACKEND] Schema proposal parsing failed - skipping backend generation");
+ } else {
const schemaProposalRowId = await step.run("persist-schema-proposal", async () => {
// ... existing code
});
+ }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/inngest/functions.ts` around lines 303 - 334, The flow currently persists
a schema proposal with status "PENDING" and immediately calls
runBackendImplementerAgent even when parseSchemaProposal may return no tables;
update the logic in the block handling schemaProposalResult.success so that
after calling parseSchemaProposal you validate parsed.tables.length > 0 (and/or
parsed.relationships as appropriate) before invoking runBackendImplementerAgent,
and if validation fails do not call runBackendImplementerAgent—either set a
different proposal status or require explicit approval; reference
parseSchemaProposal, schemaProposalResult, the persisted proposal step
("persist-schema-proposal") and runBackendImplementerAgent when making the
change.
| system: useFullstackPrompt ? FULLSTACK_PROMPT : PROMPT, | ||
| system: PROMPT, |
There was a problem hiding this comment.
Critical: Duplicate system property causes build failure and incorrect behavior.
Line 407 conditionally sets system based on useFullstackPrompt, but line 408 unconditionally overwrites it with PROMPT. This causes:
- TypeScript compilation error (pipeline is failing)
- The fullstack prompt will never be used even when
useFullstackPromptis true
🐛 Proposed fix — remove the duplicate property
const codeAgent = createAgent<AgentState>({
name: "code-agent",
description: "An expert coding agent",
system: useFullstackPrompt ? FULLSTACK_PROMPT : PROMPT,
- system: PROMPT,
tool_choice: "auto",📝 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.
| system: useFullstackPrompt ? FULLSTACK_PROMPT : PROMPT, | |
| system: PROMPT, | |
| const codeAgent = createAgent<AgentState>({ | |
| name: "code-agent", | |
| description: "An expert coding agent", | |
| system: useFullstackPrompt ? FULLSTACK_PROMPT : PROMPT, | |
| tool_choice: "auto", |
🧰 Tools
🪛 Biome (2.4.10)
[error] 407-407: This property is later overwritten by an object member with the same name.
(lint/suspicious/noDuplicateObjectKeys)
🪛 GitHub Actions: CI
[error] 408-408: TypeScript type check failed: An object literal cannot have multiple properties with the same name. Duplicate 'system' property at line 408.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/inngest/functions.ts` around lines 407 - 408, The object literal sets the
`system` property twice (first as `useFullstackPrompt ? FULLSTACK_PROMPT :
PROMPT`, then immediately as `PROMPT`), which overwrites the conditional value;
remove the duplicate unconditional `system: PROMPT` so the code uses the
conditional `system` value based on `useFullstackPrompt` (look for the object
containing `system`, `useFullstackPrompt`, `FULLSTACK_PROMPT`, and `PROMPT` in
this file and keep only the conditional `system` entry).
Summary by CodeRabbit
New Features
Chores