코드 리뷰 회복 흐름과 자동 실행 가드 정비

- 비 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\
This commit is contained in:
2026-04-15 20:58:27 +09:00
parent 3210440767
commit 96e4f80edf
10 changed files with 231 additions and 28 deletions

View File

@@ -1,5 +1,14 @@
# AX Commander # 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) - 업데이트: 2026-04-15 20:41 (KST)
- AX Agent 좌측 대화 목록을 Codex 스타일에 가깝게 1줄형 카드로 단순화했습니다. `src/AxCopilot/Views/ChatWindow.xaml``ConversationItemTemplate`는 제목과 시간을 한 줄에 배치하고, 선택된 항목은 전체 배경과 테두리가 현재 테마(`HintBackground`, `AccentColor`)를 따라 강조되도록 바뀌었습니다. - 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`를 통해 실행 중인 대화는 앞쪽 링 표시로, 백그라운드 완료 후 아직 열어보지 않은 대화는 테마색 완료 점으로 구분하도록 정리했습니다. 완료 점은 해당 대화를 열면 바로 사라집니다. - `src/AxCopilot/Views/ChatWindow.ConversationListPresentation.cs`, `src/AxCopilot/ViewModels/ChatWindowViewModel.cs`, `src/AxCopilot/Views/ChatWindow.xaml.cs`를 통해 실행 중인 대화는 앞쪽 링 표시로, 백그라운드 완료 후 아직 열어보지 않은 대화는 테마색 완료 점으로 구분하도록 정리했습니다. 완료 점은 해당 대화를 열면 바로 사라집니다.

View File

@@ -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 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 - `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

View File

@@ -219,10 +219,26 @@ public class AgentLoopCodeQualityTests
"bugfix"); "bugfix");
message.Should().Contain("Fallback sequence"); 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"); message.Should().Contain("repro/root-cause");
} }
[Fact]
public void BuildFailureReflectionMessage_UsesFileReviewFallbackWhenWorkspaceIsNotGitRepository()
{
var prompt = InvokePrivateStatic<string>(
"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] [Fact]
public void BuildFailureNextToolPriorityPrompt_IncludesOrderedPriority() public void BuildFailureNextToolPriorityPrompt_IncludesOrderedPriority()
{ {

View File

@@ -1,4 +1,5 @@
using System.Text.Json; using System.IO;
using System.Text.Json;
using AxCopilot.Handlers; using AxCopilot.Handlers;
using AxCopilot.Services; using AxCopilot.Services;
using AxCopilot.Services.Agent; using AxCopilot.Services.Agent;
@@ -104,4 +105,45 @@ public class OperationModeReadinessTests
result.Success.Should().BeFalse(); result.Success.Should().BeFalse();
result.Output.Should().Contain("사내모드"); 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("명시적으로 요청");
}
} }

View File

@@ -296,6 +296,7 @@ public partial class AgentLoopService
string? lastModifiedCodeFilePath = null; string? lastModifiedCodeFilePath = null;
var context = BuildContext(); var context = BuildContext();
context.InitialUserQuery = userQuery;
runState.WorkspaceAppearsEmpty = DetectEmptyWorkspace(context.WorkFolder); runState.WorkspaceAppearsEmpty = DetectEmptyWorkspace(context.WorkFolder);
var preferredInitialToolSequence = BuildPreferredInitialToolSequence( var preferredInitialToolSequence = BuildPreferredInitialToolSequence(
@@ -2725,7 +2726,7 @@ public partial class AgentLoopService
IReadOnlyCollection<ParsedFailurePattern> parsedPatterns) IReadOnlyCollection<ParsedFailurePattern> parsedPatterns)
{ {
if (parsedPatterns.Count == 0) if (parsedPatterns.Count == 0)
return "실행 지침: 같은 명령/파라미터 반복을 금지하고, 읽기 근거(file_read/grep/git_tool) 후에만 재시도하세요."; return "실행 지침: 같은 명령/파라미터 반복을 금지하고, 읽기 근거(file_read/grep/git_tool(diff) 또는 code_review(file_review)) 후에만 재시도하세요.";
var repeatedTools = parsedPatterns var repeatedTools = parsedPatterns
.Select(p => (p.ToolName ?? "").Trim().ToLowerInvariant()) .Select(p => (p.ToolName ?? "").Trim().ToLowerInvariant())
@@ -2740,9 +2741,6 @@ public partial class AgentLoopService
? "실패 도구 공통 패턴" ? "실패 도구 공통 패턴"
: string.Join(", ", repeatedTools); : string.Join(", ", repeatedTools);
var hasBuildLoopRisk = repeatedTools.Any(t => t.Contains("build_run") || t.Contains("test_loop")); 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 var dominantFailureKinds = parsedPatterns
.Select(p => (p.FailureKind ?? "").Trim().ToLowerInvariant()) .Select(p => (p.FailureKind ?? "").Trim().ToLowerInvariant())
.Where(x => !string.IsNullOrWhiteSpace(x)) .Where(x => !string.IsNullOrWhiteSpace(x))
@@ -2751,6 +2749,7 @@ public partial class AgentLoopService
.Take(2) .Take(2)
.Select(g => g.Key) .Select(g => g.Key)
.ToList(); .ToList();
var avoidGitDiff = dominantFailureKinds.Any(kind => ShouldPreferDirectChangeReview(ParseFailureKindToken(kind)));
var kindHint = dominantFailureKinds.Count == 0 var kindHint = dominantFailureKinds.Count == 0
? "일반 실패" ? "일반 실패"
: string.Join(", ", dominantFailureKinds.Select(DescribeFailureKindToken)); : string.Join(", ", dominantFailureKinds.Select(DescribeFailureKindToken));
@@ -2763,6 +2762,9 @@ public partial class AgentLoopService
"review" => "추측이 아닌 결함 근거를 먼저 확보하세요.", "review" => "추측이 아닌 결함 근거를 먼저 확보하세요.",
_ => "실패 로그 근거를 먼저 확보하세요." _ => "실패 로그 근거를 먼저 확보하세요."
}; };
var priority = hasBuildLoopRisk
? BuildChangeReviewPrioritySequence(avoidGitDiff, includeEditAndVerify: true)
: BuildChangeReviewPrioritySequence(avoidGitDiff, includeEditAndVerify: false);
return "실행 지침:\n" + return "실행 지침:\n" +
$"- 반복 경향 도구: {toolHint}\n" + $"- 반복 경향 도구: {toolHint}\n" +
@@ -2780,11 +2782,47 @@ public partial class AgentLoopService
"path" => "경로", "path" => "경로",
"command" => "명령/파라미터", "command" => "명령/파라미터",
"dependency" => "의존성/환경", "dependency" => "의존성/환경",
"repository" => "저장소 컨텍스트",
"timeout" => "타임아웃", "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) private static string BuildCodeQualityFollowUpPrompt(string toolName, ToolResult result, bool highImpact, bool hasBaselineBuildOrTest, TaskTypePolicy taskPolicy)
{ {
var fileRef = string.IsNullOrWhiteSpace(result.FilePath) var fileRef = string.IsNullOrWhiteSpace(result.FilePath)
@@ -2818,7 +2856,7 @@ public partial class AgentLoopService
taskTypeLine + taskTypeLine +
"1. file_read로 방금 수정한 파일을 다시 읽어 실제 반영 상태를 확인합니다.\n" + "1. file_read로 방금 수정한 파일을 다시 읽어 실제 반영 상태를 확인합니다.\n" +
"2. grep 또는 glob으로 영향받는 호출부/참조 지점을 다시 확인합니다.\n" + "2. grep 또는 glob으로 영향받는 호출부/참조 지점을 다시 확인합니다.\n" +
"3. git_tool diff 또는 동등한 검토 도구로 변경 범위를 확인합니다.\n" + "3. git_tool(diff) 또는 code_review(file_review) 같은 검토 도구로 변경 범위를 확인합니다.\n" +
extraSteps; extraSteps;
} }
@@ -2834,7 +2872,7 @@ public partial class AgentLoopService
{ {
var failureKind = ClassifyFailureRecoveryKind(toolName, result.Output); var failureKind = ClassifyFailureRecoveryKind(toolName, result.Output);
var failureHint = BuildFailureTypeRecoveryHint(failureKind, toolName); var failureHint = BuildFailureTypeRecoveryHint(failureKind, toolName);
var fallbackSequence = BuildFallbackToolSequenceHint(toolName, taskPolicy); var fallbackSequence = BuildFallbackToolSequenceHint(toolName, taskPolicy, failureKind);
if (toolName is "build_run" or "test_loop") if (toolName is "build_run" or "test_loop")
{ {
return return
@@ -2842,7 +2880,7 @@ public partial class AgentLoopService
"Before retrying, do all of the following:\n" + "Before retrying, do all of the following:\n" +
"1. Re-read the files you changed most recently.\n" + "1. Re-read the files you changed most recently.\n" +
"2. Inspect impacted callers/references with grep or glob.\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" + "4. Only then fix the root cause and re-run build/test.\n" +
failureHint + failureHint +
fallbackSequence + fallbackSequence +
@@ -2877,16 +2915,17 @@ public partial class AgentLoopService
maxRetry, maxRetry,
TaskTypePolicy.FromTaskType(taskType)); 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 var sequence = toolName switch
{ {
"build_run" or "test_loop" => "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" => "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 var taskHint = taskPolicy.TaskType switch
@@ -2965,7 +3004,7 @@ public partial class AgentLoopService
return "[System:FailureInvestigation] build/test가 실패했습니다. 다음 순서로 원인을 찾으세요.\n" + return "[System:FailureInvestigation] build/test가 실패했습니다. 다음 순서로 원인을 찾으세요.\n" +
fileLine + fileLine +
"2. grep/glob으로 영향받는 호출부와 참조 지점을 확인합니다.\n" + "2. grep/glob으로 영향받는 호출부와 참조 지점을 확인합니다.\n" +
"3. git diff 또는 동등한 방법으로 실제 변경 범위를 검토합니다.\n" + $"3. {BuildChangeReviewInstruction(ClassifyFailureRecoveryKind(toolName, toolOutput))}\n" +
failureHint + failureHint +
taskTypeLine + taskTypeLine +
highImpactLine; highImpactLine;
@@ -2981,6 +3020,7 @@ public partial class AgentLoopService
Path, Path,
Command, Command,
Dependency, Dependency,
Repository,
Timeout, Timeout,
} }
@@ -2989,11 +3029,13 @@ public partial class AgentLoopService
var text = ((output ?? "") + "\n" + toolName).ToLowerInvariant(); var text = ((output ?? "") + "\n" + toolName).ToLowerInvariant();
if (ContainsAny(text, "permission", "denied", "forbidden", "unauthorized", "access is denied", "권한", "차단", "승인")) if (ContainsAny(text, "permission", "denied", "forbidden", "unauthorized", "access is denied", "권한", "차단", "승인"))
return FailureRecoveryKind.Permission; 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", "경로", "파일을 찾을", "존재하지")) if (ContainsAny(text, "not found", "no such file", "cannot find", "directory not found", "path", "경로", "파일을 찾을", "존재하지"))
return FailureRecoveryKind.Path; return FailureRecoveryKind.Path;
if (ContainsAny(text, "command not found", "not recognized", "unknown option", "invalid argument", "syntax", "잘못된 옵션", "명령", "인식할 수")) if (ContainsAny(text, "command not found", "not recognized", "unknown option", "invalid argument", "syntax", "잘못된 옵션", "명령", "인식할 수"))
return FailureRecoveryKind.Command; 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; return FailureRecoveryKind.Dependency;
if (ContainsAny(text, "timeout", "timed out", "time out", "시간 초과")) if (ContainsAny(text, "timeout", "timed out", "time out", "시간 초과"))
return FailureRecoveryKind.Timeout; return FailureRecoveryKind.Timeout;
@@ -3011,7 +3053,9 @@ public partial class AgentLoopService
FailureRecoveryKind.Command => FailureRecoveryKind.Command =>
$"원인 분류: 명령/파라미터 이슈. '{toolName}' 입력 파라미터와 옵션 이름을 재검토하고, 최소 파라미터로 먼저 검증하세요.\n", $"원인 분류: 명령/파라미터 이슈. '{toolName}' 입력 파라미터와 옵션 이름을 재검토하고, 최소 파라미터로 먼저 검증하세요.\n",
FailureRecoveryKind.Dependency => 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 => FailureRecoveryKind.Timeout =>
"원인 분류: 타임아웃. 대상 범위를 축소(파일/테스트 필터)해 더 짧은 실행 단위로 분해한 뒤 재시도하세요.\n", "원인 분류: 타임아웃. 대상 범위를 축소(파일/테스트 필터)해 더 짧은 실행 단위로 분해한 뒤 재시도하세요.\n",
_ => _ =>
@@ -5262,7 +5306,7 @@ public partial class AgentLoopService
"1. 직전 실패 로그에서 핵심 오류 1~2줄을 먼저 인용하세요.\n" + "1. 직전 실패 로그에서 핵심 오류 1~2줄을 먼저 인용하세요.\n" +
"2. 다음 재시도는 이전과 최소 1개 축을 다르게 실행하세요 (대상 프로젝트/테스트 필터/설정/작업 디렉터리).\n" + "2. 다음 재시도는 이전과 최소 1개 축을 다르게 실행하세요 (대상 프로젝트/테스트 필터/설정/작업 디렉터리).\n" +
"3. 코드 변경 없이 동일 명령 재실행은 금지합니다.\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( private static string BuildFailureNextToolPriorityPrompt(
@@ -5302,15 +5346,17 @@ public partial class AgentLoopService
FailureRecoveryKind.Command => FailureRecoveryKind.Command =>
"도구 스키마 재확인 -> 최소 파라미터 호출 -> 옵션 확장 -> 필요 시 대체 도구", "도구 스키마 재확인 -> 최소 파라미터 호출 -> 옵션 확장 -> 필요 시 대체 도구",
FailureRecoveryKind.Dependency => 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 => FailureRecoveryKind.Timeout =>
"대상 범위 축소 -> 필터 적용 실행 -> 분할 실행 -> 최종 전체 실행", "대상 범위 축소 -> 필터 적용 실행 -> 분할 실행 -> 최종 전체 실행",
_ => failedToolName switch _ => failedToolName switch
{ {
"build_run" or "test_loop" => "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" => "file_read -> grep/glob -> git_tool(diff) -> file_edit -> build_run/test_loop", "file_edit" or "file_write" => BuildChangeReviewPrioritySequence(preferDirectReview: false, includeEditAndVerify: true),
"grep" or "glob" => "glob/grep 재구성 -> file_read -> folder_map(필요 시만)", "grep" or "glob" => "glob/grep 재구성 -> file_read -> folder_map(필요 시만)",
_ => "file_read -> grep/glob -> git_tool(diff) -> targeted tool retry" _ => BuildChangeReviewPrioritySequence(preferDirectReview: false, includeEditAndVerify: false)
} }
}; };
} }

View File

@@ -73,9 +73,21 @@ public class CodeReviewTool : IAgentTool
private async Task<ToolResult> DiffReviewAsync(AgentContext ctx, string target, string focus, CancellationToken ct) private async Task<ToolResult> 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 diffArgs = string.IsNullOrEmpty(target) ? "diff" : $"diff {target}";
var diffResult = await RunGitAsync(ctx.WorkFolder, diffArgs, ct); var diffResult = await RunGitAsync(gitRoot, diffArgs, ct);
if (diffResult == null) return ToolResult.Fail("Git을 찾을 수 없습니다."); if (diffResult == null)
{
return ToolResult.Fail(
"Git을 찾을 수 없거나 실행할 수 없습니다. " +
"Git diff를 쓸 수 없으면 code_review(file_review)로 파일별 리뷰를 진행하세요.");
}
if (string.IsNullOrWhiteSpace(diffResult)) if (string.IsNullOrWhiteSpace(diffResult))
return ToolResult.Ok("변경사항이 없습니다. (clean working tree)"); return ToolResult.Ok("변경사항이 없습니다. (clean working tree)");
@@ -368,6 +380,25 @@ public class CodeReviewTool : IAgentTool
private static string? FindGit() 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" }; var paths = new[] { "git", @"C:\Program Files\Git\bin\git.exe", @"C:\Program Files (x86)\Git\bin\git.exe" };
foreach (var p in paths) foreach (var p in paths)
{ {
@@ -384,6 +415,30 @@ public class CodeReviewTool : IAgentTool
return null; 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 파서 ────────────────────────────────────────────────────────── // ─── Diff 파서 ──────────────────────────────────────────────────────────
private static List<DiffFile> ParseDiffFiles(string diff) private static List<DiffFile> ParseDiffFiles(string diff)

View File

@@ -143,6 +143,9 @@ public class AgentContext
/// <summary>현재 활성 탭. "Chat" | "Cowork" | "Code".</summary> /// <summary>현재 활성 탭. "Chat" | "Cowork" | "Code".</summary>
public string ActiveTab { get; set; } = "Chat"; public string ActiveTab { get; set; } = "Chat";
/// <summary>현재 실행 턴의 원본 사용자 요청. 실행/열기 의도 가드에 사용합니다.</summary>
public string InitialUserQuery { get; set; } = "";
/// <summary>운영 모드. internal(사내) | external(사외). /// <summary>운영 모드. internal(사내) | external(사외).
/// 실행 중 사용자가 설정에서 모드를 바꾸면 SyncContextFromSettings를 통해 업데이트됨.</summary> /// 실행 중 사용자가 설정에서 모드를 바꾸면 SyncContextFromSettings를 통해 업데이트됨.</summary>
public string OperationMode { get; set; } = AxCopilot.Services.OperationModePolicy.InternalMode; public string OperationMode { get; set; } = AxCopilot.Services.OperationModePolicy.InternalMode;

View File

@@ -7,11 +7,18 @@ namespace AxCopilot.Services.Agent;
/// <summary>파일/URL을 시스템 기본 앱으로 여는 도구.</summary> /// <summary>파일/URL을 시스템 기본 앱으로 여는 도구.</summary>
public class OpenExternalTool : IAgentTool public class OpenExternalTool : IAgentTool
{ {
private static readonly string[] ExplicitOpenIntentKeywords =
[
"열어", "열어줘", "열어 줘", "오픈", "띄워", "띄워줘", "보여줘", "보여 줘",
"실행", "실행해", "실행해줘", "실행해 줘", "미리보기", "브라우저", "launch",
"open", "preview", "show", "serve", "run", "execute"
];
public string Name => "open_external"; public string Name => "open_external";
public string Description => public string Description =>
"Open a file with its default application or open a URL in the default browser. " + "Open a file with its default application or open a URL in the default browser. " +
"Also supports opening a folder in File Explorer. " + "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() public ToolParameterSchema Parameters => new()
{ {
@@ -44,6 +51,13 @@ public class OpenExternalTool : IAgentTool
return Task.FromResult(ToolResult.Ok($"외부 URI 열기: {rawPath}")); 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); 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}")); 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));
}
} }

View File

@@ -22,7 +22,7 @@ internal sealed class TaskTypePolicy
TaskType = "bugfix", TaskType = "bugfix",
GuidanceMessage = GuidanceMessage =
"[System:TaskType] This is a bug-fix task. Prioritize reproduction evidence, root cause linkage, smallest safe fix, and regression verification. " + "[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.", 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", 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", 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", TaskType = "feature",
GuidanceMessage = GuidanceMessage =
"[System:TaskType] This is a feature task. Prioritize affected interfaces/callers, data flow, validation paths, and test/documentation needs. " + "[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.", "Use folder_map only when the user explicitly needs folder structure or file listing.",
FailurePatternFocus = "Check new behavior flow and caller linkage first.", 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", 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", TaskType = "refactor",
GuidanceMessage = GuidanceMessage =
"[System:TaskType] This is a refactor task. Prioritize behavior preservation, reference impact, diff review, and non-regression evidence. " + "[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.", FailurePatternFocus = "Check behavior preservation and impact scope first.",
FollowUpTaskLine = "Task type: refactor. Prioritize behavior-preservation evidence over cosmetic cleanup.\n", FollowUpTaskLine = "Task type: refactor. Prioritize behavior-preservation evidence over cosmetic cleanup.\n",
FailureInvestigationTaskLine = "Extra check: validate existing call flow remains behavior-compatible.\n", FailureInvestigationTaskLine = "Extra check: validate existing call flow remains behavior-compatible.\n",
@@ -57,7 +57,7 @@ internal sealed class TaskTypePolicy
GuidanceMessage = GuidanceMessage =
"[System:TaskType] This is a review task. Prioritize concrete defects, regressions, risky assumptions, and missing tests before summaries. " + "[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. " + "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.", 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", 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", FailureInvestigationTaskLine = "Extra check: every risk must have either a concrete fix or an explicit unresolved rationale and impact.\n",

View File

@@ -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("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("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("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("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(" - 작업 유형: (e.g., report/analysis/proposal)");
sb.AppendLine(" - 산출물 파일: full absolute path (e.g., E:\\test\\report.html)"); 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("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("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("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("\n## Core Workflow");
sb.AppendLine("1. ORIENT: Pick the smallest next step that can answer the request."); sb.AppendLine("1. ORIENT: Pick the smallest next step that can answer the request.");