feat: v2.62.0 - Astra Autonomous Loop (AAL) foundation & enhanced file analysis

This commit is contained in:
g1nation
2026-05-04 12:58:43 +09:00
parent 445d530b63
commit 215c5f9457
23 changed files with 2964 additions and 62 deletions
+77 -10
View File
@@ -793,7 +793,7 @@ export class AgentExecutor {
return '';
}
const candidates = this.extractLocalProjectPaths(prompt);
const candidates = this.extractLocalProjectPaths(prompt, rootPath);
if (candidates.length === 0) {
return '';
}
@@ -803,6 +803,11 @@ export class AgentExecutor {
'[LOCAL PROJECT PATH PREFLIGHT]',
`Local project intent: ${intent}`,
this.buildLocalProjectIntentGuidance(intent),
'[CRITICAL DIRECTIVE] The file contents below have already been read from the local filesystem. You MUST use them directly in your analysis.',
'DO NOT ask the user to provide, upload, paste, or share the file contents. They are already included below.',
'DO NOT say "파일 내용을 보여주세요", "코드를 공유해 주세요", or "파일을 제공해 주세요". The files have been pre-loaded.',
'If access succeeded, proceed IMMEDIATELY with analysis. Do not ask for confirmation like "진행할까요?" or "분석을 시작할까요?". Just do it.',
'If multiple files are mentioned, analyze them sequentially in the order the user specified without pausing for confirmation between each.',
'The user provided a local project path for review, analysis, documentation, or knowledge creation. 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 succeeded and priority file previews are present, do not say that code was not provided.',
@@ -812,7 +817,7 @@ export class AgentExecutor {
'If intent is thinking, act as a project thinking partner and give a clear verdict grounded in the inspected files.'
];
for (const candidate of candidates.slice(0, 2)) {
for (const candidate of candidates.slice(0, 5)) {
sections.push(this.inspectLocalProjectPath(candidate, rootPath));
}
@@ -1004,8 +1009,27 @@ export class AgentExecutor {
}
private shouldPreflightLocalProjectPath(prompt: string): boolean {
return /(검토|리뷰|분석|확인|봐줘|고쳐|개선|디버그|지식|문서화|문서|정리|기록|위키|저장|만들|생성|설계|아키텍처|구조|방향|의견|생각|판단|어떤\s*거?\s*같|어때|knowledge|document|documentation|wiki|summari[sz]e|review|analy[sz]e|inspect|debug|fix|improve|architecture|design|structure|opinion|think|judge)/i.test(prompt)
&& /\/Volumes\/Data\/project\/Antigravity\/[^\s`"'<>]+/i.test(prompt);
const hasActionKeyword = /(검토|리뷰|분석|확인|봐줘|읽어|열어|파일|내용|코드|고쳐|개선|디버그|지식|문서화|문서|정리|기록|위키|저장|만들|생성|설계|아키텍처|구조|방향|의견|생각|판단|어떤\s*거?\s*같|어때|순서대로|보면|knowledge|document|documentation|wiki|summari[sz]e|review|analy[sz]e|inspect|debug|fix|improve|architecture|design|structure|opinion|think|judge|read|open|file|content|code)/i.test(prompt);
const hasLocalPath = this.containsLocalFilePath(prompt);
return hasActionKeyword && hasLocalPath;
}
/**
* 프롬프트에 로컬 파일/디렉토리 경로가 포함되어 있는지 감지합니다.
* 절대 경로: /Volumes/, /Users/, /home/, ~/
* 상대 경로: src/..., lib/..., components/..., tests/... 등 + 파일 확장자
*/
private containsLocalFilePath(prompt: string): boolean {
// 절대 경로
if (/(?:\/Volumes\/|\/Users\/|\/home\/|~\/)[^\s`"'<>]+/i.test(prompt)) {
return true;
}
// 상대 경로 패턴: 디렉토리/파일명.확장자 형태 (src/lib/engine.ts, components/App.tsx 등)
if (/(?:^|[\s,])(?:src|lib|components|pages|app|tests|test|utils|core|features|hooks|services|config|public|assets|docs|scripts)\//i.test(prompt)
&& /\.[a-z]{1,6}(?:[\s,;)\]]|$)/i.test(prompt)) {
return true;
}
return false;
}
private isProjectKnowledgeCreationRequest(prompt: string): boolean {
@@ -1017,7 +1041,7 @@ export class AgentExecutor {
}
private classifyLocalProjectIntent(prompt: string): LocalProjectIntent {
if (!/\/Volumes\/Data\/project\/Antigravity\/[^\s`"'<>]+/i.test(prompt)) {
if (!this.containsLocalFilePath(prompt)) {
return 'general';
}
@@ -1323,9 +1347,46 @@ export class AgentExecutor {
.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs)[0] || null;
}
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 extractLocalProjectPaths(prompt: string, rootPath?: string): string[] {
const results: string[] = [];
// 1. 절대 경로 감지: /Volumes/, /Users/, /home/, ~/
const absMatches = prompt.match(/(?:\/Volumes\/|\/Users\/|\/home\/|~\/)[^\s`"'<>]+/gi) || [];
for (const m of absMatches) {
results.push(m.replace(/[),.;\]]+$/g, ''));
}
// 2. 상대 경로 감지: src/lib/engine.ts, components/App.tsx 등
const relMatches = prompt.match(/(?:^|[\s,])(?:(?:src|lib|components|pages|app|tests|test|utils|core|features|hooks|services|config|public|assets|docs|scripts)\/[^\s`"'<>]+\.[a-z]{1,6})/gi) || [];
for (const m of relMatches) {
const cleaned = m.trim().replace(/^,\s*/, '').replace(/[),.;\]]+$/g, '');
if (rootPath) {
// 상대 경로를 워크스페이스 기준 절대 경로로 변환
const absPath = path.resolve(rootPath, cleaned);
if (fs.existsSync(absPath)) {
results.push(absPath);
} else {
// 프로젝트 루트 하위 프로젝트들에서도 검색
const subProjects = ['ConnectAI', 'Datacollector_MAC', 'Agent', 'skybound'];
let found = false;
for (const sub of subProjects) {
const subPath = path.resolve(rootPath, sub, cleaned);
if (fs.existsSync(subPath)) {
results.push(subPath);
found = true;
break;
}
}
if (!found) {
results.push(absPath); // fallback: 원래 경로 그대로
}
}
} else {
results.push(cleaned);
}
}
return Array.from(new Set(results));
}
private inspectLocalProjectPath(targetPath: string, rootPath: string): string {
@@ -1342,11 +1403,17 @@ export class AgentExecutor {
const stat = fs.statSync(absPath);
if (!stat.isDirectory()) {
const content = fs.readFileSync(absPath, 'utf8');
const fileName = path.basename(absPath);
const ext = path.extname(absPath).toLowerCase();
// 코드/문서 파일은 더 많은 내용을 제공하여 정밀한 분석이 가능하도록 함
const isCodeOrDoc = ['.ts', '.js', '.tsx', '.jsx', '.py', '.java', '.go', '.rs', '.md', '.json', '.yaml', '.yml', '.toml', '.css', '.html', '.sql', '.sh', '.zsh', '.env', '.xml', '.swift', '.kt'].includes(ext);
const previewLimit = isCodeOrDoc ? 8000 : 2000;
return [
`Path: ${targetPath}`,
'Access: succeeded',
'Type: file',
`Preview:\n${summarizeText(content, 1200)}`
`Type: file (${fileName})`,
`Size: ${content.length} characters`,
`Full content (${content.length <= previewLimit ? 'complete' : `first ${previewLimit} chars`}):\n\`\`\`${ext.slice(1)}\n${summarizeText(content, previewLimit)}\n\`\`\``
].join('\n');
}