Skip to content
Open
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
70 changes: 61 additions & 9 deletions src/agent/agent-system-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,24 +145,76 @@ export function getInteractiveSystemPrompt(): string {
{ "path": "string", "depth": "integer ≥ 1" }
</parameters>
</tool>

<tool name="find_patterns">
<description>Find similar code patterns or implementations in the codebase for a given domain or file type.</description>
<parameters>
{
"domain": "string - domain or feature area (e.g., 'components', 'auth', 'api')",
"filePattern": "string - file pattern to match (e.g., '*.tsx', 'Button*'), or empty string for no pattern",
"maxResults": "integer - maximum examples to return (1-10, default 5)"
}
</parameters>
</tool>

<tool name="find_utilities">
<description>Find available utility functions, hooks, or services that might be relevant to the current changes.</description>
<parameters>
{
"category": "string - category to search for (e.g., 'validation', 'formatting', 'api', 'hooks')",
"keyword": "string - optional keyword to filter results, or empty string for no keyword filter"
}
</parameters>
</tool>

<tool name="analyze_architecture">
<description>Analyze the architectural context and relationships for changed files.</description>
<parameters>
{
"filePaths": "array of strings - file paths to analyze",
"depth": "integer - depth of analysis (1-3, default 2)"
}
</parameters>
</tool>
</tools>

<!-- INTERACTIVE PROTOCOL -->
<!-- INTERACTIVE PROTOCOL: Human-like Contextual Review -->
<protocol>
<step1>Analyse the diff using the review guidelines below.</step1>
<step1>
**Understand the Intent**: Review the provided context about PR description, feature intent, and business goals.
What is this change trying to accomplish? Why was this approach chosen?
</step1>
<step2>
If extra context is needed (e.g., to verify imports, understand full function context, check test coverage), call exactly **one** tool and STOP.
• Do not output any other text.
• Example call (JSON is generated automatically by the model):
{ "name": "fetch_file", "arguments": { "path": "src/api/user.py" } }
**Gather Strategic Context**: Before diving into line-by-line review, use tools to understand:
• How similar features are implemented in this codebase (find_patterns)
• What utilities/patterns are available that might be relevant (find_utilities)
• How the changed files fit into the broader architecture (analyze_architecture)
• Full context of changed files if the diff seems incomplete (fetch_file)
</step2>
<step3>
After the tool result arrives (as a <code>tool</code> message), repeat
steps 1-2 until no more context is required.
**Review for Architectural Fit**: Focus on high-impact issues:
• Does this follow established patterns in the codebase?
• Are they using the right abstractions and utilities?
• Does this integrate well with the existing architecture?
• Are there better alternatives available in the codebase?
</step3>
<step4>
When confident, emit review comments using the XML schema in the response format section.
**Quality and Consistency**: Review for:
• Code quality and maintainability
• Consistency with team conventions
• Potential edge cases or bugs
• Test coverage if applicable
</step4>
<step5>
**Prioritize and Consolidate**: Focus on the most important issues that would help the developer improve.
Avoid duplicate comments on the same pattern across different locations.
Group similar issues into comprehensive feedback.
</step5>

**Tool Usage Guidelines**:
Copy link
Contributor

Choose a reason for hiding this comment

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

🔵 NIT: The example JSON in the Tool Usage Guidelines still escapes quotes (e.g., \"domain\"). Consider using raw JSON formatting (without backslashes) for readability, since this block is illustrative rather than code that’s parsed literally.

• Call exactly **one** tool per response and STOP (no other text)
• Use tools strategically to understand patterns and context, not just verify syntax
• Example: { "name": "find_patterns", "arguments": { "domain": "components", "filePattern": "*Button*" } }
</protocol>

<!-- REVIEW GUIDELINES -->
Expand Down
37 changes: 36 additions & 1 deletion src/agent/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,29 @@ export async function reviewChunkWithAgent(
existingCommentsContext = contextLines.join("\n");
}

// Extract feature intent from the diff summary for contextual review
let featureContext = "";
if (diffSummary) {
featureContext = `
<featureContext>
<intent>${escapeXml(diffSummary.prType || "Code changes")}</intent>
<summary>${escapeXml(diffSummary.summaryPoints.join(". ") || "No summary available")}</summary>
<prDescription>${escapeXml(diffSummary.prDescription || "No description provided")}</prDescription>
${diffSummary.keyRisks.length > 0 ? `
<risks>
${diffSummary.keyRisks.map(risk => `<risk tag="${escapeXml(risk.tag)}">${escapeXml(risk.description)}</risk>`).join('\n ')}
</risks>` : ''}
</featureContext>`;
}

const initialMessage = `
<reviewRequest>
<repositoryFiles>
${escapeXml(fileList)}
</repositoryFiles>

${featureContext}

<diffAnalysisContext>
${escapeXml(summaryContext)}
</diffAnalysisContext>
Expand All @@ -139,7 +157,24 @@ export async function reviewChunkWithAgent(
</diffChunk>

<instruction>
Please review this diff chunk using the provided context. ${existingComments.length > 0 ? "Pay special attention to the existing comments:\n 1. Avoid creating duplicate or similar comments unless you have significantly different insights.\n 2. Analyze whether any existing comments have been addressed by the changes in this diff.\n 3. If you find that an existing comment has been resolved by the code changes, include it in the <resolvedComments> section with a clear explanation of how it was addressed." : ""}
Review this diff chunk like a human team member would:

1. **Understand the Intent**: What is this change trying to accomplish? Use the feature context above.

2. **Gather Context**: Use the available tools to understand:
- How similar features are implemented in this codebase
- What utilities or patterns are available that might be relevant
- How these files fit into the broader architecture

3. **Focus on Architecture**: Does this follow established patterns? Are they using the right abstractions?

4. **Quality Review**: Look for maintainability, edge cases, and consistency with team conventions.

5. **Prioritize Impact**: Focus on issues that would genuinely help the developer improve the codebase.

${existingComments.length > 0 ? "Special attention to existing comments:\n - Avoid creating duplicate or similar comments unless you have significantly different insights\n - Analyze whether any existing comments have been addressed by the changes\n - If existing comments are resolved by the code changes, include them in the <resolvedComments> section" : ""}
Copy link
Contributor

Choose a reason for hiding this comment

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

🔴 REQUIRED: The literal <resolvedComments> in this instruction string will be injected unescaped into the XML payload, breaking the reviewRequest format. Escape the tag (e.g. &lt;resolvedComments&gt;) or otherwise avoid embedding raw XML tags in instruction text.

Suggested change
${existingComments.length > 0 ? "Special attention to existing comments:\n - Avoid creating duplicate or similar comments unless you have significantly different insights\n - Analyze whether any existing comments have been addressed by the changes\n - If existing comments are resolved by the code changes, include them in the <resolvedComments> section" : ""}
Replace the `<resolvedComments>` snippet with an escaped version, for example:

${existingComments.length > 0 ? "Special attention to existing comments:\n - Avoid creating duplicate or similar comments unless you have significantly different insights\n - Analyze whether any existing comments have been addressed by the changes\n - If existing comments are resolved by the code changes, include them in the <resolvedComments> section" : ""}


Take your time to gather the right context before providing feedback. Use the tools strategically to understand patterns and best practices in this codebase.
</instruction>
</reviewRequest>`;

Expand Down
216 changes: 215 additions & 1 deletion src/agent/tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -282,4 +282,218 @@ export const depGraphTool = tool({
},
});

export const allTools = [fetchFileTool, fetchSnippetTool, depGraphTool];
/**
* Tool to find similar patterns in the codebase
*/
export const findPatternsTool = tool({
name: "find_patterns",
description: "Find similar code patterns or implementations in the codebase for a given domain or file type",
parameters: z.object({
domain: z.string().describe("Domain or feature area (e.g., 'components', 'auth', 'api')"),
filePattern: z.string().nullable().default("").describe("File pattern to match (e.g., '*.tsx', 'Button*'), or empty string for no pattern"),
maxResults: z.number().int().min(1).max(10).default(5).describe("Maximum number of examples to return"),
}),
execute: async ({ domain, filePattern, maxResults }) => {
const sourceFiles = getAllSourceFiles();

let matchingFiles = sourceFiles.filter(file => {
if (filePattern && filePattern.trim() !== "") {
const pattern = filePattern.replace(/\*/g, '.*');
const regex = new RegExp(pattern, 'i');
return regex.test(file);
}
return file.toLowerCase().includes(domain.toLowerCase());
});

matchingFiles = matchingFiles.slice(0, maxResults);

const results: string[] = [];
for (const file of matchingFiles) {
try {
const absolutePath = resolve(process.cwd(), file);
if (existsSync(absolutePath)) {
const content = readFileSync(absolutePath, 'utf-8');
// Get first 50 lines for pattern analysis
const preview = content.split('\n').slice(0, 50).join('\n');
results.push(`\n=== ${file} ===\n${preview}\n`);
}
} catch {
results.push(`\n=== ${file} ===\nError reading file\n`);
}
}

if (results.length === 0) {
return `No patterns found for domain "${domain}"${filePattern && filePattern.trim() !== "" ? ` with pattern "${filePattern}"` : ''}`;
}

return `Found ${results.length} pattern examples:\n${results.join('\n')}`;
},
});

/**
* Tool to find available utilities in the codebase
*/
export const findUtilitiesTool = tool({
name: "find_utilities",
description: "Find available utility functions, hooks, or services that might be relevant to the current changes",
parameters: z.object({
category: z.string().describe("Category to search for (e.g., 'validation', 'formatting', 'api', 'hooks')"),
keyword: z.string().nullable().default("").describe("Optional keyword to filter results, or empty string for no keyword filter"),
}),
execute: async ({ category, keyword }) => {
const sourceFiles = getAllSourceFiles();

// Look for utility files
const utilityFiles = sourceFiles.filter(file => {
const lowerFile = file.toLowerCase();
return (
lowerFile.includes('util') ||
lowerFile.includes('helper') ||
lowerFile.includes('hook') ||
lowerFile.includes('service') ||
lowerFile.includes(category.toLowerCase()) ||
(keyword && keyword.trim() !== "" && lowerFile.includes(keyword.toLowerCase()))
);
}).slice(0, 8);

const utilities: string[] = [];

for (const file of utilityFiles) {
try {
const absolutePath = resolve(process.cwd(), file);
if (existsSync(absolutePath)) {
const content = readFileSync(absolutePath, 'utf-8');

// Extract exported functions/constants
const exportMatches = [
...content.matchAll(/export\s+(function|const|class)\s+(\w+)/g),
...content.matchAll(/export\s*{\s*([^}]+)\s*}/g),
];

if (exportMatches.length > 0) {
const exports = exportMatches.map(match => {
if (match[1] === 'function' || match[1] === 'const' || match[1] === 'class') {
return match[2];
} else {
// Parse export list
return match[1].split(',').map(exp => exp.trim()).join(', ');
}
}).join(', ');

// Get JSDoc comment if available
const commentMatch = content.match(/\/\*\*([\s\S]*?)\*\//);
const description = commentMatch
? commentMatch[1].replace(/\s*\*\s?/g, ' ').trim().slice(0, 100)
: '';

utilities.push(`\n=== ${file} ===\nExports: ${exports}\n${description ? `Description: ${description}` : ''}`);
}
}
} catch {
// Skip files we can't read
}
}

if (utilities.length === 0) {
return `No utilities found for category "${category}"${keyword && keyword.trim() !== "" ? ` with keyword "${keyword}"` : ''}`;
}

return `Found ${utilities.length} relevant utilities:\n${utilities.join('\n')}`;
},
});

/**
* Tool to analyze architectural context
*/
export const analyzeArchitectureTool = tool({
name: "analyze_architecture",
description: "Analyze the architectural context and relationships for changed files",
parameters: z.object({
filePaths: z.array(z.string()).describe("Array of file paths to analyze"),
depth: z.number().int().min(1).max(3).default(2).describe("Depth of analysis"),
}),
execute: async ({ filePaths }) => {
const results: string[] = [];

for (const filePath of filePaths.slice(0, 5)) { // Limit to avoid overwhelming
if (!existsSync(resolve(process.cwd(), filePath))) {
results.push(`\n=== ${filePath} ===\nFile not found`);
continue;
}

// Get dependency information for the file
const { imports } = extractDependencies(filePath);
const importedBy = findImporters(filePath, getAllSourceFiles());

let depGraph = `Dependencies: ${imports.length > 0 ? imports.join(', ') : 'None'}`;
depGraph += `\nImported by: ${importedBy.length > 0 ? importedBy.join(', ') : 'None'}`;

// Analyze the file type and purpose
const fileType = inferFileType(filePath);
const purpose = await inferFilePurpose(filePath);

results.push(`\n=== ${filePath} ===`);
results.push(`Type: ${fileType}`);
results.push(`Purpose: ${purpose}`);
results.push(`Dependencies:`);
results.push(depGraph);
}

return results.join('\n');
},
});

// Helper functions for the new tools

function inferFileType(filePath: string): string {
const ext = extname(filePath);
const dir = dirname(filePath);
const name = filePath.toLowerCase();

if (name.includes('component') || name.includes('page') || ext === '.tsx') {
return 'React Component';
}
if (name.includes('hook')) return 'Custom Hook';
if (name.includes('util') || name.includes('helper')) return 'Utility';
if (name.includes('service') || name.includes('api')) return 'Service';
if (name.includes('type') || name.includes('interface')) return 'Type Definition';
if (name.includes('test') || name.includes('spec')) return 'Test';
if (dir.includes('store') || name.includes('reducer')) return 'State Management';

return 'Module';
}

async function inferFilePurpose(filePath: string): Promise<string> {
try {
const content = readFileSync(resolve(process.cwd(), filePath), 'utf-8');

// Look for JSDoc comments at the top
const docMatch = content.match(/\/\*\*([\s\S]*?)\*\//);
if (docMatch) {
const doc = docMatch[1].replace(/\s*\*\s?/g, ' ').trim();
if (doc.length > 10) {
return doc.slice(0, 100) + (doc.length > 100 ? '...' : '');
}
}

// Look for single-line comments
const commentMatch = content.match(/^\/\/\s*(.+)/m);
if (commentMatch) {
return commentMatch[1].trim().slice(0, 100);
}

// Infer from exports
if (content.includes('export default')) {
const defaultExport = content.match(/export default\s+(\w+)/);
if (defaultExport) {
return `Exports ${defaultExport[1]}`;
}
}

return 'Purpose not documented';
} catch {
return 'Could not analyze';
}
}

export const allTools = [fetchFileTool, fetchSnippetTool, depGraphTool, findPatternsTool, findUtilitiesTool, analyzeArchitectureTool];
Loading