Feat: Implement local path code review preflight and add system prompt tests

This commit is contained in:
g1nation
2026-05-02 18:37:39 +09:00
parent e941a3be86
commit 7643362080
5 changed files with 221 additions and 1 deletions
+171
View File
@@ -271,6 +271,12 @@ export class AgentExecutor {
contextBlock = `\n\n[Currently open file: ${name}]\n\`\`\`\n${text}\n\`\`\``;
}
}
const localPathContext = prompt && loopDepth === 0
? this.buildLocalProjectPathContext(prompt, rootPath)
: '';
if (localPathContext) {
contextBlock += `\n\n${localPathContext}`;
}
// 2. Setup History
if (prompt !== null) {
@@ -604,6 +610,171 @@ export class AgentExecutor {
return runId !== this.activeRunId;
}
private buildLocalProjectPathContext(prompt: string, rootPath: string): string {
if (!this.shouldPreflightLocalProjectPath(prompt)) {
return '';
}
const candidates = this.extractLocalProjectPaths(prompt);
if (candidates.length === 0) {
return '';
}
const sections: string[] = [
'[LOCAL PROJECT PATH PREFLIGHT]',
'The user provided a local project path for review or analysis. Use this inspected context before asking for uploads.',
'If access failed, explain the concrete failure. If access succeeded, proceed with code review from the scanned files.'
];
for (const candidate of candidates.slice(0, 2)) {
sections.push(this.inspectLocalProjectPath(candidate, rootPath));
}
return sections.join('\n');
}
private shouldPreflightLocalProjectPath(prompt: string): boolean {
return /(검토|리뷰|분석|확인|봐줘|고쳐|개선|디버그|review|analy[sz]e|inspect|debug|fix|improve)/i.test(prompt)
&& /\/Volumes\/Data\/project\/Antigravity\/[^\s`"'<>]+/i.test(prompt);
}
private extractLocalProjectPaths(prompt: string): string[] {
const matches = prompt.match(/\/Volumes\/Data\/project\/Antigravity\/[^\s`"'<>]+/gi) || [];
return Array.from(new Set(matches.map((value) => value.replace(/[),.;\]]+$/g, ''))));
}
private inspectLocalProjectPath(targetPath: string, rootPath: string): string {
try {
const absPath = validatePath(rootPath, targetPath);
if (!fs.existsSync(absPath)) {
return [
`Path: ${targetPath}`,
'Access: failed',
'Reason: path does not exist in the current environment.'
].join('\n');
}
const stat = fs.statSync(absPath);
if (!stat.isDirectory()) {
const content = fs.readFileSync(absPath, 'utf8');
return [
`Path: ${targetPath}`,
'Access: succeeded',
'Type: file',
`Preview:\n${summarizeText(content, 1200)}`
].join('\n');
}
const tree = this.listProjectTree(absPath, absPath, 0, 2, 80);
const priorityFiles = this.findPriorityProjectFiles(absPath).slice(0, 8);
const previews = priorityFiles.map((file) => {
try {
const content = fs.readFileSync(file, 'utf8');
return [
`### ${path.relative(absPath, file)}`,
summarizeText(content, 1800)
].join('\n');
} catch (error: any) {
return `### ${path.relative(absPath, file)}\nRead failed: ${error.message}`;
}
}).join('\n\n');
return [
`Path: ${targetPath}`,
'Access: succeeded',
'Type: directory',
`Scanned tree:\n${tree || '(no visible files found)'}`,
priorityFiles.length > 0
? `Priority file previews:\n${previews}`
: 'Priority file previews: no package, README, docs, src, or config files found in the first scan.'
].join('\n');
} catch (error: any) {
return [
`Path: ${targetPath}`,
'Access: failed',
`Reason: ${error.message}`
].join('\n');
}
}
private listProjectTree(root: string, current: string, depth: number, maxDepth: number, limit: number): string {
if (limit <= 0 || depth > maxDepth) {
return '';
}
let entries: fs.Dirent[] = [];
try {
entries = fs.readdirSync(current, { withFileTypes: true })
.filter((entry) => !entry.name.startsWith('.') && !EXCLUDED_DIRS.has(entry.name))
.sort((a, b) => Number(b.isDirectory()) - Number(a.isDirectory()) || a.name.localeCompare(b.name));
} catch {
return '';
}
const lines: string[] = [];
for (const entry of entries) {
if (lines.length >= limit) break;
const fullPath = path.join(current, entry.name);
const relative = path.relative(root, fullPath);
lines.push(`${' '.repeat(depth)}${relative}${entry.isDirectory() ? '/' : ''}`);
if (entry.isDirectory() && depth < maxDepth) {
const child = this.listProjectTree(root, fullPath, depth + 1, maxDepth, limit - lines.length);
if (child) {
lines.push(child);
}
}
}
return lines.join('\n');
}
private findPriorityProjectFiles(root: string): string[] {
const exactNames = new Set([
'package.json',
'README.md',
'readme.md',
'tsconfig.json',
'vite.config.ts',
'vite.config.js',
'next.config.js',
'next.config.mjs',
'webpack.config.js'
]);
const results: string[] = [];
const visit = (dir: string, depth: number) => {
if (depth > 3 || results.length >= 14) return;
let entries: fs.Dirent[] = [];
try {
entries = fs.readdirSync(dir, { withFileTypes: true })
.filter((entry) => !entry.name.startsWith('.') && !EXCLUDED_DIRS.has(entry.name));
} catch {
return;
}
for (const entry of entries) {
const fullPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
if (/^(src|app|pages|components|docs|lib|server|backend|frontend|config)$/i.test(entry.name)) {
visit(fullPath, depth + 1);
}
continue;
}
const relative = path.relative(root, fullPath);
if (
exactNames.has(entry.name)
|| /(^|[\\/])(src|app|pages|components|docs|lib|server|backend|frontend)[\\/].+\.(ts|tsx|js|jsx|md|json)$/i.test(relative)
|| /\.(config|rc)\.(js|ts|json)$/i.test(entry.name)
) {
results.push(fullPath);
}
}
};
visit(root, 0);
return Array.from(new Set(results));
}
private buildMemoryContext(currentPrompt: string): string {
const config = getConfig();
if (!config.memoryEnabled) return '';