Code 탭 자동 스킬 오탐과 런타임 도구 제한 회귀 수정
원인: proactive auto skill이 실제 매치가 없어도 기본 점수만으로 선택되고, guidance 단계에서 allowed_tools 같은 하드 런타임 정책까지 자동 주입되어 빈 작업 폴더 요청에서 file_write가 빠진 채 종료됐습니다. 수정: SkillService의 proactive skill 점수를 실제 키워드·경로 신호 중심으로 다시 계산하고, auto skill은 guidance만 제공하며 하드 runtime policy는 더 이상 자동 주입하지 않도록 변경했습니다. 검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify_auto_skill_runtime_fix\ -p:IntermediateOutputPath=obj\verify_auto_skill_runtime_fix\ (경고 0 / 오류 0), dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter SkillServiceRuntimePolicyTests|FullyQualifiedName~RunAsync_EmptyWorkspace_BlocksExternalFallbackAndRecoversToFileWrite|FullyQualifiedName~RunAsync_EmptyWorkspace_DisallowsSkillManagerAndRecoversToFileWrite -p:OutputPath=bin\verify_auto_skill_runtime_fix_tests\ -p:IntermediateOutputPath=obj\verify_auto_skill_runtime_fix_tests\ (통과 15)
This commit is contained in:
@@ -1,5 +1,10 @@
|
|||||||
# AX Commander
|
# AX Commander
|
||||||
|
|
||||||
|
- 업데이트: 2026-04-15 19:46 (KST)
|
||||||
|
- Code 탭 auto skill 선택을 실제 키워드·경로 매치 기반으로 다시 조정했습니다. `src/AxCopilot/Services/Agent/SkillService.cs`가 기본 점수만으로 무관한 번들 스킬을 매 요청마다 붙이지 않도록 바뀌었습니다.
|
||||||
|
- 같은 파일에서 proactive auto skill은 guidance만 주고 `allowed_tools` 같은 하드 런타임 정책은 더 이상 자동 주입하지 않습니다. 빈 작업 폴더 생성 요청이 `file_write` 없이 종료되던 회귀를 막는 목적입니다.
|
||||||
|
- 테스트: `src/AxCopilot.Tests/Services/SkillServiceRuntimePolicyTests.cs`에 무관한 요청에서는 auto skill이 붙지 않는 케이스와, 매치된 auto skill도 런타임 정책을 강제 주입하지 않는 케이스를 추가했습니다.
|
||||||
|
|
||||||
- 업데이트: 2026-04-15 19:31 (KST)
|
- 업데이트: 2026-04-15 19:31 (KST)
|
||||||
- AX Agent 상단 라이브 안내 카드가 실행 중인데도 사라지던 회귀를 수정했습니다. `src/AxCopilot/Views/ChatWindow.xaml.cs`는 이제 같은 탭에 실행이 살아 있는 동안 상단 안내 카드와 상태 바를 유지하고, 현재 대화가 실행 대화와 다를 때는 본문 실행 이력만 숨긴 채 상단 진행 안내는 계속 보여주도록 분리합니다.
|
- AX Agent 상단 라이브 안내 카드가 실행 중인데도 사라지던 회귀를 수정했습니다. `src/AxCopilot/Views/ChatWindow.xaml.cs`는 이제 같은 탭에 실행이 살아 있는 동안 상단 안내 카드와 상태 바를 유지하고, 현재 대화가 실행 대화와 다를 때는 본문 실행 이력만 숨긴 채 상단 진행 안내는 계속 보여주도록 분리합니다.
|
||||||
- `src/AxCopilot/Views/ChatStreamingUiPolicy.cs`를 추가해 `숨김 / 현재 대화 / 같은 탭 백그라운드 대화` 상태를 명시적으로 분류하고, `src/AxCopilot.Tests/Views/ChatStreamingUiPolicyTests.cs`로 상단 가이드 유지 정책 회귀 테스트를 고정했습니다.
|
- `src/AxCopilot/Views/ChatStreamingUiPolicy.cs`를 추가해 `숨김 / 현재 대화 / 같은 탭 백그라운드 대화` 상태를 명시적으로 분류하고, `src/AxCopilot.Tests/Views/ChatStreamingUiPolicyTests.cs`로 상단 가이드 유지 정책 회귀 테스트를 고정했습니다.
|
||||||
|
|||||||
@@ -1517,3 +1517,9 @@ UI ?遺우쁽????域뱀뮆???귐뗫솯?醫딆춦 ???袁る퓮 ?臾믩씜 ??疫
|
|||||||
- `src/AxCopilot/Views/ChatStreamingUiPolicy.cs`를 추가해 `Hidden`, `ActiveConversation`, `BackgroundConversation` 세 상태를 명시적으로 분류하고, `src/AxCopilot.Tests/Views/ChatStreamingUiPolicyTests.cs`에 상단 가이드 유지 및 본문 렌더 분리 회귀 테스트를 추가했습니다.
|
- `src/AxCopilot/Views/ChatStreamingUiPolicy.cs`를 추가해 `Hidden`, `ActiveConversation`, `BackgroundConversation` 세 상태를 명시적으로 분류하고, `src/AxCopilot.Tests/Views/ChatStreamingUiPolicyTests.cs`에 상단 가이드 유지 및 본문 렌더 분리 회귀 테스트를 추가했습니다.
|
||||||
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_live_guide_persistence\\ -p:IntermediateOutputPath=obj\\verify_live_guide_persistence\\` 경고 0 / 오류 0
|
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_live_guide_persistence\\ -p:IntermediateOutputPath=obj\\verify_live_guide_persistence\\` 경고 0 / 오류 0
|
||||||
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ChatStreamingUiPolicyTests|ChatWindowSlashPolicyTests|ChatSessionStateServiceTests|AxAgentExecutionEngineTests" -p:OutputPath=bin\\verify_live_guide_persistence_tests\\ -p:IntermediateOutputPath=obj\\verify_live_guide_persistence_tests\\` 통과 98
|
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "ChatStreamingUiPolicyTests|ChatWindowSlashPolicyTests|ChatSessionStateServiceTests|AxAgentExecutionEngineTests" -p:OutputPath=bin\\verify_live_guide_persistence_tests\\ -p:IntermediateOutputPath=obj\\verify_live_guide_persistence_tests\\` 통과 98
|
||||||
|
업데이트: 2026-04-15 19:46 (KST)
|
||||||
|
- Code 탭 proactive auto skill 선택을 실제 키워드·경로 신호 기반으로 다시 제한했습니다. `src/AxCopilot/Services/Agent/SkillService.cs`에서 기본 점수만으로 무관한 번들 스킬이 항상 선택되던 경로를 제거해, 일반 코드 생성 요청에 unrelated skill runtime이 덧붙지 않도록 했습니다.
|
||||||
|
- 같은 파일에서 `BuildProactiveSkillSystemPromptAsync(...)`는 auto skill guidance에 더 이상 `[Skill Runtime Policy]`를 합치지 않도록 변경했습니다. 이 회귀 때문에 `allowed_tools`가 7개 수준으로 좁아지면서 빈 작업 폴더 생성 요청에서 `file_write`가 빠져 조기 종료되던 문제가 재현됐습니다.
|
||||||
|
- `src/AxCopilot.Tests/Services/SkillServiceRuntimePolicyTests.cs`에 `BuildProactiveSkillSystemPromptAsync_ReturnsNull_WhenNothingMeaningfullyMatches`, `BuildProactiveSkillSystemPromptAsync_DoesNotInjectHardRuntimePolicy`를 추가했습니다.
|
||||||
|
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_auto_skill_runtime_fix\\ -p:IntermediateOutputPath=obj\\verify_auto_skill_runtime_fix\\` 경고 0 / 오류 0
|
||||||
|
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "SkillServiceRuntimePolicyTests|FullyQualifiedName~RunAsync_EmptyWorkspace_BlocksExternalFallbackAndRecoversToFileWrite|FullyQualifiedName~RunAsync_EmptyWorkspace_DisallowsSkillManagerAndRecoversToFileWrite" -p:OutputPath=bin\\verify_auto_skill_runtime_fix_tests\\ -p:IntermediateOutputPath=obj\\verify_auto_skill_runtime_fix_tests\\` 통과 15
|
||||||
|
|||||||
@@ -263,6 +263,70 @@ public class SkillServiceRuntimePolicyTests
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildProactiveSkillSystemPromptAsync_ReturnsNull_WhenNothingMeaningfullyMatches()
|
||||||
|
{
|
||||||
|
var tempDir = Path.Combine(Path.GetTempPath(), "axcopilot-skill-proactive-none-" + Guid.NewGuid().ToString("N"));
|
||||||
|
Directory.CreateDirectory(tempDir);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SkillService.LoadSkills(projectRoot: tempDir);
|
||||||
|
|
||||||
|
var prompt = await SkillService.BuildProactiveSkillSystemPromptAsync(
|
||||||
|
"실시간으로 시간을 표시해주는 웹페이지를 만들어",
|
||||||
|
"Code",
|
||||||
|
tempDir);
|
||||||
|
|
||||||
|
prompt.Should().BeNull();
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task BuildProactiveSkillSystemPromptAsync_DoesNotInjectHardRuntimePolicy()
|
||||||
|
{
|
||||||
|
var tempDir = Path.Combine(Path.GetTempPath(), "axcopilot-skill-proactive-runtime-" + Guid.NewGuid().ToString("N"));
|
||||||
|
var skillDir = Path.Combine(tempDir, ".claude", "skills", "triage");
|
||||||
|
Directory.CreateDirectory(skillDir);
|
||||||
|
var skillPath = Path.Combine(skillDir, "SKILL.md");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var content = """
|
||||||
|
---
|
||||||
|
user-invocable: false
|
||||||
|
tabs: code
|
||||||
|
when_to_use: build failure test error
|
||||||
|
allowed-tools:
|
||||||
|
- file_read
|
||||||
|
- build_run
|
||||||
|
---
|
||||||
|
|
||||||
|
Investigate failures.
|
||||||
|
""";
|
||||||
|
File.WriteAllText(skillPath, content, Encoding.UTF8);
|
||||||
|
|
||||||
|
SkillService.LoadSkills(projectRoot: tempDir);
|
||||||
|
|
||||||
|
var prompt = await SkillService.BuildProactiveSkillSystemPromptAsync(
|
||||||
|
"build failure and test error in pipeline",
|
||||||
|
"Code",
|
||||||
|
tempDir);
|
||||||
|
|
||||||
|
prompt.Should().NotBeNull();
|
||||||
|
prompt.Should().Contain("[Auto Skill: triage]");
|
||||||
|
prompt.Should().Contain("Investigate failures.");
|
||||||
|
prompt.Should().NotContain("[Skill Runtime Policy]");
|
||||||
|
prompt.Should().NotContain("allowed_tools:");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task LoadSkills_LoadsAncestorProjectSkills_AndLegacyCommandFiles()
|
public async Task LoadSkills_LoadsAncestorProjectSkills_AndLegacyCommandFiles()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -178,11 +178,12 @@ public static class SkillService
|
|||||||
foreach (var skill in selected)
|
foreach (var skill in selected)
|
||||||
{
|
{
|
||||||
var compiledPrompt = await CompileSkillPromptAsync(skill, "", workFolder, ct).ConfigureAwait(false);
|
var compiledPrompt = await CompileSkillPromptAsync(skill, "", workFolder, ct).ConfigureAwait(false);
|
||||||
var runtimePolicy = BuildRuntimeDirective(skill);
|
if (string.IsNullOrWhiteSpace(compiledPrompt))
|
||||||
var payload = string.IsNullOrWhiteSpace(runtimePolicy)
|
continue;
|
||||||
? compiledPrompt
|
|
||||||
: $"{compiledPrompt}\n\n{runtimePolicy}";
|
// Auto skills are lightweight guidance only. Hard runtime overrides such as
|
||||||
sections.Add($"[Auto Skill: {skill.Name}]\n{payload}");
|
// allowed_tools or model/context switches must stay opt-in via explicit skill use.
|
||||||
|
sections.Add($"[Auto Skill: {skill.Name}]\n{compiledPrompt}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return sections.Count == 0
|
return sections.Count == 0
|
||||||
@@ -1292,10 +1293,12 @@ public static class SkillService
|
|||||||
.Select(skill => new
|
.Select(skill => new
|
||||||
{
|
{
|
||||||
Skill = skill,
|
Skill = skill,
|
||||||
Score = ScoreSkill(skill, userText, queryTokens)
|
Score = ScoreSkill(skill, userText, queryTokens),
|
||||||
|
Priority = GetSkillSourcePriority(skill.SourceScope),
|
||||||
})
|
})
|
||||||
.Where(x => x.Score > 0)
|
.Where(x => x.Score > 0)
|
||||||
.OrderByDescending(x => x.Score)
|
.OrderByDescending(x => x.Score)
|
||||||
|
.ThenByDescending(x => x.Priority)
|
||||||
.ThenBy(x => x.Skill.Name, StringComparer.OrdinalIgnoreCase)
|
.ThenBy(x => x.Skill.Name, StringComparer.OrdinalIgnoreCase)
|
||||||
.Select(x => x.Skill);
|
.Select(x => x.Skill);
|
||||||
}
|
}
|
||||||
@@ -1304,12 +1307,7 @@ public static class SkillService
|
|||||||
{
|
{
|
||||||
var score = 0;
|
var score = 0;
|
||||||
if (!string.IsNullOrWhiteSpace(skill.Paths) && _activeConditionalSkillNames.Contains(skill.Name))
|
if (!string.IsNullOrWhiteSpace(skill.Paths) && _activeConditionalSkillNames.Contains(skill.Name))
|
||||||
score += 4;
|
score += 8;
|
||||||
if (!skill.UserInvocable)
|
|
||||||
score += 1;
|
|
||||||
score += Math.Max(0, GetSkillSourcePriority(skill.SourceScope) / 200);
|
|
||||||
if (skill.DisableModelInvocation)
|
|
||||||
score += 1;
|
|
||||||
|
|
||||||
var haystack = $"{skill.Name} {skill.Label} {skill.Description} {skill.WhenToUse}".ToLowerInvariant();
|
var haystack = $"{skill.Name} {skill.Label} {skill.Description} {skill.WhenToUse}".ToLowerInvariant();
|
||||||
foreach (var token in queryTokens)
|
foreach (var token in queryTokens)
|
||||||
@@ -1320,7 +1318,16 @@ public static class SkillService
|
|||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(skill.WhenToUse)
|
if (!string.IsNullOrWhiteSpace(skill.WhenToUse)
|
||||||
&& userText.Contains(skill.WhenToUse, StringComparison.OrdinalIgnoreCase))
|
&& userText.Contains(skill.WhenToUse, StringComparison.OrdinalIgnoreCase))
|
||||||
score += 3;
|
score += 4;
|
||||||
|
|
||||||
|
// Small boosts are useful only after a real semantic/path match exists.
|
||||||
|
if (score > 0)
|
||||||
|
{
|
||||||
|
if (!skill.UserInvocable)
|
||||||
|
score += 1;
|
||||||
|
if (skill.DisableModelInvocation)
|
||||||
|
score += 1;
|
||||||
|
}
|
||||||
|
|
||||||
return score;
|
return score;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user