Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions src/core/review/artifacts/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/**
* Shared shape for the three on-disk review artifacts. Both GitHub and
* GitLab pipelines pre-compute the same trio (diff, existing comments,
* description) and write them under `${tempDir}/droid-prompts/`, then
* pass the resulting paths into the Pass 1 / Pass 2 prompts.
*
* The fetch mechanics differ substantially per platform (git+gh CLI vs
* REST API), so we don't try to share the fetchers — only the path
* shape and the disk-write helper.
*/
export type ReviewArtifactPaths = {
diffPath: string;
commentsPath: string;
descriptionPath: string;
};

/**
* Raw content for each of the three artifacts, before they're written
* to disk by `writeReviewArtifacts`.
*/
export type ReviewArtifactContents = {
diff: string;
comments: unknown; // JSON-serializable
description: string;
};
41 changes: 41 additions & 0 deletions src/core/review/artifacts/write.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import * as fs from "fs/promises";
import * as path from "path";
import type { ReviewArtifactContents, ReviewArtifactPaths } from "./types";

/**
* Naming convention for review-artifact files on disk. Each platform
* gets its own basename for the diff and description (pr.diff /
* mr.diff, pr_description.txt / mr_description.txt) but the existing
* comments file is platform-neutral.
*/
export type ReviewArtifactNames = {
diff: string;
comments: string;
description: string;
};

/**
* Write the three review-artifact files into `outDir` in parallel,
* creating `outDir` if it doesn't yet exist. Returns the resolved
* on-disk paths so the caller can hand them straight to the prompt
* builder.
*/
export async function writeReviewArtifacts(
outDir: string,
contents: ReviewArtifactContents,
names: ReviewArtifactNames,
): Promise<ReviewArtifactPaths> {
await fs.mkdir(outDir, { recursive: true });

const diffPath = path.join(outDir, names.diff);
const commentsPath = path.join(outDir, names.comments);
const descriptionPath = path.join(outDir, names.description);

await Promise.all([
fs.writeFile(diffPath, contents.diff),
fs.writeFile(commentsPath, JSON.stringify(contents.comments, null, 2)),
fs.writeFile(descriptionPath, contents.description),
]);

return { diffPath, commentsPath, descriptionPath };
}
146 changes: 146 additions & 0 deletions src/core/review/prompts/candidates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
/**
* Platform-agnostic Pass 1 (candidate generation) prompt.
*
* Both `src/create-prompt/templates/review-candidates-prompt.ts` (GitHub)
* and `src/gitlab/prompts/candidates.ts` (GitLab) delegate to this builder
* via thin adapters that supply a `ReviewTerminology` object plus the
* runtime context (entity number, SHAs, artifact paths). The /review skill
* itself is platform-agnostic; this builder produces the runtime harness
* (where to read inputs, where to write the candidates JSON, what NOT to
* call) around it.
*
* STRICT contract: this prompt MUST NOT instruct the model to call any
* posting tool. Posting is Pass 2's responsibility, and tool gating is
* additionally enforced at the `droid exec` level via `--enabled-tools`.
*/

import type { ReviewPromptContext } from "./types";

export function generateCandidatesPrompt(ctx: ReviewPromptContext): string {
const {
terminology: t,
entityNumber,
repoOrProject,
headRef,
headSha,
baseRef,
diffPath,
commentsPath,
descriptionPath,
candidatesPath,
includeSuggestions,
securityReviewEnabled,
} = ctx;

const bodyFieldDescription = includeSuggestions
? " - `body`: Comment text starting with priority tag [P0|P1|P2], then title, then 1 paragraph explanation.\n" +
" Follow the suggestion block rules from the review skill when including suggestions."
: " - `body`: Comment text starting with priority tag [P0|P1|P2], then title, then 1 paragraph explanation";

const sideFieldDescription = includeSuggestions
? ' - `side`: "RIGHT" for new/modified code (default). Use "LEFT" only for removed code **without** suggestions.\n' +
" If you include a suggestion block, choose a RIGHT-side anchor and keep it unchanged so the validator can reuse it."
: ' - `side`: "RIGHT" for new/modified code (default), "LEFT" only for removed code';

const skillInstruction = includeSuggestions
? "Invoke the 'review' skill to load the review methodology, then execute its **Pass 1: Candidate Generation** procedure — including suggestion block rules."
: "Invoke the 'review' skill to load the review methodology, then execute its **Pass 1: Candidate Generation** procedure. Do NOT include code suggestion blocks.";

const securitySubagentInstruction = securityReviewEnabled
? `

## Security Review (run concurrently)

In addition to the code review, you MUST also spawn a \`security-reviewer\` subagent via the Task tool.
This subagent runs **concurrently** with the code review subagents during Step 2.

Spawn it with:
- \`subagent_type\`: "security-reviewer"
- \`description\`: "Security review"
- \`prompt\`: Include the full ${t.entityNoun} context (${t.repoLabel.toLowerCase()}, ${t.entityNoun} ${t.metaEntityNumberKey === "prNumber" ? "number" : "IID"}, head SHA, ${t.baseRefShortLabel}) and the paths to precomputed data files (diff, description, existing comments). The security-reviewer will invoke the security-review skill and return a JSON array of security findings.

**IMPORTANT**: Spawn the security-reviewer in the SAME response as the code review subagents so they all run in parallel.

After all subagents complete (both code review and security-reviewer), merge the security findings into the \`comments\` array alongside code review findings. Security findings use the same schema but are prefixed with \`[security]\` in their body (e.g., \`[P1] [security] Title\`).
`
: "";

return `You are a senior staff software engineer and expert code reviewer.

Your task: Review ${t.entityNoun} ${t.entityNumberSigil}${entityNumber} in ${repoOrProject} and generate a JSON file with **high-confidence, actionable** review comments that pinpoint genuine issues.

${skillInstruction}${securitySubagentInstruction}

<context>
${t.repoLabel}: ${repoOrProject}
${t.entityNumberLabel}: ${entityNumber}
${t.headRefLabel}: ${headRef}
${t.headShaLabel}: ${headSha}
${t.baseRefLabel}: ${baseRef}

Precomputed data files:
- ${t.descriptionLabel}: \`${descriptionPath}\`
- ${t.diffLabel}: \`${diffPath}\`
- Existing Comments: \`${commentsPath}\`
</context>

<output_spec>
Write output to \`${candidatesPath}\` using this exact schema:

\`\`\`json
{
"version": 1,
"meta": {
"${t.metaRepoKey}": "${t.repoExample}",
"${t.metaEntityNumberKey}": 123,
"headSha": "<head sha>",
"${t.metaBaseRefKey}": "main",
"generatedAt": "<ISO timestamp>"
},
"comments": [
{
"path": "src/index.ts",
"body": "[P1] Title\\n\\n1 paragraph.",
"line": 42,
"startLine": null,
"side": "RIGHT",
"commit_id": "<head sha>"
}
],
"reviewSummary": {
"body": "1-3 sentence overall assessment"
}
}
\`\`\`

<schema_details>
- **version**: Always \`1\`

- **meta**: Metadata object
- \`${t.metaRepoKey}\`: "${repoOrProject}"
- \`${t.metaEntityNumberKey}\`: ${entityNumber}
- \`headSha\`: "${headSha}"
- \`${t.metaBaseRefKey}\`: "${baseRef}"
- \`generatedAt\`: ISO 8601 timestamp (e.g., "2024-01-15T10:30:00Z")

- **comments**: Array of comment objects
- \`path\`: ${t.pathFieldDescription}
${bodyFieldDescription}
- \`line\`: ${t.lineFieldDescription}
- \`startLine\`: \`null\` for single-line comments, or start line number for multi-line comments
${sideFieldDescription}
- \`commit_id\`: "${headSha}"

- **reviewSummary**:
- \`body\`: 1-3 sentence overall assessment
</schema_details>
</output_spec>

<critical_constraints>
**DO NOT** post to ${t.platformName}.
**DO NOT** invoke any ${t.entityNoun} mutation tools ${t.mutationToolForbiddance}.
**DO NOT** modify any files other than writing to \`${candidatesPath}\`.
Output ONLY the JSON file—no additional commentary.
</critical_constraints>
`;
}
93 changes: 93 additions & 0 deletions src/core/review/prompts/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* Shared types for the platform-agnostic review prompts.
*
* The candidate-generation (Pass 1) and validator (Pass 2) prompts are
* structurally identical between GitHub and GitLab. The only things that
* vary are labels, identifier names, MCP tool names, and a small number
* of platform-specific instruction lines. Those variations are captured
* here so the shared templates can stay platform-agnostic.
*/

export interface ReviewTerminology {
/** "PR" or "MR" */
entityNoun: string;
/** "#" for GitHub PRs, "!" for GitLab MRs */
entityNumberSigil: string;
/** "GitHub" or "GitLab" */
platformName: string;
/** Label for the repo/project value (e.g. "Repo" or "Project") */
repoLabel: string;
/** Label for the entity number row in <context> (e.g. "PR Number" or "MR IID") */
entityNumberLabel: string;
/** Label for the source branch row (e.g. "PR Head Ref" or "MR Source Branch") */
headRefLabel: string;
/** Label for the head SHA row (e.g. "PR Head SHA" or "MR Head SHA") */
headShaLabel: string;
/** Label for the target branch row (e.g. "PR Base Ref" or "MR Target Branch") */
baseRefLabel: string;
/** Short form of the target-branch concept, used in narrative prose (e.g. "base ref" or "target branch") */
baseRefShortLabel: string;
/** Label for the description artifact (e.g. "PR Description" or "MR Description") */
descriptionLabel: string;
/** Label for the diff artifact (e.g. "Full PR Diff" or "Full MR Diff") */
diffLabel: string;
/** JSON meta key for the repo identifier (e.g. "repo" or "project") */
metaRepoKey: string;
/** JSON meta key for the entity number (e.g. "prNumber" or "mrIid") */
metaEntityNumberKey: string;
/** JSON meta key for the base ref (e.g. "baseRef" or "targetBranch") */
metaBaseRefKey: string;
/** Example identifier in schema doc (e.g. "owner/repo" or "group/project") */
repoExample: string;
/** Free-form description for the `path` field (small wording variation) */
pathFieldDescription: string;
/** Free-form description for the `line` field (small wording variation) */
lineFieldDescription: string;
/** Free-form blurb listing mutation tools the candidates pass MUST NOT use */
mutationToolForbiddance: string;
/** MCP tool name for posting a batched review (Pass 2) */
submitReviewToolName: string;
/** Extra args appended to the submit_review instruction (e.g. " along with `mr_iid: 5`") */
submitReviewExtraArg: string;
/** Trailing clause on the "Do NOT include a body parameter" line */
submitReviewBodyExclusionTrailer: string;
/** MCP tool name for updating the sticky tracking comment/note */
updateTrackingToolName: string;
/** Human-readable name for the sticky comment (e.g. "tracking comment" or "sticky tracking note") */
trackingCommentName: string;
/** "comment" (GH) or "top-level note" (GL), used in "post the summary as a separate X" */
summaryEntityName: string;
/** Trailing clause on the summary-forbiddance line (e.g. " or as the body of `submit_review`" on GH; "" on GL) */
summaryPostingExtraExclusion: string;
/** Approval/changes line at the end of validator instructions */
approvalChangesNote: string;
/** Optional security-badge instruction line — GL only */
securityBadgeInstruction?: string;
}

export interface ReviewPromptContext {
terminology: ReviewTerminology;
/** PR number / MR IID */
entityNumber: number | string;
/** Full repo/project identifier */
repoOrProject: string;
/** Source branch name */
headRef: string;
/** Head SHA */
headSha: string;
/** Target branch name */
baseRef: string;
/** On-disk path to the diff artifact */
diffPath: string;
/** On-disk path to existing-comments JSON */
commentsPath: string;
/** On-disk path to description text */
descriptionPath: string;
/** On-disk path where Pass 1 writes the candidates JSON */
candidatesPath: string;
/** On-disk path where Pass 2 writes the validated JSON (validator only) */
validatedPath?: string;
includeSuggestions: boolean;
/** Spawn security-reviewer subagent during Pass 1 (candidates only) */
securityReviewEnabled: boolean;
}
Loading