스킬 런타임 2차 고도화와 도구 노출 필터 정비

프로젝트 .claude/skills 재귀 로드와 namespaced SKILL.md 파싱을 추가하고 번들/사용자/프로젝트 스킬을 함께 노출하도록 SkillService와 설정 UI를 확장했다.

슬래시 스킬 호출 시 인자 치환, 스킬 폴더 변수 치환, inline shell 실행, when_to_use 기반 자동 스킬 가이드를 실제 ChatWindow 런타임 경로에 연결했다.

blanket deny 권한은 모델 노출 전 활성 도구 목록에서 먼저 제외하도록 AgentLoopService를 보강했고 관련 테스트와 README/DEVELOPMENT 문서를 업데이트했다.

검증: dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_phase2\\ -p:IntermediateOutputPath=obj\\verify_phase2\\ (경고 0 / 오류 0)
검증: dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter "AgentToolCatalogTests|SkillServiceRuntimePolicyTests" -p:OutputPath=bin\\verify_phase2_tests\\ -p:IntermediateOutputPath=obj\\verify_phase2_tests\\ (통과 16, 기존 WorkspaceContextGeneratorTests nullable 경고 1건 유지)
This commit is contained in:
2026-04-14 18:10:16 +09:00
parent 8cb08576d5
commit b17c865c4e
12 changed files with 805 additions and 81 deletions

View File

@@ -524,7 +524,7 @@ public partial class ChatWindow : Window
if (_settings.Settings.Llm.EnableSkillSystem)
{
SkillService.EnsureSkillFolder();
SkillService.LoadSkills(_settings.Settings.Llm.SkillsFolderPath);
SkillService.LoadSkills(_settings.Settings.Llm.SkillsFolderPath, GetCurrentWorkFolder());
UpdateConditionalSkillActivation(reset: true);
}
@@ -2241,6 +2241,15 @@ public partial class ChatWindow : Window
return _settings.Settings.Llm.WorkFolder;
}
private void EnsureSkillSystemLoadedForCurrentWorkspace()
{
if (!_settings.Settings.Llm.EnableSkillSystem)
return;
SkillService.EnsureSkillFolder();
SkillService.LoadSkills(_settings.Settings.Llm.SkillsFolderPath, GetCurrentWorkFolder());
}
/// <summary>
/// 현재 작업 컨텍스트(첨부 파일 + 작업 폴더) 기준으로
/// 조건부 paths 스킬 활성화를 갱신합니다.
@@ -2248,6 +2257,7 @@ public partial class ChatWindow : Window
private void UpdateConditionalSkillActivation(bool reset = false)
{
if (!_settings.Settings.Llm.EnableSkillSystem) return;
EnsureSkillSystemLoadedForCurrentWorkspace();
var cwd = GetCurrentWorkFolder();
if (string.IsNullOrWhiteSpace(cwd) || !System.IO.Directory.Exists(cwd)) return;
if (reset) SkillService.ResetConditionalSkillActivation();
@@ -3460,6 +3470,7 @@ public partial class ChatWindow : Window
// 스킬 슬래시 명령어 매칭 (탭별 필터)
if (_settings.Settings.Llm.EnableSkillSystem)
{
EnsureSkillSystemLoadedForCurrentWorkspace();
var skillMatches = SkillService.MatchSlashCommand(text)
.Where(s => s.IsVisibleInTab(_activeTab))
.Select(s => (Cmd: "/" + s.Name,
@@ -3518,7 +3529,7 @@ public partial class ChatWindow : Window
}
/// <summary>슬래시 명령어를 감지하여 시스템 프롬프트와 사용자 텍스트를 분리합니다.</summary>
private (string? slashSystem, string userText) ParseSlashCommand(string input)
private async Task<(string? slashSystem, string userText)> ParseSlashCommandAsync(string input, CancellationToken ct = default)
{
var trimmed = input.TrimStart();
if (trimmed.StartsWith("/"))
@@ -3535,17 +3546,10 @@ public partial class ChatWindow : Window
}
// 스킬 명령어 매칭
var matchedSkill = SkillService.MatchSlashInvocation(input);
if (matchedSkill != null)
{
var slashCmd = "/" + matchedSkill.Name;
var rest = input[slashCmd.Length..].Trim();
var runtimePolicy = SkillService.BuildRuntimeDirective(matchedSkill);
var mergedPrompt = string.IsNullOrWhiteSpace(runtimePolicy)
? matchedSkill.SystemPrompt
: $"{matchedSkill.SystemPrompt}\n\n{runtimePolicy}";
return (mergedPrompt, string.IsNullOrEmpty(rest) ? matchedSkill.Label : rest);
}
EnsureSkillSystemLoadedForCurrentWorkspace();
var compiledInvocation = await SkillService.BuildSlashInvocationAsync(input, GetCurrentWorkFolder(), ct);
if (compiledInvocation != null)
return (compiledInvocation.SystemPrompt, compiledInvocation.DisplayText);
return (null, input);
}
@@ -5004,7 +5008,7 @@ public partial class ChatWindow : Window
{
llm.EnableSkillSystem = true;
SkillService.EnsureSkillFolder();
SkillService.LoadSkills(llm.SkillsFolderPath);
SkillService.LoadSkills(llm.SkillsFolderPath, GetCurrentWorkFolder());
UpdateConditionalSkillActivation(reset: true);
ScheduleSettingsSave();
_appState.LoadFromSettings(_settings);
@@ -5102,7 +5106,17 @@ public partial class ChatWindow : Window
ClearPromptCardPlaceholder();
// 슬래시 명령어 처리
var (slashSystem, displayText) = ParseSlashCommand(text);
var (slashSystem, displayText) = await ParseSlashCommandAsync(text, CancellationToken.None);
if (slashSystem == null
&& _settings.Settings.Llm.EnableSkillSystem
&& !text.TrimStart().StartsWith("/", StringComparison.Ordinal))
{
EnsureSkillSystemLoadedForCurrentWorkspace();
var autoSkillPrompt = await SkillService.BuildProactiveSkillSystemPromptAsync(displayText, _activeTab, GetCurrentWorkFolder(), CancellationToken.None);
if (!string.IsNullOrWhiteSpace(autoSkillPrompt))
slashSystem = autoSkillPrompt;
}
if (string.Equals(slashSystem, "__CLEAR__", StringComparison.Ordinal))
{