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

- 비 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
- 업데이트: 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`를 통해 실행 중인 대화는 앞쪽 링 표시로, 백그라운드 완료 후 아직 열어보지 않은 대화는 테마색 완료 점으로 구분하도록 정리했습니다. 완료 점은 해당 대화를 열면 바로 사라집니다.

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 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");
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<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]
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.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("명시적으로 요청");
}
}

View File

@@ -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<ParsedFailurePattern> 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)
}
};
}

View File

@@ -73,9 +73,21 @@ public class CodeReviewTool : IAgentTool
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 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<DiffFile> ParseDiffFiles(string diff)

View File

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

View File

@@ -7,11 +7,18 @@ namespace AxCopilot.Services.Agent;
/// <summary>파일/URL을 시스템 기본 앱으로 여는 도구.</summary>
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));
}
}

View File

@@ -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",

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("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.");