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:
2026-04-15 19:50:35 +09:00
parent 8721a0d8c7
commit f3717cda21
4 changed files with 95 additions and 13 deletions

View File

@@ -1,5 +1,10 @@
# 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)
- AX Agent 상단 라이브 안내 카드가 실행 중인데도 사라지던 회귀를 수정했습니다. `src/AxCopilot/Views/ChatWindow.xaml.cs`는 이제 같은 탭에 실행이 살아 있는 동안 상단 안내 카드와 상태 바를 유지하고, 현재 대화가 실행 대화와 다를 때는 본문 실행 이력만 숨긴 채 상단 진행 안내는 계속 보여주도록 분리합니다.
- `src/AxCopilot/Views/ChatStreamingUiPolicy.cs`를 추가해 `숨김 / 현재 대화 / 같은 탭 백그라운드 대화` 상태를 명시적으로 분류하고, `src/AxCopilot.Tests/Views/ChatStreamingUiPolicyTests.cs`로 상단 가이드 유지 정책 회귀 테스트를 고정했습니다.

View File

@@ -1517,3 +1517,9 @@ UI ?遺우쁽????域뱀뮆???귐뗫솯?醫딆춦 ???袁る퓮 ?臾믩씜 ??疫
- `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 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

View File

@@ -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]
public async Task LoadSkills_LoadsAncestorProjectSkills_AndLegacyCommandFiles()
{

View File

@@ -178,11 +178,12 @@ public static class SkillService
foreach (var skill in selected)
{
var compiledPrompt = await CompileSkillPromptAsync(skill, "", workFolder, ct).ConfigureAwait(false);
var runtimePolicy = BuildRuntimeDirective(skill);
var payload = string.IsNullOrWhiteSpace(runtimePolicy)
? compiledPrompt
: $"{compiledPrompt}\n\n{runtimePolicy}";
sections.Add($"[Auto Skill: {skill.Name}]\n{payload}");
if (string.IsNullOrWhiteSpace(compiledPrompt))
continue;
// Auto skills are lightweight guidance only. Hard runtime overrides such as
// 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
@@ -1292,10 +1293,12 @@ public static class SkillService
.Select(skill => new
{
Skill = skill,
Score = ScoreSkill(skill, userText, queryTokens)
Score = ScoreSkill(skill, userText, queryTokens),
Priority = GetSkillSourcePriority(skill.SourceScope),
})
.Where(x => x.Score > 0)
.OrderByDescending(x => x.Score)
.ThenByDescending(x => x.Priority)
.ThenBy(x => x.Skill.Name, StringComparer.OrdinalIgnoreCase)
.Select(x => x.Skill);
}
@@ -1304,12 +1307,7 @@ public static class SkillService
{
var score = 0;
if (!string.IsNullOrWhiteSpace(skill.Paths) && _activeConditionalSkillNames.Contains(skill.Name))
score += 4;
if (!skill.UserInvocable)
score += 1;
score += Math.Max(0, GetSkillSourcePriority(skill.SourceScope) / 200);
if (skill.DisableModelInvocation)
score += 1;
score += 8;
var haystack = $"{skill.Name} {skill.Label} {skill.Description} {skill.WhenToUse}".ToLowerInvariant();
foreach (var token in queryTokens)
@@ -1320,7 +1318,16 @@ public static class SkillService
if (!string.IsNullOrWhiteSpace(skill.WhenToUse)
&& 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;
}