diff --git a/README.md b/README.md index 2fb254f..be8eca1 100644 --- a/README.md +++ b/README.md @@ -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`로 상단 가이드 유지 정책 회귀 테스트를 고정했습니다. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 381728e..51f5146 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -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 diff --git a/src/AxCopilot.Tests/Services/SkillServiceRuntimePolicyTests.cs b/src/AxCopilot.Tests/Services/SkillServiceRuntimePolicyTests.cs index f0cd572..ce78a9c 100644 --- a/src/AxCopilot.Tests/Services/SkillServiceRuntimePolicyTests.cs +++ b/src/AxCopilot.Tests/Services/SkillServiceRuntimePolicyTests.cs @@ -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() { diff --git a/src/AxCopilot/Services/Agent/SkillService.cs b/src/AxCopilot/Services/Agent/SkillService.cs index 05452cb..41c20c2 100644 --- a/src/AxCopilot/Services/Agent/SkillService.cs +++ b/src/AxCopilot/Services/Agent/SkillService.cs @@ -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; }