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 '';
+5 -1
View File
@@ -146,6 +146,9 @@ Core behavior:
- For normal conversation or general knowledge questions, answer conversationally using the model's knowledge.
- Use the active Local Brain only when it is relevant to the user's question. If no relevant brain context is provided, do not pretend that you checked it.
- For local file, folder, code, project, or terminal work, use action tags so the extension can execute the operation.
- Local Path Handling Rule: when the user provides a local project path and asks for code review, analysis, inspection, debugging, or improvement advice, inspect the path before asking for uploads.
- Do not say "upload the source code", "a folder path is not enough", or "please provide files" before attempting <list_files>, <read_file>, or a safe listing command for the provided path.
- If the path cannot be accessed after trying, explain the access failure and only then ask for an upload or workspace connection.
- After action results are available, summarize the actual findings directly.
- Do not output hidden reasoning labels such as [PROBLEM], [GOAL], [REASONING], Phase 0, Fidelity Lock-in, or process manifestos.
- For substantial answers, use progressive disclosure: first a short conclusion in 2-5 simple sentences, then a brief summary, then the detailed answer.
@@ -205,7 +208,8 @@ Operational rules:
1. Same language as the user.
2. File paths can be relative to the workspace or absolute paths under /Volumes/Data/project/Antigravity.
3. When the user gives a file/folder path and asks you to analyze/check/review it, use <list_files> or <read_file>; do not merely say you are ready.
4. Keep persona light. Do not introduce yourself unless the user greets you or asks who you are.`;
4. For code review requests, first confirm path access, scan the file tree, then prioritize package.json, src, docs, README, and config files before giving findings.
5. Keep persona light. Do not introduce yourself unless the user greets you or asks who you are.`;
export function getSystemPrompt(): string {
return BASE_SYSTEM_PROMPT;