From 96e4f80edf7b70f07f9bf124f50b9d58cb3ef79f Mon Sep 17 00:00:00 2001 From: lacvet Date: Wed, 15 Apr 2026 20:58:27 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=ED=9A=8C=EB=B3=B5=20=ED=9D=90=EB=A6=84=EA=B3=BC=20=EC=9E=90?= =?UTF-8?q?=EB=8F=99=20=EC=8B=A4=ED=96=89=20=EA=B0=80=EB=93=9C=20=EC=A0=95?= =?UTF-8?q?=EB=B9=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 비 Git 작업 폴더에서 git_tool(diff)만 반복 호출하지 않도록 AgentLoop 실패 복구, 우선순위, 태스크 가이드를 code_review(file_review) 대안까지 포함하도록 조정했습니다. - CodeReviewTool diff_review가 실제 Git 저장소 루트와 Git 실행 가능 여부를 먼저 확인하고, 저장소가 아니거나 Git이 없으면 file_review 대안을 즉시 안내하도록 보강했습니다. - OpenExternalTool에 사용자 명시 요청 기반 auto-open 차단을 추가하고, Cowork 및 Code 시스템 프롬프트에도 결과물 자동 열기와 미리보기 서버 자동 실행 금지 규칙을 반영했습니다. - AgentLoopCodeQualityTests, OperationModeReadinessTests를 확장해 비 Git 리뷰 회복 경로와 암묵적 open_external 차단 회귀를 고정했습니다. - 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_review_policy_fix\\ -p:IntermediateOutputPath=obj\\verify_review_policy_fix\\ 및 dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter AgentLoopCodeQualityTests,OperationModeReadinessTests -p:OutputPath=bin\\verify_review_policy_fix_tests\\ -p:IntermediateOutputPath=obj\\verify_review_policy_fix_tests\ --- README.md | 9 ++ docs/DEVELOPMENT.md | 7 ++ .../Services/AgentLoopCodeQualityTests.cs | 18 +++- .../Services/OperationModeReadinessTests.cs | 44 +++++++++- .../Services/Agent/AgentLoopService.cs | 84 ++++++++++++++----- .../Services/Agent/CodeReviewTool.cs | 59 ++++++++++++- src/AxCopilot/Services/Agent/IAgentTool.cs | 3 + .../Services/Agent/OpenExternalTool.cs | 25 +++++- .../Services/Agent/TaskTypePolicy.cs | 8 +- .../Views/ChatWindow.SystemPromptBuilder.cs | 2 + 10 files changed, 231 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index deb1c80..b5f3506 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,14 @@ # AX Commander +- 업데이트: 2026-04-15 20:55 (KST) +- Code 탭 리뷰 로그 기준으로 비 Git 작업 폴더 회복 흐름을 보강했습니다. `src/AxCopilot/Services/Agent/AgentLoopService.cs`, `src/AxCopilot/Services/Agent/TaskTypePolicy.cs`가 `git_tool(diff)`만 고집하지 않고 `code_review(file_review)` 또는 직접 파일 검토 경로를 함께 안내하도록 바뀌어, `현재 작업 폴더는 Git 저장소가 아닙니다`와 `Git을 찾을 수 없습니다` 이후 같은 Git 계열 도구를 반복 호출하던 흐름을 줄였습니다. +- `src/AxCopilot/Services/Agent/CodeReviewTool.cs`는 `diff_review` 전에 실제 Git 저장소 루트를 확인하고, 저장소가 아니거나 Git 실행이 불가능하면 바로 `file_review` 대안을 반환하도록 보강했습니다. Git 탐지도 `where.exe` 기반으로 맞춰 `git_tool`과 `code_review` 사이 탐지 불일치를 줄였습니다. +- `src/AxCopilot/Services/Agent/OpenExternalTool.cs`, `src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs`, `src/AxCopilot/Services/Agent/IAgentTool.cs`에는 자동 실행 가드를 추가했습니다. 사용자가 명시적으로 열기/실행/미리보기를 요청하지 않은 경우 `open_external`은 차단되고, Cowork/Code 시스템 프롬프트도 결과물 생성 뒤 브라우저 실행·미리보기 서버 시작을 자동으로 하지 말라고 명시합니다. +- 테스트: `src/AxCopilot.Tests/Services/AgentLoopCodeQualityTests.cs`, `src/AxCopilot.Tests/Services/OperationModeReadinessTests.cs` +- 검증 + - `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_review_policy_fix\\ -p:IntermediateOutputPath=obj\\verify_review_policy_fix\\` 경고 0 / 오류 0 + - `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopCodeQualityTests|OperationModeReadinessTests" -p:OutputPath=bin\\verify_review_policy_fix_tests\\ -p:IntermediateOutputPath=obj\\verify_review_policy_fix_tests\\` 통과 133 + - 업데이트: 2026-04-15 20:41 (KST) - AX Agent 좌측 대화 목록을 Codex 스타일에 가깝게 1줄형 카드로 단순화했습니다. `src/AxCopilot/Views/ChatWindow.xaml`의 `ConversationItemTemplate`는 제목과 시간을 한 줄에 배치하고, 선택된 항목은 전체 배경과 테두리가 현재 테마(`HintBackground`, `AccentColor`)를 따라 강조되도록 바뀌었습니다. - `src/AxCopilot/Views/ChatWindow.ConversationListPresentation.cs`, `src/AxCopilot/ViewModels/ChatWindowViewModel.cs`, `src/AxCopilot/Views/ChatWindow.xaml.cs`를 통해 실행 중인 대화는 앞쪽 링 표시로, 백그라운드 완료 후 아직 열어보지 않은 대화는 테마색 완료 점으로 구분하도록 정리했습니다. 완료 점은 해당 대화를 열면 바로 사라집니다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 264b9ab..6a39279 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -1556,3 +1556,10 @@ UI ?遺우쁽????域뱀뮆???귐뗫솯?醫딆춦 ???袁る퓮 ?臾믩씜 ??疫 - 검증: - `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_conversation_list_refresh\\ -p:IntermediateOutputPath=obj\\verify_conversation_list_refresh\\` 경고 0 / 오류 0 - `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ChatWindowSlashPolicyTests" -p:OutputPath=bin\\verify_conversation_list_refresh_tests\\ -p:IntermediateOutputPath=obj\\verify_conversation_list_refresh_tests\\` 통과 59 +업데이트: 2026-04-15 20:55 (KST) +- Code 탭 리뷰 로그를 기준으로 비 Git 작업 폴더 회복 흐름을 조정했습니다. `src/AxCopilot/Services/Agent/AgentLoopService.cs`와 `src/AxCopilot/Services/Agent/TaskTypePolicy.cs`가 `git_tool(diff)`만 고집하지 않고 `code_review(file_review)` 또는 직접 파일 검토 경로를 함께 제시하도록 바뀌어, 저장소가 아닌 폴더에서 리뷰/검증 작업이 같은 Git 계열 도구를 반복 호출하던 회귀를 줄였습니다. +- `src/AxCopilot/Services/Agent/CodeReviewTool.cs`는 `diff_review` 전에 실제 Git 저장소 루트를 확인하고, 저장소가 아니거나 Git 실행이 불가능하면 즉시 `file_review` 대안을 반환합니다. Git 탐지도 `where.exe` 기반으로 보강해 `git_tool`과 `code_review`의 Git 탐지 결과가 달라지던 문제를 함께 줄였습니다. +- `src/AxCopilot/Services/Agent/OpenExternalTool.cs`, `src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs`, `src/AxCopilot/Services/Agent/IAgentTool.cs`에는 자동 열기 실행 가드를 추가했습니다. 사용자가 명시적으로 열기/실행/미리보기를 요청하지 않은 경우 `open_external`은 차단되고, Cowork/Code 시스템 프롬프트도 결과물 생성 뒤 브라우저 실행이나 미리보기 서버 시작을 자동으로 하지 않도록 고정했습니다. +- 테스트: `src/AxCopilot.Tests/Services/AgentLoopCodeQualityTests.cs`, `src/AxCopilot.Tests/Services/OperationModeReadinessTests.cs` +- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_review_policy_fix\\ -p:IntermediateOutputPath=obj\\verify_review_policy_fix\\` 경고 0 / 오류 0 +- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentLoopCodeQualityTests|OperationModeReadinessTests" -p:OutputPath=bin\\verify_review_policy_fix_tests\\ -p:IntermediateOutputPath=obj\\verify_review_policy_fix_tests\\` 통과 133 diff --git a/src/AxCopilot.Tests/Services/AgentLoopCodeQualityTests.cs b/src/AxCopilot.Tests/Services/AgentLoopCodeQualityTests.cs index c46f868..3a33406 100644 --- a/src/AxCopilot.Tests/Services/AgentLoopCodeQualityTests.cs +++ b/src/AxCopilot.Tests/Services/AgentLoopCodeQualityTests.cs @@ -219,10 +219,26 @@ public class AgentLoopCodeQualityTests "bugfix"); message.Should().Contain("Fallback sequence"); - message.Should().Contain("file_read -> grep/glob -> git_tool(diff)"); + message.Should().Contain("git_tool(diff)"); message.Should().Contain("repro/root-cause"); } + [Fact] + public void BuildFailureReflectionMessage_UsesFileReviewFallbackWhenWorkspaceIsNotGitRepository() + { + var prompt = InvokePrivateStatic( + "BuildFailureReflectionMessage", + "git_tool", + ToolResult.Fail("현재 작업 폴더는 Git 저장소가 아닙니다."), + 1, + 3, + "review"); + + prompt.Should().Contain("저장소 컨텍스트"); + prompt.Should().Contain("code_review(file_review)"); + prompt.Should().NotContain("git_tool(diff) -> targeted tool retry"); + } + [Fact] public void BuildFailureNextToolPriorityPrompt_IncludesOrderedPriority() { diff --git a/src/AxCopilot.Tests/Services/OperationModeReadinessTests.cs b/src/AxCopilot.Tests/Services/OperationModeReadinessTests.cs index e610b20..de3c1d0 100644 --- a/src/AxCopilot.Tests/Services/OperationModeReadinessTests.cs +++ b/src/AxCopilot.Tests/Services/OperationModeReadinessTests.cs @@ -1,4 +1,5 @@ -using System.Text.Json; +using System.IO; +using System.Text.Json; using AxCopilot.Handlers; using AxCopilot.Services; using AxCopilot.Services.Agent; @@ -104,4 +105,45 @@ public class OperationModeReadinessTests result.Success.Should().BeFalse(); result.Output.Should().Contain("사내모드"); } + + [Fact] + public async Task OpenExternal_RejectsImplicitAutoOpenWithoutExplicitUserIntent() + { + var tool = new OpenExternalTool(); + using var doc = JsonDocument.Parse("""{"path":"report.html"}"""); + var context = new AgentContext + { + OperationMode = OperationModePolicy.ExternalMode, + Permission = "Auto", + InitialUserQuery = "리뷰 보고서를 html로 만들어줘" + }; + + var result = await tool.ExecuteAsync(doc.RootElement, context, CancellationToken.None); + + result.Success.Should().BeFalse(); + result.Output.Should().Contain("명시적으로 요청"); + } + + [Fact] + public async Task OpenExternal_AllowsExplicitOpenIntentToProceedPastIntentGate() + { + var tool = new OpenExternalTool(); + var tempDir = Path.Combine(Path.GetTempPath(), "AxCopilotOpenIntentTests"); + Directory.CreateDirectory(tempDir); + + using var doc = JsonDocument.Parse("""{"path":"report.html"}"""); + var context = new AgentContext + { + OperationMode = OperationModePolicy.ExternalMode, + Permission = "Auto", + WorkFolder = tempDir, + InitialUserQuery = "생성한 report.html 열어줘" + }; + + var result = await tool.ExecuteAsync(doc.RootElement, context, CancellationToken.None); + + result.Success.Should().BeFalse(); + result.Output.Should().Contain("경로를 찾을 수 없습니다"); + result.Output.Should().NotContain("명시적으로 요청"); + } } diff --git a/src/AxCopilot/Services/Agent/AgentLoopService.cs b/src/AxCopilot/Services/Agent/AgentLoopService.cs index e42d98f..eae8064 100644 --- a/src/AxCopilot/Services/Agent/AgentLoopService.cs +++ b/src/AxCopilot/Services/Agent/AgentLoopService.cs @@ -296,6 +296,7 @@ public partial class AgentLoopService string? lastModifiedCodeFilePath = null; var context = BuildContext(); + context.InitialUserQuery = userQuery; runState.WorkspaceAppearsEmpty = DetectEmptyWorkspace(context.WorkFolder); var preferredInitialToolSequence = BuildPreferredInitialToolSequence( @@ -2725,7 +2726,7 @@ public partial class AgentLoopService IReadOnlyCollection parsedPatterns) { if (parsedPatterns.Count == 0) - return "실행 지침: 같은 명령/파라미터 반복을 금지하고, 읽기 근거(file_read/grep/git_tool) 후에만 재시도하세요."; + return "실행 지침: 같은 명령/파라미터 반복을 금지하고, 읽기 근거(file_read/grep/git_tool(diff) 또는 code_review(file_review)) 후에만 재시도하세요."; var repeatedTools = parsedPatterns .Select(p => (p.ToolName ?? "").Trim().ToLowerInvariant()) @@ -2740,9 +2741,6 @@ public partial class AgentLoopService ? "실패 도구 공통 패턴" : string.Join(", ", repeatedTools); var hasBuildLoopRisk = repeatedTools.Any(t => t.Contains("build_run") || t.Contains("test_loop")); - var priority = hasBuildLoopRisk - ? "file_read -> grep/glob -> git_tool(diff) -> file_edit -> build_run/test_loop" - : "file_read -> grep/glob -> git_tool(diff) -> targeted retry"; var dominantFailureKinds = parsedPatterns .Select(p => (p.FailureKind ?? "").Trim().ToLowerInvariant()) .Where(x => !string.IsNullOrWhiteSpace(x)) @@ -2751,6 +2749,7 @@ public partial class AgentLoopService .Take(2) .Select(g => g.Key) .ToList(); + var avoidGitDiff = dominantFailureKinds.Any(kind => ShouldPreferDirectChangeReview(ParseFailureKindToken(kind))); var kindHint = dominantFailureKinds.Count == 0 ? "일반 실패" : string.Join(", ", dominantFailureKinds.Select(DescribeFailureKindToken)); @@ -2763,6 +2762,9 @@ public partial class AgentLoopService "review" => "추측이 아닌 결함 근거를 먼저 확보하세요.", _ => "실패 로그 근거를 먼저 확보하세요." }; + var priority = hasBuildLoopRisk + ? BuildChangeReviewPrioritySequence(avoidGitDiff, includeEditAndVerify: true) + : BuildChangeReviewPrioritySequence(avoidGitDiff, includeEditAndVerify: false); return "실행 지침:\n" + $"- 반복 경향 도구: {toolHint}\n" + @@ -2780,11 +2782,47 @@ public partial class AgentLoopService "path" => "경로", "command" => "명령/파라미터", "dependency" => "의존성/환경", + "repository" => "저장소 컨텍스트", "timeout" => "타임아웃", _ => "일반", }; } + private static FailureRecoveryKind ParseFailureKindToken(string token) + { + return token switch + { + "permission" => FailureRecoveryKind.Permission, + "path" => FailureRecoveryKind.Path, + "command" => FailureRecoveryKind.Command, + "dependency" => FailureRecoveryKind.Dependency, + "repository" => FailureRecoveryKind.Repository, + "timeout" => FailureRecoveryKind.Timeout, + _ => FailureRecoveryKind.Unknown, + }; + } + + private static bool ShouldPreferDirectChangeReview(FailureRecoveryKind kind) + => kind is FailureRecoveryKind.Dependency or FailureRecoveryKind.Repository; + + private static string BuildChangeReviewPrioritySequence(bool preferDirectReview, bool includeEditAndVerify) + { + var reviewStep = preferDirectReview + ? "code_review(file_review)/직접 파일 검토" + : "git_tool(diff)"; + + return includeEditAndVerify + ? $"file_read -> grep/glob -> {reviewStep} -> file_edit -> build_run/test_loop" + : $"file_read -> grep/glob -> {reviewStep} -> targeted retry"; + } + + private static string BuildChangeReviewInstruction(FailureRecoveryKind kind) + { + return ShouldPreferDirectChangeReview(kind) + ? "Use code_review(file_review) or re-read the changed files directly to confirm what actually changed." + : "Review the current diff with git_tool(diff) or an equivalent review tool to confirm what actually changed."; + } + private static string BuildCodeQualityFollowUpPrompt(string toolName, ToolResult result, bool highImpact, bool hasBaselineBuildOrTest, TaskTypePolicy taskPolicy) { var fileRef = string.IsNullOrWhiteSpace(result.FilePath) @@ -2818,7 +2856,7 @@ public partial class AgentLoopService taskTypeLine + "1. file_read로 방금 수정한 파일을 다시 읽어 실제 반영 상태를 확인합니다.\n" + "2. grep 또는 glob으로 영향받는 호출부/참조 지점을 다시 확인합니다.\n" + - "3. git_tool diff 또는 동등한 검토 도구로 변경 범위를 확인합니다.\n" + + "3. git_tool(diff) 또는 code_review(file_review) 같은 검토 도구로 변경 범위를 확인합니다.\n" + extraSteps; } @@ -2834,7 +2872,7 @@ public partial class AgentLoopService { var failureKind = ClassifyFailureRecoveryKind(toolName, result.Output); var failureHint = BuildFailureTypeRecoveryHint(failureKind, toolName); - var fallbackSequence = BuildFallbackToolSequenceHint(toolName, taskPolicy); + var fallbackSequence = BuildFallbackToolSequenceHint(toolName, taskPolicy, failureKind); if (toolName is "build_run" or "test_loop") { return @@ -2842,7 +2880,7 @@ public partial class AgentLoopService "Before retrying, do all of the following:\n" + "1. Re-read the files you changed most recently.\n" + "2. Inspect impacted callers/references with grep or glob.\n" + - "3. Review the current diff to confirm what actually changed.\n" + + $"3. {BuildChangeReviewInstruction(failureKind)}\n" + "4. Only then fix the root cause and re-run build/test.\n" + failureHint + fallbackSequence + @@ -2877,16 +2915,17 @@ public partial class AgentLoopService maxRetry, TaskTypePolicy.FromTaskType(taskType)); - private static string BuildFallbackToolSequenceHint(string toolName, TaskTypePolicy taskPolicy) + private static string BuildFallbackToolSequenceHint(string toolName, TaskTypePolicy taskPolicy, FailureRecoveryKind failureKind) { + var includeEditAndVerify = toolName is "build_run" or "test_loop" or "file_edit" or "file_write"; var sequence = toolName switch { "build_run" or "test_loop" => - "Fallback sequence: file_read -> grep/glob -> git_tool(diff) -> file_edit -> build_run/test_loop.\n", + $"Fallback sequence: {BuildChangeReviewPrioritySequence(ShouldPreferDirectChangeReview(failureKind), includeEditAndVerify: true)}.\n", "file_edit" or "file_write" => - "Fallback sequence: file_read -> grep/glob -> git_tool(diff) -> file_edit -> build_run/test_loop.\n", + $"Fallback sequence: {BuildChangeReviewPrioritySequence(ShouldPreferDirectChangeReview(failureKind), includeEditAndVerify: true)}.\n", _ => - "Fallback sequence: file_read -> grep/glob -> git_tool(diff) -> targeted tool retry.\n", + $"Fallback sequence: {BuildChangeReviewPrioritySequence(ShouldPreferDirectChangeReview(failureKind), includeEditAndVerify: includeEditAndVerify)}.\n", }; var taskHint = taskPolicy.TaskType switch @@ -2965,7 +3004,7 @@ public partial class AgentLoopService return "[System:FailureInvestigation] build/test가 실패했습니다. 다음 순서로 원인을 찾으세요.\n" + fileLine + "2. grep/glob으로 영향받는 호출부와 참조 지점을 확인합니다.\n" + - "3. git diff 또는 동등한 방법으로 실제 변경 범위를 검토합니다.\n" + + $"3. {BuildChangeReviewInstruction(ClassifyFailureRecoveryKind(toolName, toolOutput))}\n" + failureHint + taskTypeLine + highImpactLine; @@ -2981,6 +3020,7 @@ public partial class AgentLoopService Path, Command, Dependency, + Repository, Timeout, } @@ -2989,11 +3029,13 @@ public partial class AgentLoopService var text = ((output ?? "") + "\n" + toolName).ToLowerInvariant(); if (ContainsAny(text, "permission", "denied", "forbidden", "unauthorized", "access is denied", "권한", "차단", "승인")) return FailureRecoveryKind.Permission; + if (ContainsAny(text, "git 저장소가 아닙니다", "not a git repository", "work tree", "git repository")) + return FailureRecoveryKind.Repository; if (ContainsAny(text, "not found", "no such file", "cannot find", "directory not found", "path", "경로", "파일을 찾을", "존재하지")) return FailureRecoveryKind.Path; if (ContainsAny(text, "command not found", "not recognized", "unknown option", "invalid argument", "syntax", "잘못된 옵션", "명령", "인식할 수")) return FailureRecoveryKind.Command; - if (ContainsAny(text, "module not found", "package not found", "sdk", "runtime", "missing dependency", "의존성", "설치되지")) + if (ContainsAny(text, "module not found", "package not found", "sdk", "runtime", "missing dependency", "의존성", "설치되지", "git을 찾을 수 없습니다")) return FailureRecoveryKind.Dependency; if (ContainsAny(text, "timeout", "timed out", "time out", "시간 초과")) return FailureRecoveryKind.Timeout; @@ -3011,7 +3053,9 @@ public partial class AgentLoopService FailureRecoveryKind.Command => $"원인 분류: 명령/파라미터 이슈. '{toolName}' 입력 파라미터와 옵션 이름을 재검토하고, 최소 파라미터로 먼저 검증하세요.\n", FailureRecoveryKind.Dependency => - "원인 분류: 의존성/환경 이슈. 설치/버전 누락 여부를 확인하고, 대체 가능한 도구 흐름(file_read/grep/git_tool)으로 우회하세요.\n", + "원인 분류: 의존성/환경 이슈. 설치/버전 누락 여부를 확인하고, Git/리뷰 도구가 불안정하면 file_read/grep/code_review(file_review) 흐름으로 우회하세요.\n", + FailureRecoveryKind.Repository => + "원인 분류: 저장소 컨텍스트 이슈. 현재 폴더가 Git 저장소가 아니면 git diff 대신 file_read/grep/code_review(file_review)로 파일별 검토를 진행하세요.\n", FailureRecoveryKind.Timeout => "원인 분류: 타임아웃. 대상 범위를 축소(파일/테스트 필터)해 더 짧은 실행 단위로 분해한 뒤 재시도하세요.\n", _ => @@ -5262,7 +5306,7 @@ public partial class AgentLoopService "1. 직전 실패 로그에서 핵심 오류 1~2줄을 먼저 인용하세요.\n" + "2. 다음 재시도는 이전과 최소 1개 축을 다르게 실행하세요 (대상 프로젝트/테스트 필터/설정/작업 디렉터리).\n" + "3. 코드 변경 없이 동일 명령 재실행은 금지합니다.\n" + - "4. 재시도 전 file_read/grep/git_tool(diff) 중 최소 2개 근거를 확보하세요.\n"; + "4. 재시도 전 file_read/grep/(git_tool(diff) 또는 code_review(file_review)) 중 최소 2개 근거를 확보하세요.\n"; } private static string BuildFailureNextToolPriorityPrompt( @@ -5302,15 +5346,17 @@ public partial class AgentLoopService FailureRecoveryKind.Command => "도구 스키마 재확인 -> 최소 파라미터 호출 -> 옵션 확장 -> 필요 시 대체 도구", FailureRecoveryKind.Dependency => - "dev_env_detect/process 환경 확인 -> file_read/grep/git_tool 근거 확보 -> 가능한 범위 우회", + "dev_env_detect/process 환경 확인 -> file_read/grep -> code_review(file_review) 또는 직접 검토 -> 가능한 범위 우회", + FailureRecoveryKind.Repository => + "file_read -> grep/glob -> code_review(file_review) 또는 직접 파일 검토 -> 필요 시 file_edit/build_run/test_loop", FailureRecoveryKind.Timeout => "대상 범위 축소 -> 필터 적용 실행 -> 분할 실행 -> 최종 전체 실행", _ => failedToolName switch { - "build_run" or "test_loop" => "file_read -> grep/glob -> git_tool(diff) -> file_edit -> build_run/test_loop", - "file_edit" or "file_write" => "file_read -> grep/glob -> git_tool(diff) -> file_edit -> build_run/test_loop", + "build_run" or "test_loop" => BuildChangeReviewPrioritySequence(preferDirectReview: false, includeEditAndVerify: true), + "file_edit" or "file_write" => BuildChangeReviewPrioritySequence(preferDirectReview: false, includeEditAndVerify: true), "grep" or "glob" => "glob/grep 재구성 -> file_read -> folder_map(필요 시만)", - _ => "file_read -> grep/glob -> git_tool(diff) -> targeted tool retry" + _ => BuildChangeReviewPrioritySequence(preferDirectReview: false, includeEditAndVerify: false) } }; } diff --git a/src/AxCopilot/Services/Agent/CodeReviewTool.cs b/src/AxCopilot/Services/Agent/CodeReviewTool.cs index f12ebe0..ac9d88b 100644 --- a/src/AxCopilot/Services/Agent/CodeReviewTool.cs +++ b/src/AxCopilot/Services/Agent/CodeReviewTool.cs @@ -73,9 +73,21 @@ public class CodeReviewTool : IAgentTool private async Task DiffReviewAsync(AgentContext ctx, string target, string focus, CancellationToken ct) { + if (!TryResolveGitRepositoryRoot(ctx.WorkFolder, out var gitRoot)) + { + return ToolResult.Fail( + "현재 작업 폴더는 Git 저장소가 아닙니다. " + + "diff_review 대신 code_review(file_review)로 핵심 파일을 직접 검토하세요."); + } + var diffArgs = string.IsNullOrEmpty(target) ? "diff" : $"diff {target}"; - var diffResult = await RunGitAsync(ctx.WorkFolder, diffArgs, ct); - if (diffResult == null) return ToolResult.Fail("Git을 찾을 수 없습니다."); + var diffResult = await RunGitAsync(gitRoot, diffArgs, ct); + if (diffResult == null) + { + return ToolResult.Fail( + "Git을 찾을 수 없거나 실행할 수 없습니다. " + + "Git diff를 쓸 수 없으면 code_review(file_review)로 파일별 리뷰를 진행하세요."); + } if (string.IsNullOrWhiteSpace(diffResult)) return ToolResult.Ok("변경사항이 없습니다. (clean working tree)"); @@ -368,6 +380,25 @@ public class CodeReviewTool : IAgentTool private static string? FindGit() { + try + { + var psi = new ProcessStartInfo("where.exe", "git") + { + RedirectStandardOutput = true, + UseShellExecute = false, + CreateNoWindow = true, + }; + using var proc = Process.Start(psi); + if (proc == null) return null; + var output = proc.StandardOutput.ReadToEnd().Trim(); + proc.WaitForExit(3000); + if (!string.IsNullOrWhiteSpace(output)) + return output.Split('\n')[0].Trim(); + } + catch + { + } + var paths = new[] { "git", @"C:\Program Files\Git\bin\git.exe", @"C:\Program Files (x86)\Git\bin\git.exe" }; foreach (var p in paths) { @@ -384,6 +415,30 @@ public class CodeReviewTool : IAgentTool return null; } + private static bool TryResolveGitRepositoryRoot(string workDir, out string gitRoot) + { + gitRoot = ""; + if (string.IsNullOrWhiteSpace(workDir) || !Directory.Exists(workDir)) + return false; + + var checkDir = workDir; + while (!string.IsNullOrWhiteSpace(checkDir)) + { + if (Directory.Exists(Path.Combine(checkDir, ".git"))) + { + gitRoot = checkDir; + return true; + } + + var parent = Directory.GetParent(checkDir)?.FullName; + if (string.IsNullOrWhiteSpace(parent) || string.Equals(parent, checkDir, StringComparison.OrdinalIgnoreCase)) + break; + checkDir = parent; + } + + return false; + } + // ─── Diff 파서 ────────────────────────────────────────────────────────── private static List ParseDiffFiles(string diff) diff --git a/src/AxCopilot/Services/Agent/IAgentTool.cs b/src/AxCopilot/Services/Agent/IAgentTool.cs index 2480c80..fcf6546 100644 --- a/src/AxCopilot/Services/Agent/IAgentTool.cs +++ b/src/AxCopilot/Services/Agent/IAgentTool.cs @@ -143,6 +143,9 @@ public class AgentContext /// 현재 활성 탭. "Chat" | "Cowork" | "Code". public string ActiveTab { get; set; } = "Chat"; + /// 현재 실행 턴의 원본 사용자 요청. 실행/열기 의도 가드에 사용합니다. + public string InitialUserQuery { get; set; } = ""; + /// 운영 모드. internal(사내) | external(사외). /// 실행 중 사용자가 설정에서 모드를 바꾸면 SyncContextFromSettings를 통해 업데이트됨. public string OperationMode { get; set; } = AxCopilot.Services.OperationModePolicy.InternalMode; diff --git a/src/AxCopilot/Services/Agent/OpenExternalTool.cs b/src/AxCopilot/Services/Agent/OpenExternalTool.cs index 77bc310..48ceaa7 100644 --- a/src/AxCopilot/Services/Agent/OpenExternalTool.cs +++ b/src/AxCopilot/Services/Agent/OpenExternalTool.cs @@ -7,11 +7,18 @@ namespace AxCopilot.Services.Agent; /// 파일/URL을 시스템 기본 앱으로 여는 도구. public class OpenExternalTool : IAgentTool { + private static readonly string[] ExplicitOpenIntentKeywords = + [ + "열어", "열어줘", "열어 줘", "오픈", "띄워", "띄워줘", "보여줘", "보여 줘", + "실행", "실행해", "실행해줘", "실행해 줘", "미리보기", "브라우저", "launch", + "open", "preview", "show", "serve", "run", "execute" + ]; + public string Name => "open_external"; public string Description => "Open a file with its default application or open a URL in the default browser. " + "Also supports opening a folder in File Explorer. " + - "Use after creating documents, reports, or charts for the user to view."; + "Only use when the user explicitly asks to open, preview, or launch the result."; public ToolParameterSchema Parameters => new() { @@ -44,6 +51,13 @@ public class OpenExternalTool : IAgentTool return Task.FromResult(ToolResult.Ok($"외부 URI 열기: {rawPath}")); } + if (!HasExplicitOpenIntent(context.InitialUserQuery)) + { + return Task.FromResult(ToolResult.Fail( + "사용자가 열기/실행/미리보기를 명시적으로 요청하지 않았습니다. " + + "결과 경로만 보고하고 자동 실행하지 마세요.")); + } + // 파일/폴더 경로 var path = Path.IsPathRooted(rawPath) ? rawPath : Path.Combine(context.WorkFolder, rawPath); @@ -69,4 +83,13 @@ public class OpenExternalTool : IAgentTool return Task.FromResult(ToolResult.Fail($"열기 오류: {ex.Message}")); } } + + private static bool HasExplicitOpenIntent(string? query) + { + if (string.IsNullOrWhiteSpace(query)) + return false; + + return ExplicitOpenIntentKeywords.Any(keyword => + query.Contains(keyword, StringComparison.OrdinalIgnoreCase)); + } } diff --git a/src/AxCopilot/Services/Agent/TaskTypePolicy.cs b/src/AxCopilot/Services/Agent/TaskTypePolicy.cs index efee014..5bd9fb8 100644 --- a/src/AxCopilot/Services/Agent/TaskTypePolicy.cs +++ b/src/AxCopilot/Services/Agent/TaskTypePolicy.cs @@ -22,7 +22,7 @@ internal sealed class TaskTypePolicy TaskType = "bugfix", GuidanceMessage = "[System:TaskType] This is a bug-fix task. Prioritize reproduction evidence, root cause linkage, smallest safe fix, and regression verification. " + - "Preferred tool order: targeted file_read or grep/glob/lsp -> file_edit -> build_run/test_loop as needed -> git_tool(diff) when it helps confirm the final change.", + "Preferred tool order: targeted file_read or grep/glob/lsp -> file_edit -> build_run/test_loop as needed -> git_tool(diff) or code_review(file_review) when it helps confirm the final change.", FailurePatternFocus = "Check reproduction conditions and root-cause linkage first.", FollowUpTaskLine = "Task type: bugfix. Verify the fix is directly linked to the symptom and confirm non-regression.\n", FailureInvestigationTaskLine = "Extra check: confirm the symptom is no longer reproducible and root-cause linkage is valid.\n", @@ -33,7 +33,7 @@ internal sealed class TaskTypePolicy TaskType = "feature", GuidanceMessage = "[System:TaskType] This is a feature task. Prioritize affected interfaces/callers, data flow, validation paths, and test/documentation needs. " + - "Preferred tool order: targeted file_read or grep/glob/lsp -> file_edit/file_write -> build_run/test_loop as needed -> git_tool(diff). " + + "Preferred tool order: targeted file_read or grep/glob/lsp -> file_edit/file_write -> build_run/test_loop as needed -> git_tool(diff) or code_review(file_review). " + "Use folder_map only when the user explicitly needs folder structure or file listing.", FailurePatternFocus = "Check new behavior flow and caller linkage first.", FollowUpTaskLine = "Task type: feature. Verify behavior flow, input/output path, caller impact, and test additions.\n", @@ -45,7 +45,7 @@ internal sealed class TaskTypePolicy TaskType = "refactor", GuidanceMessage = "[System:TaskType] This is a refactor task. Prioritize behavior preservation, reference impact, diff review, and non-regression evidence. " + - "Preferred tool order: targeted file_read or grep/glob/lsp -> file_edit -> build_run/test_loop as needed -> git_tool(diff).", + "Preferred tool order: targeted file_read or grep/glob/lsp -> file_edit -> build_run/test_loop as needed -> git_tool(diff) or code_review(file_review).", FailurePatternFocus = "Check behavior preservation and impact scope first.", FollowUpTaskLine = "Task type: refactor. Prioritize behavior-preservation evidence over cosmetic cleanup.\n", FailureInvestigationTaskLine = "Extra check: validate existing call flow remains behavior-compatible.\n", @@ -57,7 +57,7 @@ internal sealed class TaskTypePolicy GuidanceMessage = "[System:TaskType] This is a review task. Prioritize concrete defects, regressions, risky assumptions, and missing tests before summaries. " + "Report findings with P0-P3 severity and file evidence, then separate Fixed vs Unfixed status. " + - "Preferred tool order: targeted file_read or grep/glob/lsp -> git_tool(diff) when available -> evidence-first findings.", + "Preferred tool order: targeted file_read or grep/glob/lsp -> git_tool(diff) when available, otherwise code_review(file_review) or direct file review -> evidence-first findings.", FailurePatternFocus = "Review focus: severity accuracy (P0-P3), file-grounded evidence, and unresolved-risk clarity.", FollowUpTaskLine = "Task type: review-follow-up. For each finding, state status as Fixed or Unfixed with verification evidence.\n", FailureInvestigationTaskLine = "Extra check: every risk must have either a concrete fix or an explicit unresolved rationale and impact.\n", diff --git a/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs b/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs index a60fbd4..10a742d 100644 --- a/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs +++ b/src/AxCopilot/Views/ChatWindow.SystemPromptBuilder.cs @@ -72,6 +72,7 @@ public partial class ChatWindow sb.AppendLine("When writing a new document, avoid repetitive same-shape sections. Tailor the structure to the purpose and use summaries, findings, comparison tables, timelines, recommendations, appendices, or action items when they improve clarity."); sb.AppendLine("Prefer concrete and useful content over filler. If a section benefits from bullets, tables, or structured comparison, use them instead of flat generic paragraphs."); sb.AppendLine("After creating files, summarize what was created and include the actual output path."); + sb.AppendLine("Do NOT call open_external, launch a browser, or start a preview/server process unless the user explicitly asks to open, preview, serve, launch, or run the result."); sb.AppendLine("IMPORTANT: In your FINAL response after ALL work is done, provide a structured completion summary in this format:"); sb.AppendLine(" - 작업 유형: (e.g., report/analysis/proposal)"); sb.AppendLine(" - 산출물 파일: full absolute path (e.g., E:\\test\\report.html)"); @@ -251,6 +252,7 @@ public partial class ChatWindow sb.AppendLine("Available tools: file_read, file_write, file_edit (supports replace_all), glob, grep (supports context_lines, case_sensitive), lsp_code_intel, folder_map, process, dev_env_detect, build_run, git_tool."); sb.AppendLine("Do not pause after partial progress. Keep executing consecutive steps until completion or a concrete blocker is reached."); sb.AppendLine("IMPORTANT: When creating documents with dates, always use today's actual date above."); + sb.AppendLine("Do NOT call open_external, launch browsers, or start preview/server commands unless the user explicitly asks to open, preview, serve, launch, or run the result."); sb.AppendLine("\n## Core Workflow"); sb.AppendLine("1. ORIENT: Pick the smallest next step that can answer the request.");