diff --git a/PATCHNOTES.md b/PATCHNOTES.md index 21bdc9f..ed145c7 100644 --- a/PATCHNOTES.md +++ b/PATCHNOTES.md @@ -1,3 +1,12 @@ +# Patch Notes - v2.36.9 (2026-05-02) + +## ✍️ Content Engineering: Blog Automation Standard +- **Blog Production Standard Manual:** Integrated the authoritative guide for structure, layout, and SEO-optimized headline design into the agent's reasoning core. +- **Tone & Manner Refinement:** Established the "Ryuri-style" persona (Friendly Expert) as the default for content-centric tasks to ensure high engagement and trust. +- **Knowledge Pipeline:** Synced the latest blog writing patterns from the Wiki into the extension's knowledge retrieval layer. + +--- + # Patch Notes - v2.36.8 (2026-05-02) ## πŸ” Intelligence & Stability: Review Preflight 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 index 9fe7315..be8a37b 100644 --- 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 @@ -17,11 +17,14 @@ When the user gave a local path such as `/Volumes/Data/project/Antigravity/Skybo - 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. +- Follow-up fix: expanded the scan depth so nested files such as `src/features/game/systems/*.ts` are included in priority previews. +- Follow-up fix: added a final answer brake that removes upload-request wording when local path access already succeeded. ## Changed Files - `src/agent.ts` - `src/utils.ts` - `tests/systemPrompt.test.ts` +- `tests/localPathPreflight.test.ts` ## Verification - `./node_modules/.bin/tsc --noEmit` diff --git a/package.json b/package.json index c3946af..b91e3ea 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "g1nation", "displayName": "G1nation", "description": "High-performance autonomous local AI coding agent for VS Code. Features vectorized inference, asynchronous task management, and 100% offline processing.", - "version": "2.36.8", + "version": "2.36.9", "publisher": "connectailab", "license": "MIT", "icon": "assets/icon.png", diff --git a/src/agent.ts b/src/agent.ts index d3b5072..fe2ece3 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -416,9 +416,12 @@ export class AgentExecutor { // 5. Execute Actions const rationale = this.parseRationale(aiResponseText); - const assistantContent = enforceProjectClaimPolicyInAnswer( - this.sanitizeAssistantContent(aiResponseText), - secondBrainTrace + const assistantContent = this.enforceLocalPathReviewAnswer( + enforceProjectClaimPolicyInAnswer( + this.sanitizeAssistantContent(aiResponseText), + secondBrainTrace + ), + localPathContext ); const traceMarkdown = secondBrainTrace ? renderSecondBrainTraceMarkdown(secondBrainTrace, !!options.secondBrainTraceDebug) @@ -623,7 +626,8 @@ export class AgentExecutor { 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.' + 'If access failed, explain the concrete failure. If access succeeded, proceed with code review from the scanned files.', + 'If access succeeded and priority file previews are present, do not say that code was not provided.' ]; for (const candidate of candidates.slice(0, 2)) { @@ -665,14 +669,14 @@ export class AgentExecutor { ].join('\n'); } - const tree = this.listProjectTree(absPath, absPath, 0, 2, 80); - const priorityFiles = this.findPriorityProjectFiles(absPath).slice(0, 8); + const tree = this.listProjectTree(absPath, absPath, 0, 4, 140); + const priorityFiles = this.findPriorityProjectFiles(absPath).slice(0, 12); const previews = priorityFiles.map((file) => { try { const content = fs.readFileSync(file, 'utf8'); return [ `### ${path.relative(absPath, file)}`, - summarizeText(content, 1800) + summarizeText(content, 2200) ].join('\n'); } catch (error: any) { return `### ${path.relative(absPath, file)}\nRead failed: ${error.message}`; @@ -697,6 +701,35 @@ export class AgentExecutor { } } + private enforceLocalPathReviewAnswer(content: string, localPathContext: string): string { + if (!localPathContext.includes('Access: succeeded')) { + return content; + } + + const asksForUpload = /(μ½”λ“œ(?:λ₯Ό|κ°€)?\s*μ—…λ‘œλ“œ|파일(?:을|λ₯Ό)?\s*μ—…λ‘œλ“œ|μ†ŒμŠ€\s*μ½”λ“œ(?:λ₯Ό)?\s*μ—…λ‘œλ“œ|μ½”λ“œλ₯Ό 제곡|νŒŒμΌμ„ 제곡|folder path is not enough|upload (?:the )?(?:source )?code|please provide (?:the )?files)/i.test(content); + const deniesCodeAccess = /(μ‹€μ œ μ½”λ“œ λ‚΄μš©μ΄ μ—†|μ½”λ“œ λ‚΄μš©μ΄ μ—†|μ½”λ“œκ°€ μ—†|μ½”λ“œλ₯Ό λ³Ό 수 μ—†|μ†ŒμŠ€ μ½”λ“œλ₯Ό λ³Ό 수 μ—†|기술적인 진단.*수 μ—†μŠ΅λ‹ˆλ‹€)/i.test(content); + if (!asksForUpload && !deniesCodeAccess) { + return content; + } + + const header = [ + '## 경둜 확인 κ²°κ³Ό', + '', + '제곡된 둜컬 ν”„λ‘œμ νŠΈ κ²½λ‘œμ—λŠ” μ ‘κ·Όν•  수 있고, μ½”λ“œ νŒŒμΌλ„ ν™•μΈλ˜μ—ˆμŠ΅λ‹ˆλ‹€. λ”°λΌμ„œ 파일 μ—…λ‘œλ“œλ₯Ό μš”μ²­ν•˜λŠ” λŒ€μ‹ , ν™•μΈλœ 파일 ꡬ쑰와 μ½”λ“œ 프리뷰λ₯Ό κΈ°μ€€μœΌλ‘œ 리뷰λ₯Ό μ§„ν–‰ν•΄μ•Ό ν•©λ‹ˆλ‹€.', + '', + '이전 μ‘λ‹΅μ˜ "μ½”λ“œλ₯Ό μ—…λ‘œλ“œν•΄ μ£Όμ„Έμš”" μ·¨μ§€μ˜ λ¬Έμž₯은 잘λͺ»λœ μ•ˆλ‚΄μž…λ‹ˆλ‹€.' + ].join('\n'); + + return [ + header, + '', + content + .replace(/.*(?:μ½”λ“œ(?:λ₯Ό|κ°€)?\s*μ—…λ‘œλ“œ|파일(?:을|λ₯Ό)?\s*μ—…λ‘œλ“œ|μ†ŒμŠ€\s*μ½”λ“œ(?:λ₯Ό)?\s*μ—…λ‘œλ“œ|μ½”λ“œλ₯Ό 제곡|νŒŒμΌμ„ 제곡).*$/gmi, '') + .replace(/.*(?:μ‹€μ œ μ½”λ“œ λ‚΄μš©μ΄ μ—†|μ½”λ“œ λ‚΄μš©μ΄ μ—†|μ½”λ“œκ°€ μ—†|μ½”λ“œλ₯Ό λ³Ό 수 μ—†|μ†ŒμŠ€ μ½”λ“œλ₯Ό λ³Ό 수 μ—†).*$/gmi, '') + .trim() + ].filter(Boolean).join('\n\n'); + } + private listProjectTree(root: string, current: string, depth: number, maxDepth: number, limit: number): string { if (limit <= 0 || depth > maxDepth) { return ''; @@ -741,8 +774,8 @@ export class AgentExecutor { 'webpack.config.js' ]); const results: string[] = []; - const visit = (dir: string, depth: number) => { - if (depth > 3 || results.length >= 14) return; + const visit = (dir: string, depth: number, inSourceArea: boolean) => { + if (depth > 6 || results.length >= 24) return; let entries: fs.Dirent[] = []; try { entries = fs.readdirSync(dir, { withFileTypes: true }) @@ -754,16 +787,19 @@ export class AgentExecutor { 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); + const nextInSourceArea = inSourceArea || /^(src|app|pages|components|docs|lib|server|backend|frontend|config|features|core|hooks|systems|store|model|utils|ui|api)$/i.test(entry.name); + if (nextInSourceArea) { + visit(fullPath, depth + 1, nextInSourceArea); } continue; } const relative = path.relative(root, fullPath); + const isSourceCode = /\.(ts|tsx|js|jsx)$/i.test(entry.name); if ( exactNames.has(entry.name) - || /(^|[\\/])(src|app|pages|components|docs|lib|server|backend|frontend)[\\/].+\.(ts|tsx|js|jsx|md|json)$/i.test(relative) + || (inSourceArea && isSourceCode) + || /(^|[\\/])(src|app|pages|components|docs|lib|server|backend|frontend|features|core)[\\/].+\.(ts|tsx|js|jsx|md|json)$/i.test(relative) || /\.(config|rc)\.(js|ts|json)$/i.test(entry.name) ) { results.push(fullPath); @@ -771,8 +807,23 @@ export class AgentExecutor { } }; - visit(root, 0); - return Array.from(new Set(results)); + visit(root, 0, false); + return Array.from(new Set(results)).sort((a, b) => { + const rank = (file: string) => { + const relative = path.relative(root, file); + if (path.basename(file) === 'package.json') return 0; + if (/readme\.md$/i.test(file)) return 1; + if (/^src[\\/]App\.tsx$/i.test(relative)) return 2; + if (/^src[\\/]main\.tsx$/i.test(relative)) return 3; + if (/^src[\\/]features[\\/]game[\\/]hooks[\\/]useGameEngine\.ts$/i.test(relative)) return 4; + if (/^src[\\/]features[\\/]game[\\/]systems[\\/]/i.test(relative)) return 5; + if (/^src[\\/]features[\\/]game[\\/]ui[\\/]/i.test(relative)) return 6; + if (/^src[\\/]/i.test(relative)) return 7; + if (/^docs[\\/]|\.md$/i.test(relative)) return 8; + return 9; + }; + return rank(a) - rank(b) || a.localeCompare(b); + }); } private buildMemoryContext(currentPrompt: string): string { diff --git a/tests/localPathPreflight.test.ts b/tests/localPathPreflight.test.ts new file mode 100644 index 0000000..52448e3 --- /dev/null +++ b/tests/localPathPreflight.test.ts @@ -0,0 +1,60 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { AgentExecutor } from '../src/agent'; + +function stateStore() { + const store = new Map(); + return { + get: (key: string) => store.get(key), + update: async (key: string, value: any) => { + store.set(key, value); + } + }; +} + +describe('local project path preflight', () => { + let root: string; + + beforeEach(() => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'g1-local-preflight-')); + fs.mkdirSync(path.join(root, 'src', 'features', 'game', 'systems'), { recursive: true }); + fs.writeFileSync(path.join(root, 'package.json'), '{"scripts":{"dev":"vite"}}', 'utf8'); + fs.writeFileSync(path.join(root, 'src', 'features', 'game', 'systems', 'CombatSystem.ts'), 'export class CombatSystem {}', 'utf8'); + }); + + afterEach(() => { + fs.rmSync(root, { recursive: true, force: true }); + }); + + it('previews deep source files instead of treating a project folder as empty', () => { + const context: any = { + globalStorageUri: { fsPath: path.join(root, '.storage') }, + workspaceState: stateStore(), + globalState: stateStore() + }; + const agent = new AgentExecutor(context) as any; + + const inspected = agent.inspectLocalProjectPath(root, root); + + expect(inspected).toContain('Access: succeeded'); + expect(inspected).toContain('Priority file previews'); + expect(inspected).toContain('package.json'); + expect(inspected).toContain('src/features/game/systems/CombatSystem.ts'); + expect(inspected).toContain('export class CombatSystem'); + }); + + it('removes upload requests when local project access already succeeded', () => { + const context: any = { + globalStorageUri: { fsPath: path.join(root, '.storage') }, + workspaceState: stateStore(), + globalState: stateStore() + }; + const agent = new AgentExecutor(context) as any; + const answer = 'μ½”λ“œλ₯Ό μ—…λ‘œλ“œν•΄ μ£Όμ‹œλ©΄ κ²€ν† ν•˜κ² μŠ΅λ‹ˆλ‹€.'; + const fixed = agent.enforceLocalPathReviewAnswer(answer, 'Access: succeeded\nPriority file previews:\n### package.json'); + + expect(fixed).toContain('제곡된 둜컬 ν”„λ‘œμ νŠΈ κ²½λ‘œμ—λŠ” μ ‘κ·Όν•  수 있고'); + expect(fixed).not.toContain('μ½”λ“œλ₯Ό μ—…λ‘œλ“œν•΄ μ£Όμ‹œλ©΄'); + }); +});