Skip to content
Draft
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
5 changes: 5 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,10 @@ inputs:
description: "Override the model used for PR description fill (e.g., 'claude-sonnet-4-5-20250929', 'gpt-5.1-codex'). Only applies to fill flows."
required: false
default: ""
fix_model:
description: "Override the model used for code fix (e.g., 'claude-sonnet-4-5-20250929', 'gpt-5.1-codex'). Only applies to @droid fix flows."
required: false
default: ""
experimental_allowed_domains:
description: "Restrict network access to these domains only (newline-separated). If not set, no restrictions are applied. Provider domains are auto-detected."
required: false
Expand Down Expand Up @@ -188,6 +192,7 @@ runs:
REVIEW_CANDIDATES_PATH: ${{ inputs.review_candidates_path }}
REVIEW_VALIDATED_PATH: ${{ inputs.review_validated_path }}
FILL_MODEL: ${{ inputs.fill_model }}
FIX_MODEL: ${{ inputs.fix_model }}
ADDITIONAL_PERMISSIONS: ${{ inputs.additional_permissions }}
DROID_ARGS: ${{ inputs.droid_args }}
ALL_INPUTS: ${{ toJson(inputs) }}
Expand Down
1 change: 1 addition & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 13 additions & 1 deletion src/create-prompt/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,15 @@ import type {
PreparedContext,
EventData,
ReviewArtifacts,
FixContext,
} from "./types";

export type { CommonFields, PreparedContext, ReviewArtifacts } from "./types";
export type {
CommonFields,
PreparedContext,
ReviewArtifacts,
FixContext,
} from "./types";

const BASE_ALLOWED_TOOLS = [
"Execute",
Expand Down Expand Up @@ -303,6 +309,7 @@ export type PromptCreationOptions = {
disallowedTools?: string[];
includeActionsTools?: boolean;
reviewArtifacts?: ReviewArtifacts;
fixContext?: FixContext;
outputFilePath?: string;
};

Expand All @@ -317,6 +324,7 @@ export async function createPrompt({
disallowedTools = [],
includeActionsTools = false,
reviewArtifacts,
fixContext,
outputFilePath,
}: PromptCreationOptions) {
try {
Expand All @@ -330,6 +338,10 @@ export async function createPrompt({
reviewArtifacts,
);

if (fixContext) {
preparedContext.fixContext = fixContext;
}

if (outputFilePath) {
preparedContext.outputFilePath = outputFilePath;
}
Expand Down
145 changes: 145 additions & 0 deletions src/create-prompt/templates/fix-prompt.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import type { PreparedContext, FixContext } from "../types";

export function generateFixPrompt(context: PreparedContext): string {
const prNumber = context.eventData.isPR
? context.eventData.prNumber
: "unknown";

const repoFullName = context.repository;
const commentBody =
"commentBody" in context.eventData ? context.eventData.commentBody : "";

const userInstructions = extractUserInstructions(commentBody);

if (context.eventData.eventName === "pull_request_review_comment") {
return generateThreadFixPrompt({
prNumber,
repoFullName,
commentBody,
userInstructions,
reviewCommentContext: context.fixContext,
});
}

return generateTopLevelFixPrompt({
prNumber,
repoFullName,
commentBody,
userInstructions,
});
}

function extractUserInstructions(commentBody: string): string {
const cleaned = commentBody.replace(/@droid\s+fix/i, "").trim();
return cleaned || "";
}

type ThreadFixPromptOptions = {
prNumber: string;
repoFullName: string;
commentBody: string;
userInstructions: string;
reviewCommentContext?: FixContext;
};

function generateThreadFixPrompt({
prNumber,
repoFullName,
userInstructions,
reviewCommentContext,
}: ThreadFixPromptOptions): string {
const filePath = reviewCommentContext?.filePath ?? "unknown";
const line = reviewCommentContext?.line;
const parentBody = reviewCommentContext?.parentCommentBody ?? "";

const lineContext = line ? ` around line ${line}` : "";

return `You are fixing a specific code review issue on PR #${prNumber} in ${repoFullName}.
The gh CLI is installed and authenticated via GH_TOKEN.

## Review Issue to Fix

The following review comment identified an issue in \`${filePath}\`${lineContext}:

\`\`\`
${parentBody}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Untrusted comment content is embedded verbatim into the fix prompt

parentCommentBody and user-supplied instructions are interpolated directly into the LLM prompt (including inside a fenced block), so a malicious review comment can break the fence (```), inject higher-priority instructions, or otherwise steer the agent into unsafe actions in a code-pushing flow; consider escaping fence sequences, clearly labeling these sections as untrusted data the model must not follow as instructions, and avoid embedding unescaped values in example shell commands.

\`\`\`
${userInstructions ? `\n## Additional Instructions from User\n\n${userInstructions}\n` : ""}
## Procedure

1. **Understand the issue**: Read the review comment above carefully. Read the file at the specified path to understand the surrounding code context.
2. **Check the PR diff** for additional context:
- Run: \`gh pr diff ${prNumber} --repo ${repoFullName}\`
3. **Implement the fix**: Edit the file(s) to resolve the issue identified in the review comment.
4. **Verify the fix**:
- Read the modified file(s) to confirm correctness.
- If the project has tests, try running them: look for test scripts in package.json, Makefile, or similar config files.
5. **Commit and push**:
- Run: \`git add -A\`
- Run: \`git commit -m "fix: address review comment in ${filePath}"\`
- Run: \`git push\`

## Rules

- Only fix the specific issue mentioned in the review comment. Do not make unrelated changes.
- Keep changes minimal and focused.
- Follow the existing code style and conventions in the repository.
- If you cannot determine the correct fix with confidence, explain what you found and suggest a fix in a comment instead of making a wrong change.
- Never introduce new lint errors or break existing tests.
- Update the tracking comment with progress using the github_comment___update_droid_comment tool.
`;
}

type TopLevelFixPromptOptions = {
prNumber: string;
repoFullName: string;
commentBody: string;
userInstructions: string;
};

function generateTopLevelFixPrompt({
prNumber,
repoFullName,
userInstructions,
}: TopLevelFixPromptOptions): string {
return `You are fixing code issues on PR #${prNumber} in ${repoFullName}.
The gh CLI is installed and authenticated via GH_TOKEN.

## Procedure

1. **Gather context**:
- Run: \`gh pr view ${prNumber} --repo ${repoFullName} --json title,body\`
- Run: \`gh pr view ${prNumber} --repo ${repoFullName} --json comments,reviews\`
- Run: \`gh pr diff ${prNumber} --repo ${repoFullName}\`
- Run: \`gh api repos/${repoFullName}/pulls/${prNumber}/reviews --paginate --jq '.[] | {user: .user.login, state: .state, body: .body}'\`
- Run: \`gh api repos/${repoFullName}/pulls/${prNumber}/comments --paginate --jq '.[] | {path: .path, line: .line, body: .body, user: .user.login}'\`

2. **Identify issues to fix**:
- Review all review comments and feedback on the PR.
- ${userInstructions ? `The user specifically asked: "${userInstructions}". Prioritize these instructions.` : "Identify all actionable review findings that require code changes."}
- Categorize issues by file and severity.

3. **Implement fixes**:
- Address each identified issue systematically, file by file.
- Read each file before editing to understand the full context.
- Make minimal, focused changes that directly address the review feedback.

4. **Verify fixes**:
- Read modified files to confirm correctness.
- If the project has tests, try running them: look for test scripts in package.json, Makefile, or similar config files.
- Check for lint/format scripts and run them if available.

5. **Commit and push**:
- Run: \`git add -A\`
- Run: \`git commit -m "fix: address review feedback on PR #${prNumber}"\`
- Run: \`git push\`

## Rules

- Follow the existing code style and conventions in the repository.
- Keep changes focused on addressing review feedback. Do not refactor unrelated code.
- If a review comment is unclear or you cannot determine the correct fix, skip it and note it in the tracking comment.
- Never introduce new lint errors or break existing tests.
- Update the tracking comment with progress using the github_comment___update_droid_comment tool.
`;
}
7 changes: 7 additions & 0 deletions src/create-prompt/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,12 @@ export type ReviewArtifacts = {
descriptionPath: string;
};

export type FixContext = {
parentCommentBody: string;
filePath: string;
line: number | null;
};

export type PreparedContext = CommonFields & {
eventData: EventData;
githubContext?: GitHubContext;
Expand All @@ -117,5 +123,6 @@ export type PreparedContext = CommonFields & {
headRefOid: string;
};
reviewArtifacts?: ReviewArtifacts;
fixContext?: FixContext;
outputFilePath?: string;
};
57 changes: 57 additions & 0 deletions src/github/utils/command-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,30 @@ describe("Command Parser", () => {
expect(result?.raw).toBe("@droid review");
});

it("should detect @droid fix", () => {
const result = parseDroidCommand("@droid fix this bug");
expect(result?.command).toBe("fix");
expect(result?.raw).toBe("@droid fix");
});

it("should be case insensitive for @droid fix", () => {
const result = parseDroidCommand("@DROID FIX the issue");
expect(result?.command).toBe("fix");
expect(result?.raw).toBe("@DROID FIX");
});

it("should detect @droid fix with extra spaces", () => {
const result = parseDroidCommand("@droid fix");
expect(result?.command).toBe("fix");
expect(result?.raw).toBe("@droid fix");
});

it("should detect standalone @droid fix", () => {
const result = parseDroidCommand("@droid fix");
expect(result?.command).toBe("fix");
expect(result?.raw).toBe("@droid fix");
});

it("should parse @droid security review as security", () => {
const result = parseDroidCommand("@droid security review");
expect(result?.command).toBe("security");
Expand Down Expand Up @@ -244,6 +268,39 @@ describe("Command Parser", () => {
expect(result?.timestamp).toBe("2024-01-01T00:00:00Z");
});

it("should extract fix from PR review comment", () => {
const context = createContext("pull_request_review_comment", {
action: "created",
comment: {
body: "@droid fix this issue",
created_at: "2024-01-01T00:00:00Z",
},
pull_request: {
number: 1,
},
} as unknown as PullRequestReviewCommentEvent);
const result = extractCommandFromContext(context);
expect(result?.command).toBe("fix");
expect(result?.location).toBe("comment");
});

it("should extract fix from issue comment on PR", () => {
const context = createContext("issue_comment", {
action: "created",
comment: {
body: "@droid fix",
created_at: "2024-01-01T00:00:00Z",
},
issue: {
number: 1,
pull_request: { url: "" },
},
} as unknown as IssueCommentEvent);
const result = extractCommandFromContext(context);
expect(result?.command).toBe("fix");
expect(result?.location).toBe("comment");
});

it("should extract from PR review body", () => {
const context = createContext("pull_request_review", {
action: "submitted",
Expand Down
11 changes: 11 additions & 0 deletions src/github/utils/command-parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import type { GitHubContext } from "../context";

export type DroidCommand =
| "fill"
| "fix"
| "review"
| "security"
| "security-full"
Expand Down Expand Up @@ -49,6 +50,16 @@ export function parseDroidCommand(text: string): ParsedCommand | null {
};
}

// Check for @droid fix command (case insensitive)
const fixMatch = text.match(/@droid\s+fix/i);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] Tighten @droid fix matching to avoid false positives

The regex /@droid\s+fix/i will also match prefixes like "@droid fixed" or "@droid fixes", unintentionally triggering fix mode from ordinary PR discussion. The security matcher in this same file already uses (?:\s|$|[^-\w]) to enforce a word boundary — apply the same pattern here to prevent false triggers.

Suggested change
const fixMatch = text.match(/@droid\s+fix/i);
const fixMatch = text.match(/@droid\s+fix(?:\s|$|[^-\w])/i);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] @droid fix detection can match fixes/fixed/fixing

The current regex /@droid\s+fix/i will also match @droid fixes or @droid fixed, which can unintentionally route to fix mode and start a code-modifying run; tighten this to require a word boundary/end (e.g. /@droid\s+fix\b/i or /@droid\s+fix(?:\s|$)/i) and add a regression test for @droid fixes.

if (fixMatch) {
return {
command: "fix",
raw: fixMatch[0],
location: "body",
};
}

// Check for @droid security --full command (case insensitive)
const securityFullMatch = text.match(/@droid\s+security\s+--full/i);
if (securityFullMatch) {
Expand Down
7 changes: 6 additions & 1 deletion src/mcp/github-pr-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -624,7 +624,12 @@ export function createGitHubPRServer({
path: z
.string()
.describe("The file path to comment on (e.g., 'src/index.js')"),
body: z.string().min(1).describe("The comment text (supports markdown and GitHub code suggestion blocks)"),
body: z
.string()
.min(1)
.describe(
"The comment text (supports markdown and GitHub code suggestion blocks)",
),
line: z
.number()
.int()
Expand Down
Loading