From fadf8679781570526688fe7219c9a3c03b389613 Mon Sep 17 00:00:00 2001 From: g1nation Date: Sun, 3 May 2026 01:13:33 +0900 Subject: [PATCH] Version 2.46.0 Release: Autonomous Anti-Blocking Generation and Trace Relevance Refinement --- package.json | 2 +- src/agent.ts | 103 ++++++++++++++++++++++++++++--- src/features/secondBrainTrace.ts | 24 ++++--- tests/localPathPreflight.test.ts | 53 +++++++++++++++- tests/secondBrainTrace.test.ts | 15 +++++ 5 files changed, 180 insertions(+), 17 deletions(-) diff --git a/package.json b/package.json index 992a2bc..e3ae17b 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.43.0", + "version": "2.46.0", "publisher": "connectailab", "license": "MIT", "icon": "assets/icon.png", diff --git a/src/agent.ts b/src/agent.ts index f790c64..a11714a 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -433,8 +433,18 @@ export class AgentExecutor { if (prompt && this.isSecondBrainInventoryRequest(prompt) && brainFiles.length > 0 && this.isNoBrainDataRefusal(assistantContent)) { assistantContent = this.buildSecondBrainInventoryFallbackAnswer(activeBrain, brainFiles, secondBrainTrace); } - if (prompt && localPathContext && this.isProjectKnowledgeCreationRequest(prompt) && this.isBlockingProjectKnowledgeAnswer(assistantContent)) { - assistantContent = this.buildProjectKnowledgeFallbackAnswer(localPathContext); + if (prompt && localPathContext && this.isProjectKnowledgeCreationRequest(prompt)) { + const record = this.writeProjectKnowledgeRecord(localPathContext); + if (this.isBlockingProjectKnowledgeAnswer(assistantContent)) { + assistantContent = this.buildProjectKnowledgeFallbackAnswer(localPathContext, record); + } else if (record && !assistantContent.includes(record.filePath)) { + assistantContent = [ + assistantContent, + '', + '## 생성된 기록', + `프로젝트 지식 기록을 생성했습니다: \`${record.filePath}\`` + ].join('\n'); + } } const traceMarkdown = secondBrainTrace ? renderSecondBrainTraceMarkdown(secondBrainTrace, !!options.secondBrainTraceDebug) @@ -786,11 +796,11 @@ export class AgentExecutor { try { const content = fs.readFileSync(file, 'utf8'); return [ - `### ${path.relative(absPath, file)}`, + `File: ${path.relative(absPath, file)}`, summarizeText(content, 2200) ].join('\n'); } catch (error: any) { - return `### ${path.relative(absPath, file)}\nRead failed: ${error.message}`; + return `File: ${path.relative(absPath, file)}\nRead failed: ${error.message}`; } }).join('\n\n'); @@ -846,12 +856,12 @@ export class AgentExecutor { return /(블로킹 질문|어떤 기능 영역|어떤 부분.*먼저|어떤 기능이나 아키텍처|구체적인 방향|방향 설정이 필요|명확히 알려주시면|우선적으로 정리|최종 사용 목적|Question reason|별도의 파일 기록.*생성되지|파일 기록이 생성되지|더 깊이 있는 분석.*지정|해당 기능.*지정하여 요청)/i.test(content); } - private buildProjectKnowledgeFallbackAnswer(localPathContext: string): string { + private buildProjectKnowledgeFallbackAnswer(localPathContext: string, record?: { filePath: string; relativePath: string } | null): string { const pathMatch = localPathContext.match(/Path:\s*(.+)/); const projectPath = pathMatch?.[1]?.trim() || '제공된 로컬 프로젝트 경로'; const treeMatch = localPathContext.match(/Scanned tree:\n([\s\S]*?)(?:\nPriority file previews:|$)/); const treePreview = treeMatch?.[1]?.trim().split('\n').slice(0, 18).join('\n') || ''; - const priorityMatches = [...localPathContext.matchAll(/###\s+(.+)/g)].map((match) => match[1].trim()).slice(0, 10); + const priorityMatches = this.extractPriorityPreviewFiles(localPathContext).slice(0, 10); const priorityText = priorityMatches.length ? priorityMatches.map((file) => `- ${file}`).join('\n') : '- package.json, src, docs, config 계열 파일을 우선 확인'; @@ -891,10 +901,89 @@ export class AgentExecutor { '```', '', '## 다음 액션', - '기본값으로는 위 초안을 프로젝트 지식 1번 문서로 저장하고, 그 다음 `agent.ts` 실행 흐름 지식을 별도 문서로 쪼개는 것이 좋습니다.' + record + ? `프로젝트 지식 1번 문서를 생성했습니다: \`${record.filePath}\`` + : '기본값으로는 위 초안을 프로젝트 지식 1번 문서로 저장하고, 그 다음 `agent.ts` 실행 흐름 지식을 별도 문서로 쪼개는 것이 좋습니다.' ].filter(Boolean).join('\n'); } + private extractPriorityPreviewFiles(localPathContext: string): string[] { + const fileMarkerMatches = [...localPathContext.matchAll(/^File:\s*(.+)$/gmi)] + .map((match) => match[1].trim()); + if (fileMarkerMatches.length > 0) { + return Array.from(new Set(fileMarkerMatches)); + } + + const previewBlock = localPathContext.match(/Priority file previews:\n([\s\S]*)/)?.[1] || ''; + return Array.from(new Set([...previewBlock.matchAll(/^###\s+(.+)$/gmi)] + .map((match) => match[1].trim()) + .filter((value) => /[\\/]/.test(value) || /\.[a-z0-9]+$/i.test(value)))); + } + + private writeProjectKnowledgeRecord(localPathContext: string): { filePath: string; relativePath: string } | null { + const pathMatch = localPathContext.match(/Path:\s*(.+)/); + const projectPath = pathMatch?.[1]?.trim(); + if (!projectPath || !localPathContext.includes('Access: succeeded')) return null; + + try { + const projectName = path.basename(projectPath); + const today = new Date().toISOString().slice(0, 10); + const slug = projectName.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '') || 'project'; + const relativePath = path.join('docs', 'records', projectName, 'development', `${today}_${slug}_project_knowledge_overview.md`); + const filePath = path.join(projectPath, relativePath); + fs.mkdirSync(path.dirname(filePath), { recursive: true }); + fs.writeFileSync(filePath, this.buildProjectKnowledgeMarkdown(localPathContext), 'utf8'); + return { filePath, relativePath }; + } catch (error: any) { + logError('Failed to write project knowledge record.', { error: error?.message || String(error) }); + return null; + } + } + + private buildProjectKnowledgeMarkdown(localPathContext: string): string { + const pathMatch = localPathContext.match(/Path:\s*(.+)/); + const projectPath = pathMatch?.[1]?.trim() || 'Unknown project path'; + const projectName = path.basename(projectPath); + const treeMatch = localPathContext.match(/Scanned tree:\n([\s\S]*?)(?:\nPriority file previews:|$)/); + const treePreview = treeMatch?.[1]?.trim().split('\n').slice(0, 80).join('\n') || ''; + const priorityFiles = this.extractPriorityPreviewFiles(localPathContext); + + return [ + `# ${projectName} Project Knowledge Overview`, + '', + `Date: ${new Date().toISOString()}`, + `Project: ${projectName}`, + `Repository: \`${projectPath}\``, + '', + '## Purpose', + `${projectName}는 VS Code 안에서 로컬 AI 에이전트, Second Brain, 프로젝트 기록, 에이전트 스킬을 연결하는 개발 보조 프로젝트다.`, + '', + '## Confirmed Structure', + '- `src/agent.ts`: 에이전트 실행, 로컬 경로 프리플라이트, Second Brain Trace, 액션 실행 흐름의 중심.', + '- `src/sidebarProvider.ts`: Webview UI, 브레인/모델/프로젝트 선택, 프롬프트 전달, 기록 UI를 담당.', + '- `src/features/secondBrainTrace.ts`: Second Brain 검색 결과와 근거 정책을 구성.', + '- `src/features/projectChronicle/`: 프로젝트 기록을 Markdown으로 관리하는 Chronicle 기능.', + '- `src/core/`: 큐, 이벤트, 트랜잭션, 오류 처리 등 실행 안정성 계층.', + '- `tests/`: Second Brain, 로컬 경로 프리플라이트, Chronicle, 보안/트랜잭션 회귀 테스트.', + '', + '## Evidence Files', + ...(priorityFiles.length ? priorityFiles.map((file) => `- \`${file}\``) : ['- 확인된 우선 파일 없음']), + '', + '## Scanned Tree Excerpt', + '```text', + treePreview || '(no scanned tree captured)', + '```', + '', + '## Current Knowledge Gap', + '- 전체 아키텍처는 파일 구조와 일부 프리뷰 기준으로 파악 가능하지만, 세부 동작 지식은 `src/agent.ts`, `src/sidebarProvider.ts`, `secondBrainTrace.ts`, `projectChronicle` 순서로 심화 분석해 보강해야 한다.', + '', + '## Next Records', + '- `agent.ts` 실행 흐름 상세 분석', + '- Second Brain Trace 검색 및 근거 정책 분석', + '- Project Chronicle 기록 생성 흐름 분석' + ].join('\n'); + } + private listProjectTree(root: string, current: string, depth: number, maxDepth: number, limit: number): string { if (limit <= 0 || depth > maxDepth) { return ''; diff --git a/src/features/secondBrainTrace.ts b/src/features/secondBrainTrace.ts index c396341..594171f 100644 --- a/src/features/secondBrainTrace.ts +++ b/src/features/secondBrainTrace.ts @@ -390,12 +390,14 @@ function scoreFile(file: string, brainRoot: string, terms: string[], intent: Sec content = ''; } const sourceType = classifySourceType(relative, content); - const canSupportProjectClaim = sourceType === 'Project Evidence' || sourceType === 'User Decision'; const lower = content.toLowerCase(); + const documentProject = inferDocumentProject(relative, lower); + const projectMatchesTarget = !targetProject || !documentProject || documentProject === targetProject; + const canSupportProjectClaim = projectMatchesTarget && (sourceType === 'Project Evidence' || sourceType === 'User Decision'); let score = pathPriority(relative, intent); if (targetProject) { - score += projectRelevanceScore(relative, lower, targetProject); + score += projectRelevanceScore(relative, lower, targetProject, documentProject); } for (const term of terms) { if (basename.includes(term)) score += 4; @@ -417,17 +419,25 @@ function scoreFile(file: string, brainRoot: string, terms: string[], intent: Sec }; } -function projectRelevanceScore(relativePath: string, lowerContent: string, targetProject: string): number { +function inferDocumentProject(relativePath: string, lowerContent: string): string | undefined { + const normalized = relativePath.toLowerCase(); + const pathProject = `${normalized}\n${lowerContent}`.match(/\/volumes\/data\/project\/antigravity\/([a-z0-9_-]+)/i) + || `${normalized}\n${lowerContent}`.match(/(?:^|[\\/])(connectai|datacollector|skybound)(?:[\\/]|_|-|\b)/i); + if (pathProject?.[1]) return pathProject[1].toLowerCase(); + + const labeledProject = lowerContent.match(/(?:project|프로젝트)\s*[::]\s*`?([a-z0-9_-]+)/i); + return labeledProject?.[1]?.toLowerCase(); +} + +function projectRelevanceScore(relativePath: string, lowerContent: string, targetProject: string, documentProject?: string): number { const normalized = relativePath.toLowerCase(); let score = 0; if (normalized.includes(targetProject)) score += 12; const targetMatches = lowerContent.split(targetProject).length - 1; if (targetMatches > 0) score += Math.min(targetMatches * 4, 20); - const otherProjectMatch = lowerContent.match(/(?:project|프로젝트|repository|repo)\s*[::]?\s*`?\/?volumes\/data\/project\/antigravity\/([a-z0-9_-]+)/i) - || lowerContent.match(/(?:project|프로젝트)\s*[::]\s*([a-z0-9_-]+)/i); - const otherProject = otherProjectMatch?.[1]?.toLowerCase(); - if (otherProject && otherProject !== targetProject) score -= 24; + const otherProject = documentProject && documentProject !== targetProject ? documentProject : undefined; + if (otherProject) score -= 32; if (normalized.includes('project_logs') && otherProject && otherProject !== targetProject) score -= 8; return score; diff --git a/tests/localPathPreflight.test.ts b/tests/localPathPreflight.test.ts index 9c8a925..3911b9b 100644 --- a/tests/localPathPreflight.test.ts +++ b/tests/localPathPreflight.test.ts @@ -101,9 +101,9 @@ describe('local project path preflight', () => { 'src/', 'src/agent.ts', 'Priority file previews:', - '### package.json', + 'File: package.json', '{"name":"g1nation"}', - '### src/agent.ts', + 'File: src/agent.ts', 'export class AgentExecutor {}' ].join('\n'); @@ -125,4 +125,53 @@ describe('local project path preflight', () => { expect(agent.isBlockingProjectKnowledgeAnswer(answer)).toBe(true); }); + + it('extracts only preview file markers, not markdown headings inside previews', () => { + const context: any = { + globalStorageUri: { fsPath: path.join(root, '.storage') }, + workspaceState: stateStore(), + globalState: stateStore() + }; + const agent = new AgentExecutor(context) as any; + const localPathContext = [ + 'Priority file previews:', + 'File: README.md', + '### 1. 벡터화된 고성능 추론 엔진', + 'File: src/agent.ts', + 'export class AgentExecutor {}' + ].join('\n'); + + expect(agent.extractPriorityPreviewFiles(localPathContext)).toEqual(['README.md', 'src/agent.ts']); + }); + + it('writes a project knowledge record for accessible local project context', () => { + const context: any = { + globalStorageUri: { fsPath: path.join(root, '.storage') }, + workspaceState: stateStore(), + globalState: stateStore() + }; + const agent = new AgentExecutor(context) as any; + const localPathContext = [ + `Path: ${root}`, + 'Access: succeeded', + 'Type: directory', + 'Scanned tree:', + 'package.json', + 'src/', + 'Priority file previews:', + 'File: package.json', + '{"name":"g1nation"}', + 'File: src/features/game/systems/CombatSystem.ts', + 'export class CombatSystem {}' + ].join('\n'); + + const record = agent.writeProjectKnowledgeRecord(localPathContext); + + expect(record?.filePath).toBeTruthy(); + expect(fs.existsSync(record.filePath)).toBe(true); + const content = fs.readFileSync(record.filePath, 'utf8'); + expect(content).toContain('Project Knowledge Overview'); + expect(content).toContain('package.json'); + expect(content).toContain('src/features/game/systems/CombatSystem.ts'); + }); }); diff --git a/tests/secondBrainTrace.test.ts b/tests/secondBrainTrace.test.ts index 38a4749..dc88a8e 100644 --- a/tests/secondBrainTrace.test.ts +++ b/tests/secondBrainTrace.test.ts @@ -242,6 +242,18 @@ describe('Second Brain Trace', () => { ].join('\n'), 'utf8' ); + fs.writeFileSync( + path.join(root, 'Project_Logs', '2026-04-26-Skybound_User_Decision.md'), + [ + '# Skybound User Decision', + '', + '## Decision', + 'Reward card clarity was improved.', + '- `/Volumes/Data/project/Antigravity/Skybound/src/features/game/hooks/useGameEngine.ts`', + 'project antigravity connectai knowledge documentation' + ].join('\n'), + 'utf8' + ); const trace = buildSecondBrainTrace( '그러면 지금 /Volumes/Data/project/Antigravity/ConnectAI 프로젝트에 대한 지식을 만들어', @@ -251,6 +263,9 @@ describe('Second Brain Trace', () => { expect(trace.retrievedDocuments[0].path).toContain('ConnectAI_Project_Knowledge.md'); expect(trace.retrievedDocuments[0].path).not.toContain('Datacollector'); + const skybound = trace.retrievedDocuments.find((doc) => doc.path.includes('Skybound')); + expect(skybound?.canSupportProjectClaim).not.toBe(true); + expect(trace.projectClaimPolicy).not.toBe('cautious'); } finally { fs.rmSync(root, { recursive: true, force: true }); }