diff --git a/docs/records/ConnectAI/development/2026-05-02_local-path-code-review-preflight.md b/docs/records/ConnectAI/development/2026-05-02_local-path-code-review-preflight.md new file mode 100644 index 0000000..9fe7315 --- /dev/null +++ b/docs/records/ConnectAI/development/2026-05-02_local-path-code-review-preflight.md @@ -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. diff --git a/docs/records/ConnectAI/timeline.md b/docs/records/ConnectAI/timeline.md index ddadf3a..6f45c11 100644 --- a/docs/records/ConnectAI/timeline.md +++ b/docs/records/ConnectAI/timeline.md @@ -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. diff --git a/src/agent.ts b/src/agent.ts index fd00ad1..d3b5072 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -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 ''; diff --git a/src/utils.ts b/src/utils.ts index 06f2988..2216b09 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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 , , 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 or ; 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; diff --git a/tests/systemPrompt.test.ts b/tests/systemPrompt.test.ts new file mode 100644 index 0000000..d8b3076 --- /dev/null +++ b/tests/systemPrompt.test.ts @@ -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'); + }); +});