코드 리뷰 회복 흐름과 자동 실행 가드 정비
- 비 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:
@@ -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`를 통해 실행 중인 대화는 앞쪽 링 표시로, 백그라운드 완료 후 아직 열어보지 않은 대화는 테마색 완료 점으로 구분하도록 정리했습니다. 완료 점은 해당 대화를 열면 바로 사라집니다.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
@@ -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("명시적으로 요청");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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.");
|
||||
|
||||
Reference in New Issue
Block a user