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
@@ -0,0 +1,32 @@
# Development Log: Local Path Code Review Preflight
## Purpose
Prevent code review requests from falling back to "please upload files" when the user already provided an accessible local project path.
## User Feedback
When the user gave a local path such as `/Volumes/Data/project/Antigravity/Skybound`, the agent did not inspect the folder and repeatedly asked for uploaded files. The expected behavior is to attempt local access first, then request uploads only if access fails.
## Implementation Summary
- Added a Local Path Handling Rule to the base system prompt.
- Explicitly forbade upload requests before attempting local path inspection.
- Added code review ordering guidance: confirm path access, scan tree, inspect `package.json`, `src`, `docs`, README, and config files first.
- Added deterministic local project path preflight in the agent execution flow.
- When a review/analysis prompt contains an allowed Antigravity project path, the agent now:
- validates path access
- scans the directory tree
- previews priority project files
- injects the result into the model context before the answer is generated
- If access fails, the injected context includes the concrete failure reason.
## Changed Files
- `src/agent.ts`
- `src/utils.ts`
- `tests/systemPrompt.test.ts`
## Verification
- `./node_modules/.bin/tsc --noEmit`
- `npm run compile`
- `./node_modules/.bin/jest --runInBand`
## Result
For local project review requests, the agent should now inspect the provided path first and only ask for uploads when the path is inaccessible.
+1
View File
@@ -20,3 +20,4 @@
- Added a deterministic output brake that removes unsupported technical structure claims from final answers when Trace policy is `general-only`.
- Tuned Second Brain retrieval by query intent so UX/business/approval questions prioritize customer journey, requirement fit, and business value notes over generic architecture notes.
- Tuned answer readability: paragraph-first summaries, fewer bullet-heavy sections, clearer numbered-section spacing, and larger markdown headings in the sidebar.
- Added local project path preflight for code review requests so the agent scans accessible Antigravity project folders before asking for uploads.
+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;
+12
View File
@@ -0,0 +1,12 @@
import { getSystemPrompt } from '../src/utils';
describe('base system prompt', () => {
it('requires local project paths to be inspected before upload requests', () => {
const prompt = getSystemPrompt();
expect(prompt).toContain('Local Path Handling Rule');
expect(prompt).toContain('inspect the path before asking for uploads');
expect(prompt).toContain('Do not say "upload the source code"');
expect(prompt).toContain('For code review requests, first confirm path access');
});
});