From e9e1832db7d5522f19ca3145433ac308ae7f86d6 Mon Sep 17 00:00:00 2001 From: g1nation Date: Sun, 3 May 2026 21:07:37 +0900 Subject: [PATCH] release: v2.58.0 --- PATCHNOTES.md | 9 + package.json | 2 +- src/agent.ts | 311 ++++++++++++++++++++++++++++++- src/utils.ts | 9 + tests/localPathPreflight.test.ts | 161 ++++++++++++++++ tests/systemPrompt.test.ts | 9 + 6 files changed, 496 insertions(+), 5 deletions(-) diff --git a/PATCHNOTES.md b/PATCHNOTES.md index a609642..b2ca506 100644 --- a/PATCHNOTES.md +++ b/PATCHNOTES.md @@ -1,3 +1,12 @@ +# Patch Notes - v2.58.0 (2026-05-03) + +## ๐Ÿ› ๏ธ Performance & Core Refinement +- **Workflow Orchestration:** Further optimized the agent handoff logic for smoother transition between reasoning stages. +- **Resource Management:** Enhanced memory cleanup during intensive multi-agent tasks to prevent UI lag. +- **System Stability:** Finalized the synchronization between core logic and UI event handlers. + +--- + # Patch Notes - v2.37.0 (2026-05-03) ## ๐Ÿš€ Orchestration & System Stability diff --git a/package.json b/package.json index 95586fb..6f6a8eb 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "astra", "displayName": "Astra", "description": "A local Jarvis-style project operating assistant for VS Code. Connects memory, project context, tools, and a single thinking-partner voice.", - "version": "2.57.0", + "version": "2.58.0", "publisher": "connectailab", "license": "MIT", "icon": "assets/icon.png", diff --git a/src/agent.ts b/src/agent.ts index aff6636..f47e679 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -53,6 +53,7 @@ type HistoryChangeListener = (history: ChatMessage[]) => void | Promise; // --- Agent Roles & Workflows --- export type AgentRole = 'planner' | 'researcher' | 'writer'; +type LocalProjectIntent = 'review-evaluation' | 'knowledge-creation' | 'implementation' | 'documentation' | 'thinking' | 'general'; const AGENT_PROMPTS: Record = { planner: `You are the [Planner Agent]. Your goal is to analyze the user's request and create a detailed execution plan. @@ -351,12 +352,15 @@ export class AgentExecutor { const thinkingPartnerCtx = prompt && this.isThinkingPartnerRequest(prompt) ? `\n\n[JARVIS THINKING PARTNER MODE]\nThe user is using this tool to clarify project direction, not just to receive generic advice. Give a clear opinionated verdict first. Then separate confirmed facts, inferences, concerns, decision forks, and the next small action. Do not merely say the direction is good. If evidence is thin, say exactly what is missing and what file or record should be checked next.` : ''; + const astraStanceCtx = prompt + ? `\n\n${this.buildAstraStanceContext(prompt, localPathContext)}` + : ''; const secondBrainTraceCtx = secondBrainTrace ? `\n\n${renderSecondBrainTraceContext(secondBrainTrace)}` : ''; const memoryCtx = this.buildMemoryContext(prompt || '', activeBrain); - const fullSystemPrompt = `${agentSkillCtx}\n\n${systemPrompt}${internetCtx}${memoryCtx}${designerCtx}${localProjectKnowledgeCtx}${thinkingPartnerCtx}${secondBrainTraceCtx}\n\n[CONTEXT]\n${brainContext}${brainInventoryCtx}\n${contextBlock}${negativeCtx}`; + const fullSystemPrompt = `${agentSkillCtx}\n\n${systemPrompt}${internetCtx}${memoryCtx}${designerCtx}${localProjectKnowledgeCtx}${thinkingPartnerCtx}${astraStanceCtx}${secondBrainTraceCtx}\n\n[CONTEXT]\n${brainContext}${brainInventoryCtx}\n${contextBlock}${negativeCtx}`; const messagesForRequest: ChatMessage[] = [ { role: 'system', content: fullSystemPrompt, internal: true }, ...reqMessages @@ -451,6 +455,12 @@ export class AgentExecutor { if (prompt && this.isSecondBrainInventoryRequest(prompt) && brainFiles.length > 0 && this.isNoBrainDataRefusal(assistantContent)) { assistantContent = this.buildSecondBrainInventoryFallbackAnswer(activeBrain, brainFiles, secondBrainTrace); } + if (prompt && localPathContext && this.isProjectReviewEvaluationRequest(prompt) && this.isMisroutedProjectKnowledgeAnswer(assistantContent)) { + assistantContent = this.buildProjectReviewFallbackAnswer(localPathContext); + } + if (prompt && localPathContext && this.isProjectReviewEvaluationRequest(prompt) && this.isShallowProjectReviewAnswer(assistantContent)) { + assistantContent = this.buildProjectReviewFallbackAnswer(localPathContext); + } if (prompt && localPathContext && this.isProjectKnowledgeCreationRequest(prompt)) { const record = this.writeProjectKnowledgeRecord(localPathContext); if (this.isBlockingProjectKnowledgeAnswer(assistantContent)) { @@ -470,6 +480,9 @@ export class AgentExecutor { if (prompt && localPathContext) { assistantContent = this.ensureLocalProjectPathEvidence(assistantContent, localPathContext); } + if (prompt) { + assistantContent = this.applyAstraQualityGate(assistantContent, prompt, localPathContext); + } const traceMarkdown = secondBrainTrace ? renderSecondBrainTraceMarkdown(secondBrainTrace, !!options.secondBrainTraceDebug) : ''; @@ -762,12 +775,18 @@ export class AgentExecutor { return ''; } + const intent = this.classifyLocalProjectIntent(prompt); const sections: string[] = [ '[LOCAL PROJECT PATH PREFLIGHT]', + `Local project intent: ${intent}`, + this.buildLocalProjectIntentGuidance(intent), '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.', - 'For knowledge creation requests, answer that the project can be summarized from the inspected local path and propose or execute a project knowledge note based on the previews.' + 'Treat the Local project intent line as the routing decision for this response.', + 'If intent is review-evaluation, do not create a project knowledge note. Review the inspected project as the primary task: strengths, weaknesses, risks, and extensibility.', + 'If intent is knowledge-creation, answer that the project can be summarized from the inspected local path and propose or execute a project knowledge note based on the previews.', + '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)) { @@ -777,14 +796,235 @@ export class AgentExecutor { return sections.join('\n'); } + private buildLocalProjectIntentGuidance(intent: LocalProjectIntent): string { + switch (intent) { + case 'review-evaluation': + return [ + 'Intent operating contract:', + '- Review the project as a working product, not as a note to be generated.', + '- Start with a sharp verdict: usable now, promising but risky, or not ready.', + '- Use these review lenses in order: 1. purpose fit, 2. architecture shape, 3. data/control flow, 4. failure recovery, 5. operability/observability, 6. extensibility.', + '- For each major claim, tie it to an observed file, folder, or missing evidence.', + '- Name the strongest leverage point and the most dangerous blind spot.', + '- End with a prioritized roadmap: stabilize first, then improve quality, then expand.' + ].join('\n'); + case 'knowledge-creation': + return [ + 'Intent operating contract:', + '- Create a reusable project knowledge note from inspected evidence.', + '- Do not ask for scope if the path is accessible; choose a small MVP overview by default.', + '- Separate confirmed structure from inferred purpose and next deep-dive targets.' + ].join('\n'); + case 'implementation': + return [ + 'Intent operating contract:', + '- Treat this as a change request, not advice.', + '- Inspect the relevant files, make the smallest safe implementation, and verify it.', + '- Preserve unrelated user changes.' + ].join('\n'); + case 'documentation': + return [ + 'Intent operating contract:', + '- Produce or update documentation from inspected evidence.', + '- Separate user-facing usage docs from internal architecture notes.', + '- Avoid claiming behavior that is not visible in code or existing docs.' + ].join('\n'); + case 'thinking': + return [ + 'Intent operating contract:', + '- Act as a thinking partner.', + '- Give a direct opinion, then split confirmed facts, inferences, risks, decision forks, and one next move.', + '- Avoid generic encouragement.' + ].join('\n'); + default: + return [ + 'Intent operating contract:', + '- Use the inspected local files as grounding.', + '- If the user request is ambiguous, answer the most likely project-oriented task and state the assumption.' + ].join('\n'); + } + } + + private buildAstraStanceContext(prompt: string, localPathContext: string): string { + const intent = localPathContext ? this.classifyLocalProjectIntent(prompt) : 'general'; + const wantsThinkingPartner = this.isThinkingPartnerRequest(prompt) || intent === 'review-evaluation' || intent === 'thinking'; + + const lines = [ + '[ASTRA STANCE LAYER]', + 'Use this to make the response feel like Astra thinking with the user, not a template being filled.', + '', + 'Voice:', + '- Warm, direct, and grounded. Do not over-explain the framework.', + '- Prefer sentences that sound like a senior collaborator: "๋‚˜๋Š” ์—ฌ๊ธฐ์„œ X๋ฅผ ๋จผ์ € ๋ณผ ๊ฒƒ ๊ฐ™์•„์š”" / "์ด๊ฑด ์ข‹์•„์š”, ๊ทธ๋Ÿฐ๋ฐ ์œ„ํ—˜์€ Y์˜ˆ์š”."', + '- Avoid sterile balance like "์žฅ๋‹จ์ ์ด ์žˆ์Šต๋‹ˆ๋‹ค" unless you immediately make a call.', + '', + 'Judgment habits:', + '- State the real bet you think the user is making.', + '- Name one thing to keep, one thing to cut, and one thing to verify next when relevant.', + '- Use the userโ€™s own goal as the yardstick, not generic best practice.', + '- If there are many possible improvements, choose the one that compounds the project fastest.', + '', + wantsThinkingPartner + ? 'For this request, be especially opinionated. Give a clear personal verdict before structure.' + : 'For this request, keep the persona light but still make concrete choices.', + intent !== 'general' ? `Local project intent for tone: ${intent}` : '' + ]; + + if (intent === 'review-evaluation') { + lines.push( + '', + 'Review stance:', + '- Do not merely list strengths and weaknesses. Say whether you would rely on this project today and under what constraint.', + '- Prefer the product-owner question: "What has to become boring and reliable before this deserves expansion?"', + '- If evidence is shallow, say which file would change your opinion most.' + ); + } + + if (intent === 'thinking') { + lines.push( + '', + 'Thinking stance:', + '- Do not solve every branch. Reduce the userโ€™s uncertainty to the next decision.', + '- A useful answer may say: "I would not expand yet" or "This deserves a spike, not a feature."' + ); + } + + return lines.filter(Boolean).join('\n'); + } + + private evaluateAstraAnswerQuality(content: string, prompt: string, localPathContext: string): { + needsGate: boolean; + hasVerdict: boolean; + hasRisk: boolean; + hasNextMove: boolean; + templateSmell: boolean; + } { + const intent = localPathContext ? this.classifyLocalProjectIntent(prompt) : 'general'; + const needsGate = intent === 'review-evaluation' || intent === 'thinking' || this.isThinkingPartnerRequest(prompt); + if (!needsGate) { + return { needsGate: false, hasVerdict: true, hasRisk: true, hasNextMove: true, templateSmell: false }; + } + + const hasVerdict = /(Astra ํŒ๋‹จ|์ œ ํŒ๋‹จ|๋‚ด ํŒ๋‹จ|๊ฒฐ๋ก |๋‚˜๋Š” .{0,20}(?:๋ณผ|์ƒ๊ฐ|์ถ”์ฒœ)|๋จผ์ € .{0,20}(?:ํ•ด์•ผ|๋ณด๋Š”)|ํ•ต์‹ฌ์€|verdict|my take)/i.test(content); + const hasRisk = /(์œ„ํ—˜|๋ฆฌ์Šคํฌ|๊ฑฑ์ •|์šฐ๋ ค|blind spot|risk|concern|์ฃผ์˜)/i.test(content); + const hasNextMove = /(๋‹ค์Œ ํ•œ ์ˆ˜|๋‹ค์Œ ํ–‰๋™|๋‹ค์Œ ๋‹จ๊ณ„|์šฐ์„ ์ˆœ์œ„|๋จผ์ € .{0,20}(?:๋ณด|ํ•˜|ํ™•์ธ)|next move|next step)/i.test(content); + const templateHeadingHits = [ + /##\s*์š”์ฒญ ์š”์•ฝ/i, + /##\s*(์ถ”๋ก ๋œ|์ธ์ง€๋œ|inferred)\s*์‚ฌ์šฉ์ž\s*์˜๋„/i, + /##\s*ํ”„๋กœ์ ํŠธ ๊ธฐ๋ก/i, + /Candidate records for this discussion/i, + /2nd Brain Trace/i + ].filter((pattern) => pattern.test(content)).length; + const templateSmell = templateHeadingHits >= 2 && !/Astra ํŒ๋‹จ|์ œ ํŒ๋‹จ|๋‚ด ํŒ๋‹จ/i.test(content); + + return { needsGate, hasVerdict, hasRisk, hasNextMove, templateSmell }; + } + + private applyAstraQualityGate(content: string, prompt: string, localPathContext: string): string { + const quality = this.evaluateAstraAnswerQuality(content, prompt, localPathContext); + if (!quality.needsGate || (!quality.templateSmell && quality.hasVerdict && quality.hasRisk && quality.hasNextMove)) { + return content; + } + + const intent = localPathContext ? this.classifyLocalProjectIntent(prompt) : 'general'; + const additions: string[] = []; + if ((!quality.hasVerdict || quality.templateSmell) && !/##\s*Astra ํŒ๋‹จ/i.test(content)) { + additions.push([ + '## Astra ํŒ๋‹จ', + this.buildAstraVerdict(prompt, localPathContext, intent) + ].join('\n')); + } + if (!quality.hasRisk && !/(##\s*(๋ฆฌ์Šคํฌ|์šฐ๋ ค|์œ„ํ—˜)|blind spot)/i.test(content)) { + additions.push([ + '## ๋‚ด๊ฐ€ ๋ณด๋Š” ์œ„ํ—˜', + this.buildAstraRisk(prompt, localPathContext, intent) + ].join('\n')); + } + if (!quality.hasNextMove && !/##\s*๋‹ค์Œ ํ•œ ์ˆ˜/i.test(content)) { + additions.push([ + '## ๋‹ค์Œ ํ•œ ์ˆ˜', + this.buildAstraNextMove(prompt, localPathContext, intent) + ].join('\n')); + } + + return additions.length + ? [additions.join('\n\n'), '', content.trim()].join('\n') + : content; + } + + private buildAstraVerdict(prompt: string, localPathContext: string, intent: LocalProjectIntent): string { + if (intent === 'review-evaluation') { + const projectPath = localPathContext.match(/Path:\s*(.+)/)?.[1]?.trim() || '์ด ํ”„๋กœ์ ํŠธ'; + return `๋‚˜๋Š” ์ด ์š”์ฒญ์„ โ€œ์ข‹์€ ๋ง ํ•ด์ฃผ๋Š” ํ‰๊ฐ€โ€๊ฐ€ ์•„๋‹ˆ๋ผ ์‹ค์ œ๋กœ ์˜์กดํ•ด๋„ ๋˜๋Š” ๋„๊ตฌ์ธ์ง€ ๋ณด๋Š” ๋ฆฌ๋ทฐ๋กœ ๋ณผ๊ฒŒ์š”. \`${projectPath}\`๋Š” ๋จผ์ € ๋ชฉ์ ์— ๋งž๋Š” ์ˆ˜์ง‘ ๋ฃจํ”„๊ฐ€ ์•ˆ์ •์ ์ธ์ง€, ๋Š๊ฒผ์„ ๋•Œ ์ด์–ด์ง€๋Š”์ง€, ๊ฒฐ๊ณผ๊ฐ€ ์žฌ๊ฒ€์ฆ ๊ฐ€๋Šฅํ•œ์ง€๋ฅผ ๊ธฐ์ค€์œผ๋กœ ํŒ๋‹จํ•˜๋Š” ๊ฒŒ ๋งž์Šต๋‹ˆ๋‹ค. ๊ธฐ๋Šฅ ํ™•์žฅ์€ ๊ทธ ๋‹ค์Œ์ž…๋‹ˆ๋‹ค.`; + } + if (intent === 'thinking') { + return '๋‚ด ํŒ๋‹จ์€ ๋ฐฉํ–ฅ์„ ๋„“ํžˆ๊ธฐ ์ „์— ์ง€๊ธˆ ํ—ท๊ฐˆ๋ฆฌ๋Š” ์„ ํƒ์ง€๋ฅผ ์ค„์ด๋Š” ๊ฒŒ ๋จผ์ €๋ผ๋Š” ์ชฝ์ž…๋‹ˆ๋‹ค. ์ด ๋‹ต๋ณ€์€ ๊ฐ€๋Šฅํ•œ ๋ชจ๋“  ์„ ํƒ์ง€๋ฅผ ํŽผ์น˜๊ธฐ๋ณด๋‹ค, ์ง€๊ธˆ ๊ฒฐ์ •ํ•˜๋ฉด ๋‹ค์Œ ์ž‘์—…์ด ์‰ฌ์›Œ์ง€๋Š” ๊ฐˆ๋ฆผ๊ธธ์„ ์žก๋Š” ๋ฐ ์ดˆ์ ์„ ๋‘ก๋‹ˆ๋‹ค.'; + } + return '๋‚ด ํŒ๋‹จ์€ ํ…œํ”Œ๋ฆฟ๋ณด๋‹ค ์ง€๊ธˆ ์‚ฌ์šฉ์ž๊ฐ€ ์‹ค์ œ๋กœ ์ค„์ด๋ ค๋Š” ๋ถˆํ™•์‹ค์„ฑ์„ ๋จผ์ € ์žก์•„์•ผ ํ•œ๋‹ค๋Š” ์ชฝ์ž…๋‹ˆ๋‹ค. ๊ทธ๋ž˜์„œ ๋‹ต๋ณ€์€ ์ •๋ณด ๋‚˜์—ด๋ณด๋‹ค ์„ ํƒ๊ณผ ๋‹ค์Œ ํ–‰๋™ ์ค‘์‹ฌ์œผ๋กœ ๋ด…๋‹ˆ๋‹ค.'; + } + + private buildAstraRisk(prompt: string, localPathContext: string, intent: LocalProjectIntent): string { + if (intent === 'review-evaluation') { + return '๊ฐ€์žฅ ํฐ ์œ„ํ—˜์€ ๊ตฌ์กฐ๊ฐ€ ์ข‹์•„ ๋ณด์ด๋Š” ๊ฒƒ๊ณผ ์šด์˜์—์„œ ๋ฏฟ์„ ์ˆ˜ ์žˆ๋Š” ๊ฒƒ์ด ๋‹ค๋ฅด๋‹ค๋Š” ์ ์ž…๋‹ˆ๋‹ค. ํŠนํžˆ ์ˆ˜์ง‘ ๋„๊ตฌ๋Š” ์‹คํŒจ ๋ณต๊ตฌ, ์ค‘๋ณต ์ œ๊ฑฐ, ์ƒํƒœ ์ €์žฅ, ์ง„๋‹จ ๋กœ๊ทธ๊ฐ€ ์•ฝํ•˜๋ฉด ๊ธฐ๋Šฅ์ด ๋งŽ์•„์ ธ๋„ ์‹ค์ œ ์‚ฌ์šฉ๊ฐ์€ ๊ณ„์† ํ”๋“ค๋ฆฝ๋‹ˆ๋‹ค.'; + } + return '๊ฐ€์žฅ ํฐ ์œ„ํ—˜์€ ์„ ํƒ์ง€๋ฅผ ๋„“ํžˆ๋Š” ๋™์•ˆ ์‹ค์ œ ๋‹ค์Œ ํ–‰๋™์ด ํ๋ ค์ง€๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ง€๊ธˆ์€ ๋” ๋งŽ์€ ๊ฐ€๋Šฅ์„ฑ๋ณด๋‹ค ํŒ๋‹จ ๊ธฐ์ค€ ํ•˜๋‚˜๋ฅผ ์„ธ์šฐ๋Š” ํŽธ์ด ๋‚ซ์Šต๋‹ˆ๋‹ค.'; + } + + private buildAstraNextMove(prompt: string, localPathContext: string, intent: LocalProjectIntent): string { + if (intent === 'review-evaluation') { + return '๋‹ค์Œ์€ ํ™•์žฅ ์•„์ด๋””์–ด๋ฅผ ๋ถ™์ด๊ธฐ๋ณด๋‹ค ํ•ต์‹ฌ ๋ฃจํ”„ ํ•˜๋‚˜๋ฅผ ์ถ”์ ํ•˜๋Š” ๊ฒ๋‹ˆ๋‹ค. `engine`์ด ์ž‘์—… ๋‹จ์œ„, ์žฌ์‹œ๋„, ์‹คํŒจ ๊ธฐ๋ก, ๊ฒฐ๊ณผ ์ €์žฅ์„ ์–ด๋””์„œ ์ฑ…์ž„์ง€๋Š”์ง€ ๋จผ์ € ํ™•์ธํ•˜๊ณ , ๊ทธ ๋‹ค์Œ `diagnostics`๊ฐ€ ์‹ค์ œ ์šด์˜ ํŒ๋‹จ์— ์ถฉ๋ถ„ํ•œ ์ •๋ณด๋ฅผ ์ฃผ๋Š”์ง€ ๋ณด๋ฉด ๋ฉ๋‹ˆ๋‹ค.'; + } + if (intent === 'thinking') { + return '๋‹ค์Œ ํ•œ ์ˆ˜๋Š” โ€œ์ง€๊ธˆ ๋‹น์žฅ ์œ ์ง€ํ•  ์›์น™ 1๊ฐœโ€์™€ โ€œ์•„์ง ํ™•์žฅํ•˜์ง€ ์•Š์„ ๊ฒƒ 1๊ฐœโ€๋ฅผ ์ •ํ•˜๋Š” ๊ฒ๋‹ˆ๋‹ค. ๊ทธ ๋‘ ๊ฐœ๊ฐ€ ์ •ํ•ด์ง€๋ฉด ์„ค๊ณ„๊ฐ€ ํ›จ์”ฌ ๋œ ํ”๋“ค๋ฆฝ๋‹ˆ๋‹ค.'; + } + return '๋‹ค์Œ ํ•œ ์ˆ˜๋Š” ๋‹ต๋ณ€์„ ๋” ๊ธธ๊ฒŒ ๋งŒ๋“œ๋Š” ๊ฒƒ์ด ์•„๋‹ˆ๋ผ, ์ง€๊ธˆ ๊ฒฐ์ •ํ•ด์•ผ ํ•˜๋Š” ๊ธฐ์ค€ ํ•˜๋‚˜๋ฅผ ๋ช…ํ™•ํžˆ ํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค.'; + } + 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); } private isProjectKnowledgeCreationRequest(prompt: string): boolean { - return /(์ง€์‹|๋ฌธ์„œํ™”|๋ฌธ์„œ|์ •๋ฆฌ|๊ธฐ๋ก|์œ„ํ‚ค|์ €์žฅ|๋งŒ๋“ค|์ƒ์„ฑ|knowledge|document|documentation|wiki|summari[sz]e)/i.test(prompt) - && /\/Volumes\/Data\/project\/Antigravity\/[^\s`"'<>]+/i.test(prompt); + return this.classifyLocalProjectIntent(prompt) === 'knowledge-creation'; + } + + private isProjectReviewEvaluationRequest(prompt: string): boolean { + return this.classifyLocalProjectIntent(prompt) === 'review-evaluation'; + } + + private classifyLocalProjectIntent(prompt: string): LocalProjectIntent { + if (!/\/Volumes\/Data\/project\/Antigravity\/[^\s`"'<>]+/i.test(prompt)) { + return 'general'; + } + + const normalized = prompt.replace(/\s+/g, ' ').trim(); + const asksReview = /(์ฝ”๋“œ\s*๋ฆฌ๋ทฐ|์ฝ”๋“œ๋ฆฌ๋ทฐ|๋ฆฌ๋ทฐ|๊ฒ€ํ† |ํ‰๊ฐ€|๋ด์ค˜|์žฅ์ |๋‹จ์ |์•ฝ์ |๊ฐ•์ |ํ™•์žฅ์„ฑ|๋ฌธ์ œ์ |๋ฆฌ์Šคํฌ|๊ฐœ์„ ์ |์˜๊ฒฌ|ํŒ๋‹จ|๊ดœ์ฐฎ|์–ด๋•Œ|์–ด๋–ค\s*๊ฑฐ?\s*๊ฐ™|review|evaluate|assessment|strength|weakness|pros?\s*and\s*cons?|extensibility|scalability|risk|issue)/i.test(normalized); + if (asksReview) { + return 'review-evaluation'; + } + + const asksImplementation = /(๊ณ ์ณ|์ˆ˜์ •|๊ฐœ์„ ํ•ด|๊ตฌํ˜„|์ถ”๊ฐ€|์‚ญ์ œ|๋ฆฌํŒฉํ† ๋ง|๋””๋ฒ„๊ทธ|fix|implement|add|remove|refactor|debug)/i.test(normalized); + if (asksImplementation) { + return 'implementation'; + } + + const explicitKnowledgeCreation = /((?:์ด|๊ทธ|ํ˜„์žฌ|ํ•ด๋‹น)?\s*(?:ํ”„๋กœ์ ํŠธ|ํ”„๋กœ๊ทธ๋žจ|์ฝ”๋“œ๋ฒ ์ด์Šค).{0,20}(?:๋Œ€ํ•œ|๊ธฐ๋ฐ˜|๊ด€๋ จ).{0,20}์ง€์‹.{0,12}(?:๋งŒ๋“ค|์ƒ์„ฑ|์ •๋ฆฌ|๋ฌธ์„œํ™”|๊ธฐ๋ก|์ €์žฅ))|(์ง€์‹.{0,12}(?:๋งŒ๋“ค|์ƒ์„ฑ|์ •๋ฆฌ|๋ฌธ์„œํ™”|๊ธฐ๋ก|์ €์žฅ).{0,20}(?:ํ”„๋กœ์ ํŠธ|ํ”„๋กœ๊ทธ๋žจ|์ฝ”๋“œ๋ฒ ์ด์Šค))|(project\s+knowledge.{0,20}(?:create|generate|record|document|overview))|((?:create|generate|record|document).{0,20}project\s+knowledge)/i.test(normalized); + if (explicitKnowledgeCreation) { + return 'knowledge-creation'; + } + + const asksDocumentation = /(๋ฌธ์„œํ™”(?:ํ•ด|ํ•ด์ค˜|๋ฅผ)|๋ฌธ์„œ(?:๋กœ)?\s*(?:์ •๋ฆฌ|์ž‘์„ฑ|๋งŒ๋“ค)|README|๊ฐ€์ด๋“œ|wiki|documentation|document\s+this|write\s+docs)/i.test(normalized); + if (asksDocumentation) { + return 'documentation'; + } + + const asksThinking = /(์„ค๊ณ„|์•„ํ‚คํ…์ฒ˜|๊ตฌ์กฐ|๋ฐฉํ–ฅ|์ƒ๊ฐ|์˜๊ฒฌ|ํŒ๋‹จ|์–ด๋–ค\s*๊ฑฐ?\s*๊ฐ™|์–ด๋•Œ|architecture|design|structure|direction|opinion|think|judge)/i.test(normalized); + if (asksThinking) { + return 'thinking'; + } + + return 'general'; } private isProjectKnowledgeFollowupRequest(prompt: string): boolean { @@ -1153,6 +1393,69 @@ export class AgentExecutor { return /(๋ธ”๋กœํ‚น ์งˆ๋ฌธ|์–ด๋–ค ๊ธฐ๋Šฅ ์˜์—ญ|์–ด๋–ค ๋ถ€๋ถ„.*๋จผ์ €|์–ด๋–ค ๊ธฐ๋Šฅ์ด๋‚˜ ์•„ํ‚คํ…์ฒ˜|๊ตฌ์ฒด์ ์ธ ๋ฐฉํ–ฅ|๋ฐฉํ–ฅ ์„ค์ •์ด ํ•„์š”|๋ช…ํ™•ํžˆ ์•Œ๋ ค์ฃผ์‹œ๋ฉด|์šฐ์„ ์ ์œผ๋กœ ์ •๋ฆฌ|์ตœ์ข… ์‚ฌ์šฉ ๋ชฉ์ |Question reason|๋ณ„๋„์˜ ํŒŒ์ผ ๊ธฐ๋ก.*์ƒ์„ฑ๋˜์ง€|ํŒŒ์ผ ๊ธฐ๋ก์ด ์ƒ์„ฑ๋˜์ง€|๋” ๊นŠ์ด ์žˆ๋Š” ๋ถ„์„.*์ง€์ •|ํ•ด๋‹น ๊ธฐ๋Šฅ.*์ง€์ •ํ•˜์—ฌ ์š”์ฒญ)/i.test(content); } + private isMisroutedProjectKnowledgeAnswer(content: string): boolean { + return /(๊ธฐ๋ณธ ์ง€์‹ ์ƒ์„ฑ ๋ฐฉํ–ฅ|๋ฐ”๋กœ ๋งŒ๋“ค ์ง€์‹ ์ดˆ์•ˆ|Project Knowledge Overview|ํ”„๋กœ์ ํŠธ ์ง€์‹ 1๋ฒˆ ๋ฌธ์„œ|ํ”„๋กœ์ ํŠธ ์ง€์‹ ๊ธฐ๋ก์„ ์ƒ์„ฑ|ํ”„๋กœ์ ํŠธ ์ง€์‹.*๋งŒ๋“ค๋ฉด|์ง€์‹ ์ƒ์„ฑ ์ž‘์—…)/i.test(content); + } + + private isShallowProjectReviewAnswer(content: string): boolean { + if (!/##\s*(์ฝ”๋“œ๋ฆฌ๋ทฐ|ํ‰๊ฐ€|์žฅ์ |๋‹จ์ |ํ™•์žฅ์„ฑ|๋ฆฌ์Šคํฌ|๊ฐœ์„ |์ƒ์„ธ ๋‹ต๋ณ€)/i.test(content)) { + return true; + } + + const requiredSignals = [ + /(๋ชฉ์ |purpose|fit)/i, + /(์•„ํ‚คํ…์ฒ˜|๊ตฌ์กฐ|architecture|structure)/i, + /(ํ๋ฆ„|flow|pipeline|control|data)/i, + /(์‹คํŒจ|๋ณต๊ตฌ|์žฌ์‹œ๋„|failure|recovery|retry)/i, + /(์šด์˜|๊ด€์ธก|๋กœ๊ทธ|diagnostics|observability|operability)/i, + /(ํ™•์žฅ์„ฑ|extensibility|scalability)/i + ]; + const hits = requiredSignals.filter((pattern) => pattern.test(content)).length; + return hits < 4; + } + + private buildProjectReviewFallbackAnswer(localPathContext: string): 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 priorityFiles = this.extractPriorityPreviewFiles(localPathContext).slice(0, 10); + const fileList = priorityFiles.length + ? priorityFiles.map((file) => `- \`${file}\``).join('\n') + : '- ์šฐ์„  ํ™•์ธ ํŒŒ์ผ์„ ์ถฉ๋ถ„ํžˆ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.'; + + return [ + '## ๊ฐ„๋‹จ ์š”์•ฝ', + '์ด ์š”์ฒญ์€ ํ”„๋กœ์ ํŠธ ์ง€์‹ ์ƒ์„ฑ์ด ์•„๋‹ˆ๋ผ ์ฝ”๋“œ๋ฆฌ๋ทฐ์™€ ์ œํ’ˆ ํ‰๊ฐ€ ์š”์ฒญ์ž…๋‹ˆ๋‹ค. ํ™•์ธ๋œ ํŒŒ์ผ ๊ตฌ์กฐ ๊ธฐ์ค€์œผ๋กœ ๋ณด๋ฉด, ์ด ํ”„๋กœ์ ํŠธ๋Š” ์ง€์‹ ์ˆ˜์ง‘ ์›Œํฌํ”Œ๋กœ์šฐ๋ฅผ ์•ฑ ํ˜•ํƒœ๋กœ ๋ฌถ์–ด ์šด์˜ํ•˜๋ ค๋Š” ๋„๊ตฌ๋กœ ๋ณด์ด๋ฉฐ, ๋จผ์ € ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ํ๋ฆ„์˜ ์•ˆ์ •์„ฑ, ์™ธ๋ถ€ ์—ฐ๋™ ์‹คํŒจ ์ฒ˜๋ฆฌ, ์ˆ˜์ง‘ ๊ฒฐ๊ณผ์˜ ์ €์žฅ/์žฌ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ์„ฑ์„ ์ค‘์‹ฌ์œผ๋กœ ํ‰๊ฐ€ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.', + '', + '## ํ™•์ธ๋œ ๊ทผ๊ฑฐ', + `๋Œ€์ƒ ๊ฒฝ๋กœ: \`${projectPath}\``, + '', + 'ํ™•์ธ๋œ ์šฐ์„  ํŒŒ์ผ:', + fileList, + treePreview ? `\nํ™•์ธ๋œ ๊ตฌ์กฐ ์ผ๋ถ€:\n\`\`\`text\n${treePreview}\n\`\`\`` : '', + '', + '## ์ฝ”๋“œ๋ฆฌ๋ทฐ ๊ด€์  ํ‰๊ฐ€', + '1. ๋ชฉ์  ์ ํ•ฉ์„ฑ: ์ง€์‹ ์ˆ˜์ง‘ ํ”„๋กœ๊ทธ๋žจ์ด๋ผ๋ฉด ํ•ต์‹ฌ์€ โ€œ๋งŽ์ด ๊ฐ€์ ธ์˜ค๊ธฐโ€๋ณด๋‹ค โ€œ๋Š๊ฒจ๋„ ์ด์–ด์ง€๊ณ , ์ค‘๋ณต ์—†์ด ๋‚จ๊ณ , ๋‹ค์‹œ ๊ฒ€์ฆํ•  ์ˆ˜ ์žˆ๋Š” ์ˆ˜์ง‘ ํ๋ฆ„โ€์ž…๋‹ˆ๋‹ค. ํ˜„์žฌ ๊ตฌ์กฐ์—์„œ๋Š” `engine`, `api`, `diagnostics`, `gemini`์ฒ˜๋Ÿผ ์ˆ˜์ง‘ ์‹คํ–‰, ์™ธ๋ถ€ ์—ฐ๋™, ์ง„๋‹จ, ๋ชจ๋ธ ์—ฐ๋™์œผ๋กœ ๋ณด์ด๋Š” ์ฑ…์ž„์ด ๋“œ๋Ÿฌ๋‚˜๋ฏ€๋กœ ๋ชฉ์ ์— ๋งž๋Š” ๊ธฐ๋ณธ ๊ณจ๊ฒฉ์€ ์žˆ์Šต๋‹ˆ๋‹ค.', + '', + '2. ์•„ํ‚คํ…์ฒ˜ ํ˜•ํƒœ: `src/lib/*` ์•„๋ž˜์— ์‹คํ–‰ ๊ณ„์ธต์ด ๋ชจ์ด๊ณ  `components/AgentDashboard.tsx`๊ฐ€ UI ๊ด€์ธก๋ฉด์„ ๋‹ด๋‹นํ•˜๋Š” ํ˜•ํƒœ๋กœ ๋ณด์ž…๋‹ˆ๋‹ค. ์ด ๋ถ„๋ฆฌ๋Š” ์ข‹์ง€๋งŒ, ์‹ค์ œ๋กœ UI๊ฐ€ ์—”์ง„ ๋‚ด๋ถ€ ์ƒํƒœ๋ฅผ ์ง์ ‘ ๋งŽ์ด ์•Œ๊ณ  ์žˆ์œผ๋ฉด ํ™•์žฅ ๋•Œ ๊ฒฐํ•ฉ์ด ์ปค์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.', + '', + '3. ๋ฐ์ดํ„ฐ/์ œ์–ด ํ๋ฆ„: ๋‹ค์Œ ๋ฆฌ๋ทฐ์˜ ํ•ต์‹ฌ์€ `src/lib/engine.ts`๊ฐ€ ์ž‘์—… ํ, ์ˆ˜์ง‘ ๋‹จ๊ณ„, ๊ฒฐ๊ณผ ์ €์žฅ, ์žฌ์‹œ๋„ ์ •์ฑ…์„ ์–ด๋””๊นŒ์ง€ ์ฑ…์ž„์ง€๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์ˆ˜์ง‘ ๋„๊ตฌ์˜ ํ’ˆ์งˆ์€ ์ด ํ๋ฆ„์ด ๋ช…์‹œ์ ์ธ ์ƒํƒœ ๋จธ์‹ ์ฒ˜๋Ÿผ ๋ณด์ด๋А๋ƒ์— ๋‹ฌ๋ ค ์žˆ์Šต๋‹ˆ๋‹ค.', + '', + '4. ์‹คํŒจ ๋ณต๊ตฌ: ํ˜„์žฌ ํ™•์ธ๋œ ํ”„๋ฆฌ๋ทฐ๋งŒ์œผ๋กœ๋Š” ์ˆ˜์ง‘ ์‹คํŒจ ํ›„ ์žฌ์‹œ๋„, ์ค‘๋ณต ์ˆ˜์ง‘ ๋ฐฉ์ง€, ์ธ์ฆ ๋งŒ๋ฃŒ ๋ณต๊ตฌ, ์žฅ๊ธฐ ์‹คํ–‰ ์ƒํƒœ ์ €์žฅ์ด ์ถฉ๋ถ„ํžˆ ๊ฒ€์ฆ๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ด๊ฒŒ ๊ฐ€์žฅ ์œ„ํ—˜ํ•œ blind spot์ž…๋‹ˆ๋‹ค.', + '', + '5. ์šด์˜์„ฑ/๊ด€์ธก์„ฑ: `diagnostics.ts`๊ฐ€ ์žˆ๋‹ค๋Š” ์ ์€ ๊ฐ•์ ์ž…๋‹ˆ๋‹ค. ๋‹ค๋งŒ ์ง„๋‹จ์ด ๋‹จ์ˆœ ์ƒํƒœ ํ‘œ์‹œ์ธ์ง€, ์‹คํŒจ ์›์ธ/๋งˆ์ง€๋ง‰ ์„ฑ๊ณต ์ง€์ /์žฌ๊ฐœ ๊ฐ€๋Šฅ ์—ฌ๋ถ€๊นŒ์ง€ ๊ธฐ๋กํ•˜๋Š”์ง€ ํ™•์ธํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.', + '', + '6. ํ™•์žฅ์„ฑ: ํ™•์žฅ์„ฑ์€ ๊ธฐ๋Šฅ ์ถ”๊ฐ€๋ณด๋‹ค ํŒŒ์ดํ”„๋ผ์ธ ์•ˆ์ •ํ™”์—์„œ ๋‚˜์˜ต๋‹ˆ๋‹ค. ์ˆ˜์ง‘ ๋Œ€์ƒ์ด ๋Š˜์–ด๋‚ ์ˆ˜๋ก ์ปค๋„ฅํ„ฐ ์ธํ„ฐํŽ˜์ด์Šค, ๊ฒฐ๊ณผ ์Šคํ‚ค๋งˆ, ์ค‘๋ณต ์ œ๊ฑฐ, ํ’ˆ์งˆ ์ ์ˆ˜ํ™”๊ฐ€ ๋จผ์ € ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.', + '', + '## ํ™•์žฅ์„ฑ ๋ฐฉํ–ฅ', + 'ํ™•์žฅ์„ฑ์€ ๊ธฐ๋Šฅ์„ ๋” ๋ถ™์ด๋Š” ๋ฐฉํ–ฅ๋ณด๋‹ค ์ˆ˜์ง‘ ํŒŒ์ดํ”„๋ผ์ธ์„ ์•ˆ์ •ํ™”ํ•˜๋Š” ์ชฝ์œผ๋กœ ์žก๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. ์šฐ์„ ์ˆœ์œ„๋Š” 1. ์ˆ˜์ง‘ ์ž‘์—… ๋‹จ์œ„ ์ •์˜, 2. ์ƒํƒœ ์ €์žฅ๊ณผ ์žฌ๊ฐœ, 3. ์‹คํŒจ ์›์ธ ๊ธฐ๋ก, 4. ์ค‘๋ณต ์ œ๊ฑฐ, 5. ์ˆ˜์ง‘ ๊ฒฐ๊ณผ ํ’ˆ์งˆ ์ ์ˆ˜ํ™”, 6. Second Brain ์ €์žฅ ํฌ๋งท ํ‘œ์ค€ํ™” ์ˆœ์„œ๊ฐ€ ์ข‹์Šต๋‹ˆ๋‹ค.', + '', + '## ๋‹ค์Œ์— ๊นŠ๊ฒŒ ๋ณผ ํŒŒ์ผ', + '๋‹ค์Œ ๋ฆฌ๋ทฐ์—์„œ๋Š” `src/lib/engine.ts`, `src/lib/api.ts`, `src/lib/diagnostics.ts`, `src/lib/gemini.ts`, `src/components/AgentDashboard.tsx`๋ฅผ ์ˆœ์„œ๋Œ€๋กœ ๋ณด๋ฉด ๋ฉ๋‹ˆ๋‹ค. ํŠนํžˆ `engine.ts`๊ฐ€ ์‹ค์ œ ์ˆ˜์ง‘ ํ”Œ๋กœ์šฐ์˜ ์ค‘์‹ฌ์ธ์ง€ ํ™•์ธํ•˜๊ณ , ์‹คํŒจ/์žฌ์‹œ๋„/์ƒํƒœ ์ €์žฅ์ด ์–ด๋””์„œ ์ฑ…์ž„์ง€๋Š”์ง€ ๋ณด๋Š” ๊ฒŒ ์ฒซ ๋ฒˆ์งธ์ž…๋‹ˆ๋‹ค.' + ].filter(Boolean).join('\n'); + } + private buildProjectKnowledgeFallbackAnswer(localPathContext: string, record?: { filePath: string; relativePath: string } | null): string { const pathMatch = localPathContext.match(/Path:\s*(.+)/); const projectPath = pathMatch?.[1]?.trim() || '์ œ๊ณต๋œ ๋กœ์ปฌ ํ”„๋กœ์ ํŠธ ๊ฒฝ๋กœ'; diff --git a/src/utils.ts b/src/utils.ts index 015a19f..4379086 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -174,6 +174,15 @@ Core behavior: - If available evidence is only general knowledge, never say the project architecture is flexible, technically stable, scalable, gateway-based, microservice-ready, separated into layers, or structurally prepared. Say the technical structure cannot be judged from the current information. - For questions about customer evaluation, approval likelihood, requirement fit, UX, business value, product discovery, or purchase conversion, do not over-focus on technical architecture. Treat approval likelihood as an inference unless explicit approval criteria are provided. +Astra stance: +- You are not a template renderer. You are a local operating partner with taste, memory, and engineering judgment. +- Your default posture is calm but opinionated: say what you would actually do, what you would postpone, and what you would refuse to overbuild. +- Preserve the user's momentum. When the user is sorting out an idea, turn fog into 1-2 crisp choices instead of giving a balanced essay. +- Speak like a capable collaborator sitting next to the user: warm, direct, occasionally wry, never theatrical. +- Let your point of view show through concrete preferences: simple local files before databases, reliable recovery before new features, evidence before claims, working loops before grand architecture. +- If the user's framing is off, gently correct the frame before answering inside it. +- If the answer starts sounding like a checklist, collapse it into a verdict, a reason, a risk, and the next move. + Available action tags: [ACTION 1: CREATE NEW FILES] diff --git a/tests/localPathPreflight.test.ts b/tests/localPathPreflight.test.ts index 1db5d2c..b629ba5 100644 --- a/tests/localPathPreflight.test.ts +++ b/tests/localPathPreflight.test.ts @@ -70,6 +70,117 @@ describe('local project path preflight', () => { expect(agent.shouldPreflightLocalProjectPath(prompt)).toBe(true); }); + it('does not mistake a knowledge collection tool review for project knowledge creation', () => { + const context: any = { + globalStorageUri: { fsPath: path.join(root, '.storage') }, + workspaceState: stateStore(), + globalState: stateStore() + }; + const agent = new AgentExecutor(context) as any; + const prompt = '/Volumes/Data/project/Antigravity/Datacollector_MAC ์ด ํ”„๋กœ์ ํŠธ๋Š” ์žฌ๊ฐ€ ์ง€์‹์„ ์ˆ˜์ง‘ํ•˜๋Š”๋ฐ ์‚ฌ์šฉํ•˜๋Š” ํ”„๋กœ๊ทธ๋žจ์ด์•ผ. ์ด ํ”„๋กœ๊ทธ๋žจ์„ ๋„ˆ๊ฐ€ ์ฝ”๋“œ๋ฆฌ๋ทฐ๋ฅผ ํ•˜๊ณ  ์ด ํ”„๋กœ๊ทธ๋žจ์— ๋Œ€ํ•œ ๋„ˆ์˜ ํ‰๊ฐ€๋ฅผ ๋“ฃ๊ณ  ์‹ถ์–ด. ์žฅ์ ๊ณผ ๋‹จ์ , ์•ž์œผ๋กœ์˜ ํ™•์žฅ์„ฑ์€ ์–ด๋–ป๊ฒŒ ์žก์•„์•ผํ• ์ง€.'; + + expect(agent.shouldPreflightLocalProjectPath(prompt)).toBe(true); + expect(agent.isProjectReviewEvaluationRequest(prompt)).toBe(true); + expect(agent.isProjectKnowledgeCreationRequest(prompt)).toBe(false); + }); + + it('classifies local project intent by requested task instead of subject words', () => { + const context: any = { + globalStorageUri: { fsPath: path.join(root, '.storage') }, + workspaceState: stateStore(), + globalState: stateStore() + }; + const agent = new AgentExecutor(context) as any; + const projectPath = '/Volumes/Data/project/Antigravity/Datacollector_MAC'; + + expect(agent.classifyLocalProjectIntent(`${projectPath} ์ง€์‹ ์ˆ˜์ง‘์šฉ ์•ฑ์ธ๋ฐ ํ•œ๋ฒˆ ๋ด์ค˜. ์•ž์œผ๋กœ ํ™•์žฅ์„ฑ์€?`)).toBe('review-evaluation'); + expect(agent.classifyLocalProjectIntent(`${projectPath} ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ํ”„๋กœ๊ทธ๋žจ์ด์•ผ. ์žฅ๋‹จ์ ๊ณผ ๋ฆฌ์Šคํฌ ํ‰๊ฐ€ํ•ด์ค˜.`)).toBe('review-evaluation'); + expect(agent.classifyLocalProjectIntent(`${projectPath} ์ด ํ”„๋กœ์ ํŠธ์— ๋Œ€ํ•œ ์ง€์‹์„ ๋งŒ๋“ค์–ด์ค˜.`)).toBe('knowledge-creation'); + expect(agent.classifyLocalProjectIntent(`${projectPath} ์ด ํ”„๋กœ์ ํŠธ README์™€ ์‚ฌ์šฉ ๊ฐ€์ด๋“œ๋ฅผ ๋ฌธ์„œ๋กœ ์ •๋ฆฌํ•ด์ค˜.`)).toBe('documentation'); + expect(agent.classifyLocalProjectIntent(`${projectPath} ์ด ํ”„๋กœ์ ํŠธ์˜ ์•„ํ‚คํ…์ฒ˜ ๋ฐฉํ–ฅ์€ ์–ด๋–ป๊ฒŒ ์ƒ๊ฐํ•ด?`)).toBe('thinking'); + }); + + it('adds deep review lenses for local project review intent', () => { + const context: any = { + globalStorageUri: { fsPath: path.join(root, '.storage') }, + workspaceState: stateStore(), + globalState: stateStore() + }; + const agent = new AgentExecutor(context) as any; + const guidance = agent.buildLocalProjectIntentGuidance('review-evaluation'); + + expect(guidance).toContain('purpose fit'); + expect(guidance).toContain('architecture shape'); + expect(guidance).toContain('data/control flow'); + expect(guidance).toContain('failure recovery'); + expect(guidance).toContain('operability/observability'); + expect(guidance).toContain('extensibility'); + }); + + it('adds an Astra stance layer for opinionated project collaboration', () => { + const context: any = { + globalStorageUri: { fsPath: path.join(root, '.storage') }, + workspaceState: stateStore(), + globalState: stateStore() + }; + const agent = new AgentExecutor(context) as any; + const prompt = '/Volumes/Data/project/Antigravity/Datacollector_MAC ์ด ์ง€์‹ ์ˆ˜์ง‘ ์•ฑ์„ ํ‰๊ฐ€ํ•ด์ค˜. ์žฅ์  ๋‹จ์  ํ™•์žฅ์„ฑ๋„ ๋ณด๊ณ  ์‹ถ์–ด.'; + const localPathContext = [ + 'Path: /Volumes/Data/project/Antigravity/Datacollector_MAC', + 'Access: succeeded' + ].join('\n'); + + const stance = agent.buildAstraStanceContext(prompt, localPathContext); + + expect(stance).toContain('[ASTRA STANCE LAYER]'); + expect(stance).toContain('not a template'); + expect(stance).toContain('State the real bet'); + expect(stance).toContain('Review stance'); + expect(stance).toContain('would rely on this project today'); + expect(stance).toContain('Local project intent for tone: review-evaluation'); + }); + + it('quality-gates template-like review answers into an Astra verdict', () => { + const context: any = { + globalStorageUri: { fsPath: path.join(root, '.storage') }, + workspaceState: stateStore(), + globalState: stateStore() + }; + const agent = new AgentExecutor(context) as any; + const prompt = '/Volumes/Data/project/Antigravity/Datacollector_MAC ์ด ์ง€์‹ ์ˆ˜์ง‘ ์•ฑ์„ ํ‰๊ฐ€ํ•ด์ค˜. ์žฅ์  ๋‹จ์  ํ™•์žฅ์„ฑ๋„ ๋ณด๊ณ  ์‹ถ์–ด.'; + const localPathContext = [ + 'Path: /Volumes/Data/project/Antigravity/Datacollector_MAC', + 'Access: succeeded' + ].join('\n'); + const templateAnswer = [ + '## ๊ฐ„๋‹จ ์š”์•ฝ', + '์ข‹์€ ํ”„๋กœ์ ํŠธ์ž…๋‹ˆ๋‹ค.', + '## ์š”์ฒญ ์š”์•ฝ', + 'ํ”„๋กœ์ ํŠธ ํ‰๊ฐ€ ์š”์ฒญ์ž…๋‹ˆ๋‹ค.', + '## ์ถ”๋ก ๋œ ์‚ฌ์šฉ์ž ์˜๋„', + '๋ฐฉํ–ฅ์„ฑ์„ ์•Œ๊ณ  ์‹ถ์–ดํ•ฉ๋‹ˆ๋‹ค.' + ].join('\n'); + + const gated = agent.applyAstraQualityGate(templateAnswer, prompt, localPathContext); + + expect(gated).toContain('## Astra ํŒ๋‹จ'); + expect(gated).toContain('## ๋‚ด๊ฐ€ ๋ณด๋Š” ์œ„ํ—˜'); + expect(gated).toContain('## ๋‹ค์Œ ํ•œ ์ˆ˜'); + expect(gated).toContain('์‹ค์ œ๋กœ ์˜์กดํ•ด๋„ ๋˜๋Š” ๋„๊ตฌ'); + }); + + it('does not quality-gate tiny non-project replies', () => { + const context: any = { + globalStorageUri: { fsPath: path.join(root, '.storage') }, + workspaceState: stateStore(), + globalState: stateStore() + }; + const agent = new AgentExecutor(context) as any; + const answer = '์ข‹์•„์š”. ๋ฐ”๋กœ ์ง„ํ–‰ํ• ๊ฒŒ์š”.'; + + expect(agent.applyAstraQualityGate(answer, '์ข‹์•„', '')).toBe(answer); + }); + it('treats architecture opinion requests with local paths as inspectable work', () => { const context: any = { globalStorageUri: { fsPath: path.join(root, '.storage') }, @@ -164,6 +275,56 @@ describe('local project path preflight', () => { expect(fallback).not.toContain('์–ด๋–ค ๊ธฐ๋Šฅ ์˜์—ญ์„ ๊ฐ€์žฅ ๋จผ์ €'); }); + it('replaces misrouted project-knowledge answers for code review requests', () => { + const context: any = { + globalStorageUri: { fsPath: path.join(root, '.storage') }, + workspaceState: stateStore(), + globalState: stateStore() + }; + const agent = new AgentExecutor(context) as any; + const localPathContext = [ + 'Path: /Volumes/Data/project/Antigravity/Datacollector_MAC', + 'Access: succeeded', + 'Type: directory', + 'Scanned tree:', + 'src/App.tsx', + 'src/lib/engine.ts', + 'src/lib/api.ts', + 'Priority file previews:', + 'File: src/lib/engine.ts', + 'export async function runMission() {}', + 'File: src/lib/api.ts', + 'export async function fetchSources() {}' + ].join('\n'); + const wrongAnswer = '## ๊ธฐ๋ณธ ์ง€์‹ ์ƒ์„ฑ ๋ฐฉํ–ฅ\n## ๋ฐ”๋กœ ๋งŒ๋“ค ์ง€์‹ ์ดˆ์•ˆ\n# Datacollector_MAC Project Knowledge Overview'; + + expect(agent.isMisroutedProjectKnowledgeAnswer(wrongAnswer)).toBe(true); + const fixed = agent.buildProjectReviewFallbackAnswer(localPathContext); + expect(fixed).toContain('์ฝ”๋“œ๋ฆฌ๋ทฐ์™€ ์ œํ’ˆ ํ‰๊ฐ€ ์š”์ฒญ'); + expect(fixed).toContain('## ์ฝ”๋“œ๋ฆฌ๋ทฐ ๊ด€์  ํ‰๊ฐ€'); + expect(fixed).toContain('๋ชฉ์  ์ ํ•ฉ์„ฑ'); + expect(fixed).toContain('๋ฐ์ดํ„ฐ/์ œ์–ด ํ๋ฆ„'); + expect(fixed).toContain('์‹คํŒจ ๋ณต๊ตฌ'); + expect(fixed).toContain('์šด์˜์„ฑ/๊ด€์ธก์„ฑ'); + expect(fixed).toContain('## ํ™•์žฅ์„ฑ ๋ฐฉํ–ฅ'); + expect(fixed).not.toContain('๋ฐ”๋กœ ๋งŒ๋“ค ์ง€์‹ ์ดˆ์•ˆ'); + }); + + it('detects shallow project review answers', () => { + const context: any = { + globalStorageUri: { fsPath: path.join(root, '.storage') }, + workspaceState: stateStore(), + globalState: stateStore() + }; + const agent = new AgentExecutor(context) as any; + + expect(agent.isShallowProjectReviewAnswer('์ข‹์€ ํ”„๋กœ์ ํŠธ์ž…๋‹ˆ๋‹ค. ์žฅ์ ์€ ๊ตฌ์กฐ๊ฐ€ ์ข‹๊ณ  ๋‹จ์ ์€ ํ…Œ์ŠคํŠธ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.')).toBe(true); + expect(agent.isShallowProjectReviewAnswer([ + '## ์ฝ”๋“œ๋ฆฌ๋ทฐ ๊ด€์  ํ‰๊ฐ€', + '๋ชฉ์  ์ ํ•ฉ์„ฑ, ์•„ํ‚คํ…์ฒ˜ ๊ตฌ์กฐ, ๋ฐ์ดํ„ฐ ํ๋ฆ„, ์‹คํŒจ ๋ณต๊ตฌ, ์šด์˜ ๋กœ๊ทธ, ํ™•์žฅ์„ฑ์„ ๊ธฐ์ค€์œผ๋กœ ๋ด…๋‹ˆ๋‹ค.' + ].join('\n'))).toBe(false); + }); + it('treats no-record-created answers as incomplete project knowledge creation', () => { const context: any = { globalStorageUri: { fsPath: path.join(root, '.storage') }, diff --git a/tests/systemPrompt.test.ts b/tests/systemPrompt.test.ts index d8b3076..4f6e054 100644 --- a/tests/systemPrompt.test.ts +++ b/tests/systemPrompt.test.ts @@ -9,4 +9,13 @@ describe('base system prompt', () => { expect(prompt).toContain('Do not say "upload the source code"'); expect(prompt).toContain('For code review requests, first confirm path access'); }); + + it('gives Astra a stance instead of only response structure', () => { + const prompt = getSystemPrompt(); + + expect(prompt).toContain('Astra stance'); + expect(prompt).toContain('not a template renderer'); + expect(prompt).toContain('calm but opinionated'); + expect(prompt).toContain('If the answer starts sounding like a checklist'); + }); });