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`.
|
- 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 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.
|
- 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\`\`\``;
|
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
|
// 2. Setup History
|
||||||
if (prompt !== null) {
|
if (prompt !== null) {
|
||||||
@@ -604,6 +610,171 @@ export class AgentExecutor {
|
|||||||
return runId !== this.activeRunId;
|
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 {
|
private buildMemoryContext(currentPrompt: string): string {
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
if (!config.memoryEnabled) return '';
|
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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
1. Same language as the user.
|
||||||
2. File paths can be relative to the workspace or absolute paths under /Volumes/Data/project/Antigravity.
|
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.
|
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 {
|
export function getSystemPrompt(): string {
|
||||||
return BASE_SYSTEM_PROMPT;
|
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