Feat: Implement local path code review preflight and add system prompt tests
This commit is contained in:
@@ -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.
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user