# AX Copilot 차기 개발 계획 (v1.7.1 ~ v2.0) — 구현 수준 상세 명세 > **개정 기준**: 2026년 4월 — Claude Code 내부 아키텍처 문서 심층 분석 + 구현 수준 상세화 > 각 Phase는 C# 인터페이스·메서드 시그니처·WPF 바인딩·설정 스키마·통합 포인트까지 명시 > 현재 v1.7.1 — 52개 에이전트 도구, 29개 내장 스킬, 20개 코워크 프리셋 --- ## Claude Code 아키텍처 갭 진단 (v1.7.1 기준) | 갭 영역 | CC 보유 기능 | AX 현재 | 우선순위 Phase | |---------|------------|---------|--------------| | 훅 이벤트 | 17종 이벤트, 4타입(command/http/prompt/agent) | ~8종, 2타입 | 17-C | | 훅 출력 | additionalContext·permissionDecision·updatedInput 주입 | 없음 | 17-C | | 스킬 격리 | context:fork 서브에이전트 컨텍스트 | 없음 | 17-D | | 스킬 경로 활성화 | paths: glob 패턴 자동 주입 | 없음 | 17-D | | @include 메모리 | @파일경로 5단계 포함 | 없음 | 17-E | | 경로 기반 규칙 | rules/*.md paths: 프론트매터 | 없음 | 17-E | | acceptEdits 모드 | 파일 편집 자동승인, bash 확인 유지 | 없음 | 17-F | | 패턴 권한 규칙 | Bash(git *) 패턴 허용/차단 | 없음 | 17-F | | 코디네이터 에이전트 | 계획/라우팅 전담, 구현 위임 | 없음 | 18-A | | Worktree 격리 | 서브에이전트 독립 git 워킹카피 | 없음 | 18-A | | 백그라운드 에이전트 | 비동기 실행 + 완료 알림 | 없음 | 18-A | | Chat UI | 3패널, 인라인 설정, 세션 헤더 바 | 단일 패널, 별도 설정창 | 17-UI | --- ## Phase 17-UI — AX Agent 채팅 화면 전면 개편 (v1.8.0) > **목표**: ChatGPT Codex 스타일 3패널 레이아웃으로 채팅 화면 완전 재설계. > 에이전트 설정을 SettingsWindow에서 채팅 화면 인라인 패널로 이전. > Chat/Cowork/Code 탭 및 프리셋 유지. ### 17-UI-1: 신규 파일 구조 ``` src/AxCopilot/Views/AgentWindow/ AgentWindow.xaml ← 기존 AgentChatWindow 대체 AgentWindow.xaml.cs AgentSidebarView.xaml ← 좌측 사이드바 (240px↔48px) AgentSidebarView.xaml.cs AgentSessionHeaderBar.xaml ← 상단 세션 헤더 바 AgentSessionHeaderBar.xaml.cs AgentInlineSettingsPanel.xaml ← 우측 슬라이드인 설정 패널 (300px) AgentInlineSettingsPanel.xaml.cs AgentInputArea.xaml ← 하단 입력 영역 AgentInputArea.xaml.cs AgentChatView.xaml ← 채팅 메시지 영역 AgentChatView.xaml.cs AgentDiffPanel.xaml ← 통합 diff 뷰 src/AxCopilot/ViewModels/ AgentWindowViewModel.cs ← 신규 AgentSidebarViewModel.cs ← 신규 AgentSessionHeaderViewModel.cs ← 신규 AgentInlineSettingsViewModel.cs ← 신규 ``` ### 17-UI-2: AgentWindow.xaml 레이아웃 구조 ```xml ``` **애니메이션 스펙:** - 사이드바 확장/축소: `DoubleAnimation` Duration=`0:0:0.2` EasingFunction=`CubicEase EaseInOut` - Width: 240 ↔ 48 (GridLength 직접 애니메이션, `GridLengthAnimation` 헬퍼 클래스 사용) - 설정 패널 슬라이드: `DoubleAnimation` Duration=`0:0:0.2` EasingFunction=`CubicEase EaseOut` - Width: 0 ↔ 300 - 아이콘 회전 (사이드바 토글 화살표): `RotateTransform` 0 ↔ 180도 ### 17-UI-3: AgentWindowViewModel ```csharp public class AgentWindowViewModel : ViewModelBase { // ── 패널 상태 ── private bool _isSidebarExpanded = true; public bool IsSidebarExpanded { get => _isSidebarExpanded; set { SetProperty(ref _isSidebarExpanded, value); OnSidebarToggled(); } } private bool _isSettingsPanelOpen; public bool IsSettingsPanelOpen { get => _isSettingsPanelOpen; set => SetProperty(ref _isSettingsPanelOpen, value); } // ── 자식 ViewModel ── public AgentSidebarViewModel Sidebar { get; } public AgentSessionHeaderViewModel SessionHeader { get; } public AgentInlineSettingsViewModel InlineSettings { get; } public AgentInputAreaViewModel InputArea { get; } public ObservableCollection Messages { get; } // ── 커맨드 ── public ICommand ToggleSidebarCommand { get; } // IsSidebarExpanded 토글 public ICommand ToggleSettingsPanelCommand { get; } // IsSettingsPanelOpen 토글 public ICommand NewSessionCommand { get; } public ICommand SendMessageCommand { get; } public ICommand InterruptCommand { get; } // ── 의존성 ── public AgentWindowViewModel( AgentLoopService agentLoop, SkillLoaderService skillLoader, AgentMemoryService memory, SettingsService settings, IAgentSessionRepository sessionRepo) { ... } // ── 기존 AgentChatWindow 통합 ── // AgentLoopService.MessageReceived → Messages 추가 // AgentLoopService.ToolExecuting → SessionHeader.IsRunning = true // AgentLoopService.Completed → SessionHeader.IsRunning = false private void WireAgentEvents() { ... } } ``` ### 17-UI-4: AgentSessionHeaderViewModel ```csharp public class AgentSessionHeaderViewModel : ViewModelBase { // ── 모델 선택 ── public ObservableCollection AvailableModels { get; } private string _selectedModel; public string SelectedModel { get => _selectedModel; set { SetProperty(ref _selectedModel, value); ApplyModelChange(); } } // ── 모드 상태 ── public PlanMode PlanMode { get; set; } // Off | Always | Auto public PermissionMode PermissionMode { get; set; } // Default | AcceptEdits | Plan | Bypass public AgentTab ActiveTab { get; set; } // Chat | Cowork | Code public string ActivePreset { get; set; } // ── 실행 상태 ── public bool IsRunning { get; set; } public string StatusText { get; set; } // "도구 실행 중...", "생각 중..." public int CurrentIteration { get; set; } public int MaxIterations { get; set; } // ── 도구 상태 아이콘 (우측 상단 아이콘 행) ── // MCP 연결됨, 훅 활성, 스킬 N개 로드됨 등을 아이콘으로 표시 public bool IsMcpConnected { get; set; } public int ActiveHookCount { get; set; } public int LoadedSkillCount { get; set; } // ── 커맨드 ── public ICommand ChangePlanModeCommand { get; } // 클릭 시 Off→Always→Auto 순환 public ICommand ChangePermissionModeCommand { get; } public ICommand ChangeTabCommand { get; } // Chat/Cowork/Code public ICommand ChangePresetCommand { get; } public ICommand ToggleSettingsCommand { get; } // AgentWindowViewModel.ToggleSettingsPanelCommand 위임 public ICommand InterruptCommand { get; } public ICommand ExportPdfCommand { get; } } ``` **XAML 바인딩 예시 (AgentSessionHeaderBar.xaml):** ```xml ``` ### 17-UI-5: AgentSidebarViewModel ```csharp public class AgentSidebarViewModel : ViewModelBase { // ── 대화 이력 ── public ObservableCollection RecentSessions { get; } public SessionSummaryViewModel? SelectedSession { get; set; } // ── 프리셋 목록 ── public ObservableCollection Presets { get; } public PresetViewModel? ActivePreset { get; set; } // ── 확장 상태 ── // IsExpanded = true: 240px (아이콘+라벨), false: 48px (아이콘만) public bool IsExpanded { get; set; } // ── 커맨드 ── public ICommand NewSessionCommand { get; } public ICommand SelectSessionCommand { get; } // param: SessionSummaryViewModel public ICommand DeleteSessionCommand { get; } public ICommand SelectPresetCommand { get; } public ICommand EditPresetCommand { get; } public ICommand ImportSkillCommand { get; } } ``` ### 17-UI-6: AgentInlineSettingsPanel (SettingsWindow 분리) **SettingsWindow에서 이전되는 항목:** ``` [이전 대상 — AgentInlineSettingsPanel] - LLM 서비스 선택 (Chat/Cowork/Code별) - 모델 선택 (탭별) - 플랜 모드 (Off/Always/Auto) - 권한 모드 (Default/AcceptEdits/Plan) - MaxAgentIterations (탭별) - MaxRetryOnError - EnablePostToolVerification (탭별) - EnableCoworkVerification / EnableCodeVerification - 컨텍스트 파일 포함 설정 - 시스템 프롬프트 미리보기/편집 - 활성 프리셋 편집 [SettingsWindow에 남는 항목] - 일반: 테마, 폰트, AI 활성화 토글 - 런처: 단축키, 독 바, Everything 경로 - MCP 서버 등록 (전문 설정) - 훅 규칙 편집기 (전문 설정) - 권한 패턴 규칙 편집기 (전문 설정) - LSP 설정 - 암호화/보안 설정 ``` ```csharp public class AgentInlineSettingsViewModel : ViewModelBase { // ── 탭별 LLM 설정 (ActiveTab에 따라 바인딩 전환) ── public string LlmService { get; set; } public string ModelName { get; set; } public int MaxIterations { get; set; } public bool EnableVerification { get; set; } public PlanMode PlanMode { get; set; } public PermissionMode PermissionMode { get; set; } // ── 시스템 프롬프트 미리보기 ── public string SystemPromptPreview { get; set; } // 처음 200자 public ICommand EditSystemPromptCommand { get; } // 전체 편집 팝업 // ── 활성 스킬 목록 ── public ObservableCollection ActiveSkills { get; } public ICommand ManageSkillsCommand { get; } // ── MCP 상태 요약 ── public ObservableCollection McpServers { get; } // 설정 변경 즉시 SettingsService.Save() 호출 // 탭 전환 시 해당 탭 설정으로 바인딩 교체 public void SwitchToTab(AgentTab tab) { ... } } ``` ### 17-UI-7: AgentInputArea ```csharp public class AgentInputAreaViewModel : ViewModelBase { public string InputText { get; set; } public bool IsMultiline { get; set; } // Shift+Enter로 전환 public ObservableCollection AttachedFiles { get; } public bool IsAtMentionOpen { get; set; } // @ 입력 시 팝업 public bool IsSlashMenuOpen { get; set; } // / 입력 시 스킬 메뉴 public ObservableCollection SlashSuggestions { get; } // 커맨드 public ICommand SendCommand { get; } public ICommand AttachFileCommand { get; } public ICommand ClearAttachmentsCommand { get; } public ICommand OpenSlashMenuCommand { get; } // / 키 감지 public ICommand InsertAtMentionCommand { get; } // @ 키 감지 } ``` **XAML (AgentInputArea.xaml) 핵심:** ```xml ``` ### 17-UI-8: 통합 포인트 | 기존 클래스 | 변경 내용 | |-----------|---------| | `AgentChatWindow.xaml.cs` | `AgentWindow`로 교체 (리네임 + 재구성). `App.xaml.cs`의 `OpenAgentWindow()` 호출부 교체 | | `AgentChatViewModel.cs` | `AgentWindowViewModel`로 흡수·통합 | | `SettingsWindow.xaml` | AX Agent 탭 내 에이전트 설정 섹션 제거, "채팅 화면에서 설정 가능" 안내 문구 추가 | | `AgentLoopService.cs` | 이벤트 구독 포인트 변경 없음. ViewModel에서 이벤트 연결 방식만 변경 | | `App.xaml.cs` | `_agentWindow` 필드를 `AgentWindow` 타입으로 변경 | ### 17-UI-9: 구현 순서 ``` 1. AgentWindowViewModel + AgentSessionHeaderViewModel (VM 먼저) 2. AgentWindow.xaml 레이아웃 골격 (3열 Grid, 빈 자식) 3. AgentSessionHeaderBar.xaml (탭, 모드, 상태) 4. AgentSidebarView.xaml (대화 목록, 프리셋) 5. 기존 AgentChatView 내용 이식 (메시지 버블 등) 6. AgentInputArea.xaml (입력창, 첨부, 슬래시 메뉴) 7. AgentInlineSettingsPanel.xaml + SettingsWindow 항목 이전 8. 사이드바/설정 패널 애니메이션 9. GridLengthAnimation 헬퍼 클래스 구현 10. App.xaml.cs OpenAgentWindow() 교체 및 전체 통합 테스트 ``` --- ## Phase 17-A — Reflexion 강화 (v1.8.0) > **목표**: 성공·실패 모두 구조화된 자기평가 저장 → 동일 작업 유형 재실행 시 자동 참고 ### 17-A-1: 핵심 신규 클래스 ```csharp // 반성 메모리 엔트리 public record ReflexionEntry { public string TaskType { get; init; } // "code_generation", "file_refactor" 등 public string Summary { get; init; } // 작업 요약 public bool IsSuccess { get; init; } public float CompletionScore { get; init; } // 0.0~1.0 public string[] Weaknesses { get; init; } // 부족한 점 (실패 시) public string[] Strengths { get; init; } // 잘된 점 (성공 시) public string[] Lessons { get; init; } // 다음에 적용할 교훈 public DateTime CreatedAt { get; init; } public string SessionId { get; init; } } // 반성 메모리 저장소 인터페이스 public interface IReflexionRepository { Task SaveAsync(ReflexionEntry entry); Task> GetByTaskTypeAsync(string taskType, int limit = 5); Task BuildContextPromptAsync(string taskType); // 저장 위치: %APPDATA%\AxCopilot\reflexion\.jsonl } // 반성 메모리 구현 public class JsonlReflexionRepository : IReflexionRepository { private readonly string _baseDir; // %APPDATA%\AxCopilot\reflexion\ public Task SaveAsync(ReflexionEntry entry) { ... } public Task> GetByTaskTypeAsync(string taskType, int limit = 5) { ... } public Task BuildContextPromptAsync(string taskType) { // 최근 5개 엔트리를 시스템 프롬프트 삽입용 텍스트로 조합 // "이전 유사 작업에서 배운 점: ..." } } // 자기평가 생성 서비스 public class ReflexionEvaluatorService { private readonly ILlmClient _llm; private readonly IReflexionRepository _repo; // AgentLoopService.Completed 이벤트에서 호출 public Task EvaluateAsync( AgentSession session, AgentResult result, CancellationToken ct); // 평가 프롬프트: 완성도 점수, 강점/약점, 교훈 3가지 추출 private string BuildEvaluationPrompt(AgentSession session, AgentResult result) { ... } } // 작업 유형 분류기 public class TaskTypeClassifier { // 사용자 입력 → 작업 유형 분류 (규칙 기반 + LLM 보조) public string Classify(string userMessage) { ... } // "파일 수정", "코드 생성", "문서 작성", "검색", "분석", "테스트" 등 private static readonly Dictionary _keywords = ...; } ``` ### 17-A-2: AgentLoopService 통합 포인트 ```csharp // AgentLoopService.cs 에 추가 public class AgentLoopService { // 기존 필드에 추가 private readonly ReflexionEvaluatorService _reflexion; private readonly IReflexionRepository _reflexionRepo; private readonly TaskTypeClassifier _taskClassifier; // RunAsync() 시작 시: 반성 컨텍스트 주입 private async Task BuildSystemPromptAsync(string userMessage) { var taskType = _taskClassifier.Classify(userMessage); var reflexionContext = await _reflexionRepo.BuildContextPromptAsync(taskType); // 기존 시스템 프롬프트 끝에 reflexionContext 추가 } // RunAsync() 완료 후: 자기평가 저장 private async Task OnSessionCompletedAsync(AgentSession session, AgentResult result) { var entry = await _reflexion.EvaluateAsync(session, result, CancellationToken.None); await _reflexionRepo.SaveAsync(entry); } } ``` ### 17-A-3: 검증 대상 확대 (17-A2) ```csharp // 기존 PostToolVerificationService 확장 public class PostToolVerificationService { // 기존: bash, process 도구 후 검증 // 추가: script_create, file_edit 도구 후 검증 private static readonly HashSet _verifiableTools = new() { "bash", "process", "script_create", // 신규 "file_edit", // 신규 "file_write", // 신규 }; // 검증 체크리스트 (사용자 편집 가능) // 저장: %APPDATA%\AxCopilot\verification_checklist.json public Task LoadChecklistAsync() { ... } public Task SaveChecklistAsync(VerificationChecklist checklist) { ... } } public record VerificationChecklist { [JsonPropertyName("code_checks")] public string[] CodeChecks { get; init; } = new[] { "구문 오류 없음", "참조 무결성 확인", "null 참조 없음" }; [JsonPropertyName("file_checks")] public string[] FileChecks { get; init; } = new[] { "파일이 실제로 수정됨", "인코딩 올바름 (UTF-8)" }; } ``` ### 17-A-4: 설정 스키마 변경 ```csharp // AppSettings.cs → LlmSettings 에 추가 public class LlmSettings { // ... 기존 ... [JsonPropertyName("enable_reflexion")] public bool EnableReflexion { get; set; } = true; [JsonPropertyName("reflexion_max_context_entries")] public int ReflexionMaxContextEntries { get; set; } = 5; [JsonPropertyName("reflexion_evaluate_on_success")] public bool ReflexionEvaluateOnSuccess { get; set; } = true; } ``` --- ## Phase 17-B — 구조화된 태스크 상태 + 이벤트 로그 (v1.8.0) > **목표**: 대화 압축 시에도 유지되는 Working Memory + JSONL 이벤트 로그 ### 17-B-1: TaskState (Working Memory) ```csharp // 세션 전체에 걸쳐 유지되는 구조화된 작업 상태 public class TaskState { public string SessionId { get; init; } public string CurrentTask { get; set; } // 현재 수행 중인 작업 요약 public List ReferencedFiles { get; set; } // 이번 세션에서 참조한 파일들 public List DecisionLog { get; set; } // 의사결정 이력 public Dictionary KeyFacts { get; set; } // 핵심 사실 (key→value) public string ContextSummary { get; set; } // 대화 압축 시 갱신되는 요약 // 직렬화: %APPDATA%\AxCopilot\sessions\\task_state.json public Task SaveAsync(string baseDir) { ... } public static Task LoadAsync(string sessionId, string baseDir) { ... } } public record DecisionLogEntry { public DateTime Timestamp { get; init; } public string Decision { get; init; } public string Reason { get; init; } public string[] Alternatives { get; init; } } // TaskState 관리 서비스 public class TaskStateService { private readonly string _baseDir; private TaskState? _current; public TaskState Current => _current ?? throw new InvalidOperationException(); public Task InitializeAsync(string sessionId) { ... } public Task UpdateCurrentTaskAsync(string taskDescription) { ... } public Task AddReferencedFileAsync(string filePath) { ... } public Task LogDecisionAsync(string decision, string reason, string[] alternatives) { ... } public Task UpdateContextSummaryAsync(string summary) { ... } // AgentLoopService.ContextCompacting 이벤트에서 호출 // 압축 전 현재 TaskState를 시스템 프롬프트에 추가 public string BuildCompactContextInjection() { ... } } ``` ### 17-B-2: Event-Sourced 이벤트 로그 ```csharp // 모든 에이전트 이벤트의 구조화된 기록 public record AgentEventRecord { [JsonPropertyName("seq")] public long SeqNo { get; init; } [JsonPropertyName("id")] public string EventId { get; init; } // GUID [JsonPropertyName("parent_id")] public string? ParentEventId { get; init; } [JsonPropertyName("session_id")] public string SessionId { get; init; } [JsonPropertyName("type")] public AgentEventType Type { get; init; } [JsonPropertyName("ts")] public DateTime Timestamp { get; init; } [JsonPropertyName("payload")] public JsonElement Payload { get; init; } // 이벤트별 페이로드 } public enum AgentEventType { SessionStart, SessionEnd, UserMessage, AssistantMessage, ToolRequest, ToolResult, HookFired, HookResult, SkillActivated, SkillCompleted, CompactionTriggered, CompactionCompleted, SubagentSpawned, SubagentCompleted, Error } // JSONL 기반 이벤트 로그 저장소 // 저장: %APPDATA%\AxCopilot\sessions\\events.jsonl public class AgentEventLog { private readonly string _filePath; private long _seq = 0; public Task AppendAsync(AgentEventType type, object payload, string? parentId = null) { ... } public IAsyncEnumerable ReadAllAsync() { ... } public IAsyncEnumerable ReadFromAsync(long fromSeq) { ... } } // AgentLoopService에서 이벤트 기록 public class AgentLoopService { private AgentEventLog? _eventLog; // 기존 _events 컬렉션 대신 또는 병렬로 이벤트 로그 기록 private async Task RecordEventAsync(AgentEventType type, object payload) { if (_eventLog != null) await _eventLog.AppendAsync(type, payload); } } ``` ### 17-B-3: 설정 스키마 변경 ```csharp // AppSettings.cs 에 추가 public class AppSettings { // ... 기존 ... [JsonPropertyName("enable_event_log")] public bool EnableEventLog { get; set; } = true; [JsonPropertyName("event_log_retention_days")] public int EventLogRetentionDays { get; set; } = 30; [JsonPropertyName("enable_task_state")] public bool EnableTaskState { get; set; } = true; } ``` --- ## Phase 17-C — 훅 시스템 고도화 (v1.8.0) > **목표**: Claude Code의 17종 이벤트·4타입 훅 시스템을 AX Copilot에 이식 ### 17-C-1: 훅 이벤트 열거형 확장 ```csharp // 기존 HookEvent 열거형에 추가 public enum HookEvent { // ── 기존 (유지) ── PreToolUse, PostToolUse, AgentStop, // ── 신규 (Phase 17-C) ── UserPromptSubmit, // 사용자 프롬프트 제출 전 (차단·수정 가능) PreCompact, // 컨텍스트 압축 전 PostCompact, // 컨텍스트 압축 후 FileChanged, // watchPaths에 등록된 파일 변경 시 CwdChanged, // 작업 디렉토리 변경 시 (프로젝트 전환 감지) SessionStart, // 세션 시작 (watchPaths 등록, 초기 컨텍스트 주입) SessionEnd, // 세션 종료 (정리 작업) ConfigChange, // .ax/rules/*.md 또는 AX.md 파일 변경 PermissionRequest, // 도구 실행 권한 요청 (프로그래밍 방식 승인/거부) PreSkillExecute, // 스킬 실행 전 PostSkillExecute, // 스킬 실행 후 SubagentStart, // 서브에이전트 시작 SubagentStop, // 서브에이전트 종료 AgentIterationStart, // 에이전트 루프 반복 시작 AgentIterationEnd // 에이전트 루프 반복 종료 } ``` ### 17-C-2: 훅 타입 인터페이스 (Strategy 패턴) ```csharp // 훅 실행기 인터페이스 (Strategy) public interface IHookExecutor { HookType Type { get; } Task ExecuteAsync(HookContext context, CancellationToken ct); } // 훅 컨텍스트 (이벤트 정보 + 도구 입력 등) public record HookContext { public HookEvent Event { get; init; } public string? ToolName { get; init; } // PreToolUse, PostToolUse 시 public string? ToolInput { get; init; } // 도구 입력 JSON public string? ToolOutput { get; init; } // PostToolUse 시 도구 출력 public string? UserMessage { get; init; } // UserPromptSubmit 시 public string? ChangedFilePath { get; init; } // FileChanged 시 public string SessionId { get; init; } public Dictionary EnvVars { get; init; } // $TOOL_NAME, $ARGUMENTS 등 } // 훅 결과 public record HookResult { public bool Block { get; init; } = false; // true: 도구 실행 차단 public string? BlockReason { get; init; } public string? AdditionalContext { get; init; } // 시스템 프롬프트에 추가할 텍스트 public string? UpdatedInput { get; init; } // 수정된 도구 입력 (updatedInput) public PermissionDecision? PermissionDecision { get; init; } // allow | deny public IReadOnlyList? WatchPaths { get; init; } // SessionStart 시 등록 public string? StatusMessage { get; init; } // 스피너 커스텀 메시지 } public enum PermissionDecision { Allow, Deny, Ask } // ── 구현체 ── // 1. command 타입: 외부 프로세스 실행 public class CommandHookExecutor : IHookExecutor { public HookType Type => HookType.Command; public async Task ExecuteAsync(HookContext context, CancellationToken ct) { // stdout → stdout 읽어서 HookResult 파싱 // exit code 2 → Block = true // JSON 출력 → AdditionalContext, UpdatedInput 파싱 } } // 2. http 타입: HTTP 웹훅 public class HttpHookExecutor : IHookExecutor { public HookType Type => HookType.Http; // POST JSON 페이로드 → 응답 JSON 파싱 → HookResult } // 3. prompt 타입: LLM 검사 (소형 모델로 빠른 판단) public class PromptHookExecutor : IHookExecutor { public HookType Type => HookType.Prompt; private readonly ILlmClientFactory _llmFactory; public async Task ExecuteAsync(HookContext context, CancellationToken ct) { // 설정된 prompt 템플릿 + 컨텍스트 변수 치환 // LLM 호출 (기본 claude-haiku-4-5, 타임아웃 15초) // 응답에서 "BLOCK"/"ALLOW" 및 이유 파싱 } // 훅 설정 필드 public record PromptHookConfig { public string Prompt { get; init; } public string Model { get; init; } = "claude-haiku-4-5"; public int TimeoutSeconds { get; init; } = 15; } } // 4. agent 타입: 미니 에이전트 루프 public class AgentHookExecutor : IHookExecutor { public HookType Type => HookType.Agent; private readonly AgentLoopService _agentLoop; public async Task ExecuteAsync(HookContext context, CancellationToken ct) { // 격리된 미니 에이전트 루프 실행 // 파일 읽기·명령 실행·검증 가능 // 최대 반복: 3회 (하드코딩 또는 설정) // 결과를 HookResult로 변환 } } // 훅 실행기 팩토리 (Factory 패턴) public class HookExecutorFactory { private readonly IReadOnlyDictionary _executors; public HookExecutorFactory( CommandHookExecutor cmd, HttpHookExecutor http, PromptHookExecutor prompt, AgentHookExecutor agent) { _executors = new Dictionary { [HookType.Command] = cmd, [HookType.Http] = http, [HookType.Prompt] = prompt, [HookType.Agent] = agent, }; } public IHookExecutor GetExecutor(HookType type) => _executors.TryGetValue(type, out var exec) ? exec : throw new NotSupportedException($"훅 타입 미지원: {type}"); } ``` ### 17-C-3: 훅 속성 모델 확장 ```csharp // 기존 HookDefinition 에 속성 추가 public class HookDefinition { // ── 기존 ── public string? Matcher { get; init; } public HookType Type { get; init; } public string? Command { get; init; } public string? Url { get; init; } // ── 신규 속성 (Phase 17-C) ── [JsonPropertyName("if")] public string? Condition { get; init; } // 조건부 실행 (권한 모드 등 체크) [JsonPropertyName("once")] public bool Once { get; init; } = false; // 실행 후 자기 제거 [JsonPropertyName("async")] public bool IsAsync { get; init; } = false; // 비동기 실행 (결과 기다리지 않음) [JsonPropertyName("async_rewake")] public bool AsyncRewake { get; init; } = false; // 비동기 완료 후 에이전트 재깨우기 [JsonPropertyName("status_message")] public string? StatusMessage { get; init; } // 실행 중 스피너 메시지 [JsonPropertyName("timeout")] public int TimeoutSeconds { get; init; } = 30; // prompt 타입 전용 [JsonPropertyName("prompt")] public string? Prompt { get; init; } [JsonPropertyName("model")] public string? Model { get; init; } // agent 타입 전용 [JsonPropertyName("max_iterations")] public int MaxIterations { get; init; } = 3; } ``` ### 17-C-4: HookRunnerService 확장 ```csharp public class HookRunnerService { private readonly HookExecutorFactory _executorFactory; private readonly HookConfigRepository _configRepo; private readonly FileWatcherService _fileWatcher; // FileChanged 훅용 // ── 기존 메서드 시그니처 유지 + 새 이벤트 지원 ── // 훅 실행 (모든 이벤트 공통) public async Task RunAsync( HookEvent hookEvent, HookContext context, CancellationToken ct) { var defs = _configRepo.GetHooksForEvent(hookEvent, context.ToolName); // once 훅: 실행 후 설정에서 제거 // async 훅: 비동기 실행, 결과 기다리지 않음 // asyncRewake 훅: 완료 시 AgentLoopService.ResumeAsync() 호출 // if 조건: EvaluateCondition(def.Condition, context) 통과 시만 실행 } // FileChanged 감시 시작 (SessionStart 훅 watchPaths에서 호출) public void RegisterWatchPaths(IReadOnlyList patterns) { foreach (var pattern in patterns) _fileWatcher.Watch(pattern, path => RunAsync(HookEvent.FileChanged, new HookContext { ChangedFilePath = path, ... }, CancellationToken.None)); } // Once 훅 자기 제거 private void RemoveOnceHook(HookDefinition def) { ... } } public record HookRunResult { public bool AnyBlocked { get; init; } public string? CombinedContext { get; init; } // 여러 훅의 AdditionalContext 합산 public string? UpdatedInput { get; init; } // 마지막 updatedInput public PermissionDecision? PermissionDecision { get; init; } } ``` ### 17-C-5: AgentLoopService 통합 ```csharp // AgentLoopService.cs 에서 새 이벤트 발화 위치: public class AgentLoopService { // 1. 사용자 메시지 수신 시 → UserPromptSubmit 훅 private async Task OnUserMessageAsync(string message) { var ctx = new HookContext { Event = HookEvent.UserPromptSubmit, UserMessage = message, ... }; var result = await _hookRunner.RunAsync(HookEvent.UserPromptSubmit, ctx, _ct); if (result.AnyBlocked) return null; // 차단 return result.UpdatedInput ?? message; // 수정된 메시지 반환 } // 2. 세션 시작 시 → SessionStart 훅 private async Task OnSessionStartAsync() { var result = await _hookRunner.RunAsync(HookEvent.SessionStart, ..., _ct); if (result.WatchPaths?.Count > 0) _hookRunner.RegisterWatchPaths(result.WatchPaths); } // 3. 컨텍스트 압축 전후 → PreCompact/PostCompact 훅 private async Task CompactContextAsync() { await _hookRunner.RunAsync(HookEvent.PreCompact, ..., _ct); // ... 실제 압축 ... await _hookRunner.RunAsync(HookEvent.PostCompact, ..., _ct); } // 4. 권한 요청 시 → PermissionRequest 훅 private async Task RequestPermissionAsync(string toolName, string input) { var ctx = new HookContext { Event = HookEvent.PermissionRequest, ToolName = toolName, ToolInput = input }; var result = await _hookRunner.RunAsync(HookEvent.PermissionRequest, ctx, _ct); return result.PermissionDecision ?? PermissionDecision.Ask; // 훅 미설정 시 기존 UI 표시 } } ``` ### 17-C-6: 설정 스키마 변경 ```csharp // AppSettings.cs → LlmSettings에 추가 public class LlmSettings { [JsonPropertyName("hooks")] public HooksConfig Hooks { get; set; } = new(); } public class HooksConfig { [JsonPropertyName("user_prompt_submit")] public List UserPromptSubmit { get; set; } = new(); [JsonPropertyName("pre_compact")] public List PreCompact { get; set; } = new(); [JsonPropertyName("post_compact")] public List PostCompact { get; set; } = new(); [JsonPropertyName("file_changed")] public List FileChanged { get; set; } = new(); [JsonPropertyName("session_start")] public List SessionStart { get; set; } = new(); [JsonPropertyName("session_end")] public List SessionEnd { get; set; } = new(); [JsonPropertyName("permission_request")] public List PermissionRequest { get; set; } = new(); // ... 기존 PreToolUse, PostToolUse, AgentStop 유지 ... } ``` ### 17-C-7: 훅 편집기 UI (SettingsWindow에 잔류) ```csharp // SettingsWindow.xaml — Hooks 탭 (기존 위치 유지) // 신규: 훅 타입 선택 드롭다운 (Command/Http/Prompt/Agent) // 신규: prompt 타입 선택 시 → Prompt 텍스트박스 + Model 드롭다운 표시 // 신규: agent 타입 선택 시 → Prompt 텍스트박스 + MaxIterations 숫자 입력 표시 // 신규: 속성 체크박스 행: [once] [async] [asyncRewake] // 신규: statusMessage 텍스트 입력 // 신규: condition 텍스트 입력 (고급 섹션) ``` --- ## Phase 17-D — 스킬 시스템 고도화 (v1.8.0) > **목표**: fork 격리·경로 자동 활성화·스킬 범위 훅·모델 오버라이드 구현 ### 17-D-1: SkillFrontmatter 확장 ```csharp // 기존 SkillFrontmatter에 필드 추가 public class SkillFrontmatter { // ── 기존 ── [JsonPropertyName("description")] public string Description { get; init; } = string.Empty; [JsonPropertyName("when_to_use")] public string WhenToUse { get; init; } = string.Empty; [JsonPropertyName("arguments")] public IReadOnlyList Arguments { get; init; } = Array.Empty(); // ── 신규 (Phase 17-D) ── [JsonPropertyName("context")] public SkillContext Context { get; init; } = SkillContext.Default; // "fork" → 격리된 서브에이전트 컨텍스트 [JsonPropertyName("paths")] public IReadOnlyList Paths { get; init; } = Array.Empty(); // glob 패턴 목록: ["**/*.py", "src/**/*.cs"] // 해당 파일 작업 시 자동 컨텍스트 주입 [JsonPropertyName("model")] public string? Model { get; init; } // 스킬별 모델 오버라이드: "claude-opus-4-6", "claude-haiku-4-5" 등 [JsonPropertyName("user_invocable")] public bool UserInvocable { get; init; } = true; // false → /스킬 목록에서 숨김, AI가 자동 활성화만 가능 [JsonPropertyName("hooks")] public SkillHooksConfig? Hooks { get; init; } // 스킬 실행 중에만 적용되는 훅 } public enum SkillContext { Default, Fork } public class SkillHooksConfig { [JsonPropertyName("pre_skill_execute")] public List PreSkillExecute { get; init; } = new(); [JsonPropertyName("post_skill_execute")] public List PostSkillExecute { get; init; } = new(); [JsonPropertyName("post_tool_use")] public List PostToolUse { get; init; } = new(); } ``` ### 17-D-2: PathBasedSkillActivator ```csharp // 파일 경로에 따라 적용할 스킬을 자동 선택 public class PathBasedSkillActivator { private readonly SkillLoaderService _skillLoader; // 현재 작업 중인 파일 경로 기반으로 활성 스킬 목록 반환 public IReadOnlyList GetActiveSkillsForFile(string filePath) { return _skillLoader.GetAllSkills() .Where(s => s.Frontmatter.Paths.Any(pattern => GlobMatcher.IsMatch(filePath, pattern))) .ToList(); } // 활성 스킬의 컨텍스트를 시스템 프롬프트 주입용 텍스트로 빌드 public string BuildSkillContextInjection(IReadOnlyList skills) { if (skills.Count == 0) return string.Empty; var sb = new StringBuilder(); sb.AppendLine("## 현재 파일에 자동 적용된 스킬"); foreach (var skill in skills) { sb.AppendLine($"### {skill.Name}"); sb.AppendLine(skill.Body); } return sb.ToString(); } // GlobMatcher: ** 와일드카드, *.ext 패턴 지원 private static class GlobMatcher { public static bool IsMatch(string path, string pattern) { ... } } } ``` ### 17-D-3: ForkContextSkillRunner ```csharp // fork 컨텍스트 스킬: 격리된 서브에이전트 루프에서 실행 public class ForkContextSkillRunner { private readonly AgentLoopService _parentLoop; public async Task ExecuteInForkAsync( LoadedSkill skill, string userInput, CancellationToken ct) { // 1. 격리된 AgentSession 생성 (부모 대화 히스토리 제외) // 2. 스킬의 시스템 프롬프트만 포함 // 3. 모델 오버라이드 적용 (skill.Frontmatter.Model) // 4. 스킬 범위 훅 등록 (skill.Frontmatter.Hooks) // 5. 미니 에이전트 루프 실행 (최대 10 반복) // 6. 결과 텍스트 반환 (메인 대화에 reply로 주입) } } public record SkillResult { public bool IsSuccess { get; init; } public string Output { get; init; } = string.Empty; public string? ErrorMessage { get; init; } public int Iterations { get; init; } } ``` ### 17-D-4: SkillLoaderService 확장 ```csharp public class SkillLoaderService { // 기존 메서드 유지 + // 신규: UserInvocable=false 스킬을 슬래시 메뉴에서 필터 public IReadOnlyList GetUserInvocableSkills() => GetAllSkills().Where(s => s.Frontmatter.UserInvocable).ToList(); // 신규: paths 패턴이 있는 스킬 목록 (PathBasedSkillActivator용) public IReadOnlyList GetPathScopedSkills() => GetAllSkills().Where(s => s.Frontmatter.Paths.Count > 0).ToList(); } ``` ### 17-D-5: AgentLoopService 통합 ```csharp public class AgentLoopService { private readonly PathBasedSkillActivator _pathActivator; private readonly ForkContextSkillRunner _forkRunner; // 도구 실행 후: 작업 파일 경로 감지 → 경로 기반 스킬 자동 주입 private async Task OnFileToolExecutedAsync(string filePath) { var skills = _pathActivator.GetActiveSkillsForFile(filePath); if (skills.Count > 0) { var injection = _pathActivator.BuildSkillContextInjection(skills); _contextBuilder.AddDynamicContext("path_skills", injection); } } // 스킬 실행 시: fork vs default 분기 public async Task ExecuteSkillAsync(LoadedSkill skill, string input, CancellationToken ct) { if (skill.Frontmatter.Context == SkillContext.Fork) { var result = await _forkRunner.ExecuteInForkAsync(skill, input, ct); return result.Output; } else { // 기존 방식: 메인 컨텍스트에서 실행 return await ExecuteSkillInMainContextAsync(skill, input, ct); } } } ``` ### 17-D-6: 설정 없음 (프론트매터 기반) > 스킬 시스템 고도화는 SKILL.md 프론트매터 확장이 핵심. 별도 AppSettings 변경 최소화. > SkillLoaderService에서 신규 프론트매터 필드 파싱만 추가. --- ## Phase 17-E — 메모리/컨텍스트 고도화 (v1.8.0) > **목표**: @include 지시어·경로 기반 규칙 주입·/compact 명령·파일 되감기 ### 17-E-1: AxMdIncludeResolver ```csharp // AX.md 및 rules/*.md에서 @파일경로 지시어를 재귀 해석 public class AxMdIncludeResolver { private const int MaxDepth = 5; private const long MaxFileSize = 40_000; // 40,000자 경고 임계값 // @파일경로 지시어 모두 해석하여 최종 텍스트 반환 public async Task ResolveAsync( string content, string basePath, int depth = 0, HashSet? visited = null) { if (depth >= MaxDepth) return content; // 최대 깊이 초과 visited ??= new HashSet(StringComparer.OrdinalIgnoreCase); // @로 시작하는 줄 감지: "@./shared/common.md", "@../global/rules.md" var regex = new Regex(@"^@(.+)$", RegexOptions.Multiline); // ... 파일 읽기·재귀·순환 감지 ... } // 파일 크기 40,000자 초과 경고 public bool IsOversized(string content) => content.Length > MaxFileSize; // 순환 참조 감지: visited 집합으로 추적 private void DetectCircular(string filePath, HashSet visited) { if (visited.Contains(filePath)) throw new InvalidOperationException($"순환 참조 감지: {filePath}"); visited.Add(filePath); } } ``` ### 17-E-2: PathScopedRuleInjector ```csharp // .ax/rules/*.md의 paths: 프론트매터를 읽어 현재 파일에 맞는 규칙만 주입 public class PathScopedRuleInjector { private readonly string _rulesDir; // 프로젝트/.ax/rules/ private IReadOnlyList? _cachedRules; // 캐시 로드 (FileSystemWatcher로 변경 시 무효화) public async Task LoadRulesAsync() { ... } // 현재 파일 경로에 맞는 규칙 파일 필터링 public IReadOnlyList GetActiveRulesForFile(string currentFilePath) { return (_cachedRules ?? Array.Empty()) .Where(r => r.Frontmatter.Paths.Count == 0 || // paths 없으면 항상 적용 r.Frontmatter.Paths.Any(p => GlobMatcher.IsMatch(currentFilePath, p))) .ToList(); } // 시스템 프롬프트 주입용 텍스트 빌드 public string BuildInjection(IReadOnlyList rules) { var sb = new StringBuilder("## 프로젝트 규칙\n\n"); foreach (var rule in rules) sb.AppendLine(rule.Body).AppendLine(); return sb.ToString(); } } public record RuleFile { public string FilePath { get; init; } public RuleFrontmatter Frontmatter { get; init; } public string Body { get; init; } } public record RuleFrontmatter { [JsonPropertyName("name")] public string Name { get; init; } = string.Empty; [JsonPropertyName("paths")] public IReadOnlyList Paths { get; init; } = Array.Empty(); // 빈 배열 → 항상 적용 // ["src/**/*.py"] → Python 파일 작업 시만 적용 } ``` ### 17-E-3: ContextCompactionService + /compact 명령 ```csharp // 컨텍스트 압축 서비스 public class ContextCompactionService { private readonly AgentLoopService _agentLoop; private readonly HookRunnerService _hookRunner; private readonly TaskStateService _taskState; // /compact 슬래시 명령 처리 public async Task CompactAsync( string sessionId, CompactionOptions opts, CancellationToken ct) { // 1. PreCompact 훅 실행 await _hookRunner.RunAsync(HookEvent.PreCompact, ..., ct); // 2. 현재 TaskState를 새 컨텍스트 요약에 포함 var stateContext = _taskState.BuildCompactContextInjection(); // 3. LLM으로 대화 이력 요약 생성 var summary = await GenerateSummaryAsync(sessionId, stateContext, ct); // 4. 대화 이력 교체 (요약으로) _agentLoop.ReplaceHistoryWithSummary(sessionId, summary); // 5. PostCompact 훅 실행 await _hookRunner.RunAsync(HookEvent.PostCompact, ..., ct); return new CompactionResult { Summary = summary, SavedTokens = ... }; } // rewind_files: 특정 메시지 이후 에이전트가 수정한 파일 되돌리기 public async Task RewindFilesAsync(string sessionId, string afterMessageId) { // 1. AgentEventLog에서 afterMessageId 이후 file_edit 이벤트 수집 // 2. 각 이벤트의 이전 파일 내용(백업)을 복원 // 3. git checkout -- 또는 내부 백업에서 복원 } } public record CompactionOptions { public bool KeepTaskState { get; init; } = true; public bool KeepRecentN { get; init; } = true; public int RecentNMessages { get; init; } = 5; // 최근 N개 메시지는 유지 } ``` ### 17-E-4: /compact 슬래시 명령 등록 ```csharp // SlashCommandRegistry 에 등록 public class CompactSlashCommand : ISlashCommand { public string Name => "compact"; public string Description => "대화 컨텍스트를 수동으로 압축합니다"; public bool UserInvocable => true; private readonly ContextCompactionService _compaction; public async Task ExecuteAsync( string args, AgentSession session, CancellationToken ct) { var result = await _compaction.CompactAsync(session.Id, new CompactionOptions(), ct); return SlashCommandResult.Success( $"컨텍스트 압축 완료. 절약된 토큰: {result.SavedTokens:N0}"); } } ``` ### 17-E-5: 파일 크기 경고 UI ```csharp // WorkflowAnalyzerViewModel 또는 AgentSessionHeaderViewModel 에 추가 public bool HasOversizedRules { get; set; } // 40,000자 초과 시 true public string OversizedRuleWarning { get; set; } // "python.md가 40,000자를 초과합니다. 파일 분리를 권장합니다." // AgentSessionHeaderBar.xaml에 경고 배지 표시 // // // ``` --- ## Phase 17-F — 권한 시스템 고도화 (v1.8.0) > **목표**: acceptEdits 모드 추가 + 패턴 기반 허용/차단 규칙 + MCP 도구 권한 ### 17-F-1: PermissionMode 열거형 확장 ```csharp // 기존 AgentDecisionLevel → PermissionMode 로 개념 통합 public enum PermissionMode { Default, // 기존: 잠재적으로 위험한 작업에 확인 요청 AcceptEdits, // 신규: 파일 편집 자동승인, bash/process 확인 유지 Plan, // 기존: 읽기 전용, 쓰기 차단 BypassPermissions // 기존: 모든 확인 건너뜀 (자동화 전용) } ``` ### 17-F-2: PermissionRule + Chain (Chain of Responsibility 패턴) ```csharp // 권한 규칙 하나 public record PermissionRule { [JsonPropertyName("tool")] public string ToolName { get; init; } // "process", "file_edit", "mcp__myserver__*" [JsonPropertyName("pattern")] public string? Pattern { get; init; } // "git *", "rm -rf *", null(모든 입력) [JsonPropertyName("behavior")] public PermissionBehavior Behavior { get; init; } // Allow | Deny | Ask } public enum PermissionBehavior { Allow, Deny, Ask } // 책임 연쇄 핸들러 인터페이스 public abstract class PermissionHandler { protected PermissionHandler? _next; public PermissionHandler SetNext(PermissionHandler next) { _next = next; return next; } public abstract PermissionDecision? Handle(string toolName, string input); } // 구현: Deny 규칙 핸들러 public class DenyRuleHandler : PermissionHandler { private readonly IReadOnlyList _denyRules; public override PermissionDecision? Handle(string toolName, string input) { if (_denyRules.Any(r => Matches(r, toolName, input))) return PermissionDecision.Deny; return _next?.Handle(toolName, input); } private bool Matches(PermissionRule rule, string toolName, string input) => rule.ToolName == toolName && (rule.Pattern == null || GlobMatcher.IsMatch(input, rule.Pattern)); } // 구현: Allow 규칙 핸들러 public class AllowRuleHandler : PermissionHandler { private readonly IReadOnlyList _allowRules; public override PermissionDecision? Handle(string toolName, string input) { ... } } // 구현: AcceptEdits 모드 핸들러 public class AcceptEditsHandler : PermissionHandler { private readonly PermissionMode _mode; private static readonly HashSet _editTools = new() { "file_edit", "file_write", "script_create" }; public override PermissionDecision? Handle(string toolName, string input) { if (_mode == PermissionMode.AcceptEdits && _editTools.Contains(toolName)) return PermissionDecision.Allow; // 파일 편집 자동승인 return _next?.Handle(toolName, input); } } // 권한 결정 서비스 (Chain 조립) public class PermissionDecisionService { private readonly PermissionHandler _chain; public PermissionDecisionService(AppSettings settings) { // Chain 조립: Deny → Allow → AcceptEdits → Default(Ask) var denyHandler = new DenyRuleHandler(settings.Permissions.DenyRules); var allowHandler = new AllowRuleHandler(settings.Permissions.AllowRules); var acceptEditsHandler = new AcceptEditsHandler(settings.Permissions.Mode); var defaultHandler = new DefaultAskHandler(); denyHandler.SetNext(allowHandler).SetNext(acceptEditsHandler).SetNext(defaultHandler); _chain = denyHandler; } public PermissionDecision Decide(string toolName, string input) => _chain.Handle(toolName, input) ?? PermissionDecision.Ask; } ``` ### 17-F-3: MCP 도구 권한 ```csharp // MCP 도구 규칙: mcp__서버명, mcp__서버명__도구명 // ToolName 패턴: "mcp__myserver", "mcp__myserver__query_db" // PermissionRule에서 ToolName="mcp__myserver" → 해당 서버 모든 도구 차단 public class McpPermissionFilter { private readonly PermissionDecisionService _permissions; // McpClientService.ExecuteToolAsync() 호출 전 적용 public bool IsToolAllowed(string serverName, string toolName) { var fullName = $"mcp__{serverName}__{toolName}"; var serverPattern = $"mcp__{serverName}"; var byTool = _permissions.Decide(fullName, string.Empty); if (byTool == PermissionDecision.Deny) return false; var byServer = _permissions.Decide(serverPattern, string.Empty); if (byServer == PermissionDecision.Deny) return false; return true; } } ``` ### 17-F-4: 설정 스키마 변경 ```csharp // AppSettings.cs 에 추가 public class AppSettings { [JsonPropertyName("permissions")] public PermissionsConfig Permissions { get; set; } = new(); } public class PermissionsConfig { [JsonPropertyName("mode")] public PermissionMode Mode { get; set; } = PermissionMode.Default; [JsonPropertyName("allow")] public List AllowRules { get; set; } = new(); // 예: [{"tool":"process","pattern":"git *","behavior":"allow"}] [JsonPropertyName("deny")] public List DenyRules { get; set; } = new(); // 예: [{"tool":"process","pattern":"rm -rf *","behavior":"deny"}] } ``` ### 17-F-5: 권한 편집기 UI (SettingsWindow — 전문 설정 탭) ``` [설정창 > 권한 탭] 권한 모드: [Default ▾] (Default / AcceptEdits / Plan / BypassPermissions) ── 허용 규칙 ────────────────────────────── [도구: process ] [패턴: git * ] [허용 ▾] [삭제] [도구: process ] [패턴: dotnet * ] [허용 ▾] [삭제] [+ 규칙 추가] ── 차단 규칙 ────────────────────────────── [도구: process ] [패턴: rm -rf * ] [차단 ▾] [삭제] [도구: mcp__외부서버 ] [패턴: (전체) ] [차단 ▾] [삭제] [+ 규칙 추가] ``` ```csharp // PermissionsTabViewModel (SettingsWindow 내 탭) public class PermissionsTabViewModel : ViewModelBase { public PermissionMode SelectedMode { get; set; } public ObservableCollection AllowRules { get; } public ObservableCollection DenyRules { get; } public ICommand AddAllowRuleCommand { get; } public ICommand AddDenyRuleCommand { get; } public ICommand DeleteRuleCommand { get; } // param: PermissionRuleViewModel public ICommand SaveCommand { get; } } ``` ### 17-F-6: MCP HTTP+SSE 전송 추가 ```csharp // McpClientService.cs 에 전송 타입 추가 public enum McpTransport { Stdio, Http, Sse } public class McpServerConfig { // 기존 [JsonPropertyName("command")] public string? Command { get; init; } [JsonPropertyName("args")] public IReadOnlyList Args { get; init; } = Array.Empty(); // 신규 [JsonPropertyName("type")] public McpTransport Transport { get; init; } = McpTransport.Stdio; [JsonPropertyName("url")] public string? Url { get; init; } // HTTP/SSE 전용 [JsonPropertyName("headers")] public Dictionary Headers { get; init; } = new(); // $VAR 환경변수 치환 지원 } // McpClientService 에 HTTP/SSE 클라이언트 추가 public class McpClientService { // 기존: stdio 클라이언트 // 신규: HTTP 전송 클라이언트 private async Task SendHttpAsync(McpServerConfig config, McpRequest req, CancellationToken ct) { // headers의 $VAR → 환경변수 치환 // POST {config.Url} + Authorization 헤더 } // 신규: SSE 전송 클라이언트 private async Task SubscribeSseAsync(McpServerConfig config, McpRequest req, CancellationToken ct) { // Server-Sent Events 수신 } } ``` --- ## Phase 17-G — 멀티파일 Diff + 자동 컨텍스트 수집 (v1.8.0) ### 17-G-1: 멀티파일 통합 Diff 뷰 ```csharp // 기존 DiffPanel 확장 public class MultiFileDiffViewModel : ViewModelBase { public ObservableCollection FileDiffs { get; } public int TotalChangedFiles => FileDiffs.Count; public int TotalAddedLines { get; } public int TotalRemovedLines { get; } // 전체 수락/거부 public ICommand AcceptAllCommand { get; } public ICommand RejectAllCommand { get; } // 파일별 수락/거부 public ICommand AcceptFileCommand { get; } // param: FileDiffViewModel public ICommand RejectFileCommand { get; } // param: FileDiffViewModel } public class FileDiffViewModel : ViewModelBase { public string FilePath { get; } public ObservableCollection Hunks { get; } public bool IsExpanded { get; set; } = true; // 파일별 접기/펼치기 public DiffFileStatus Status { get; } // Modified | Added | Deleted | Renamed public ICommand AcceptCommand { get; } public ICommand RejectCommand { get; } } ``` **XAML 핵심:** ```xml ``` ### 17-G-2: 자동 컨텍스트 수집 ```csharp // 사용자 메시지에서 파일명 감지 → 자동 읽기 public class AutoContextCollector { private readonly IAgentTool _fileReadTool; // 패턴: "파일명.확장자", "`백틱경로`", "src/path/to/file" 형태 감지 private static readonly Regex _filePatterns = new( @"(?:^|\s)([A-Za-z가-힣_\-\.\/\\]+\.[a-zA-Z]{1,6})(?:\s|$)|`([^`]+)`", RegexOptions.Multiline); public async Task> CollectAsync( string userMessage, string projectRoot, CancellationToken ct) { var matches = _filePatterns.Matches(userMessage); var results = new List(); foreach (Match m in matches) { var path = m.Groups[1].Value ?? m.Groups[2].Value; var fullPath = Path.Combine(projectRoot, path); if (File.Exists(fullPath)) { var content = await File.ReadAllTextAsync(fullPath, ct); results.Add(new FileContext { Path = path, Content = content }); } } return results; } } // 도구 위험도 정적 매핑 public static class ToolRiskMapper { public static ToolRisk GetRisk(string toolName) => toolName switch { "file_read" or "code_search" or "list_directory" => ToolRisk.Low, "file_edit" or "script_create" or "git_commit" => ToolRisk.Medium, "process" or "bash" or "file_delete" or "registry_write" => ToolRisk.High, _ when toolName.StartsWith("mcp__") => ToolRisk.Medium, _ => ToolRisk.Medium }; } public enum ToolRisk { Low, Medium, High } ``` --- ## Phase 18-A — 멀티에이전트 팀 + Worktree 격리 (v2.0) > **목표**: 코디네이터 에이전트 모드·Worktree 격리·위임 도구·백그라운드 에이전트 ### 18-A-1: 코디네이터 에이전트 ```csharp // 코디네이터 모드: 계획/라우팅 전담 public interface IAgentCoordinator { Task CreatePlanAsync(string userRequest, CancellationToken ct); Task ExecutePlanAsync(CoordinatorPlan plan, IProgress progress, CancellationToken ct); } public class CoordinatorAgent : IAgentCoordinator { private readonly AgentLoopService _loop; private readonly DelegateAgentTool _delegateTool; public async Task CreatePlanAsync(string userRequest, CancellationToken ct) { // 전용 시스템 프롬프트로 계획 생성 // 출력: JSON 형태의 SubtaskList // 각 서브태스크: agentType, description, dependencies[] } public async Task ExecutePlanAsync(CoordinatorPlan plan, IProgress progress, CancellationToken ct) { // 의존성 없는 서브태스크 병렬 실행 // 완료된 서브태스크 결과를 다음 서브태스크 컨텍스트에 주입 // 최종 결과 병합 } } public record CoordinatorPlan { public string OriginalRequest { get; init; } public IReadOnlyList Tasks { get; init; } } public record SubTask { public string Id { get; init; } public string AgentType { get; init; } // "code-reviewer", "researcher", "implementer" public string Description { get; init; } public IReadOnlyList Dependencies { get; init; } // 선행 SubTask Id 목록 public SubTaskStatus Status { get; set; } // Pending | Running | Completed | Failed public string? Result { get; set; } } public enum SubTaskStatus { Pending, Running, Completed, Failed } ``` ### 18-A-2: WorktreeManager ```csharp // git worktree 기반 격리 실행 환경 public class WorktreeManager : IDisposable { private readonly List _active = new(); // 새 worktree 생성 (임시 브랜치) public async Task CreateAsync( string repoRoot, string branchName, CancellationToken ct) { var worktreePath = Path.Combine(Path.GetTempPath(), "ax-worktree-" + Guid.NewGuid().ToString("N")[..8]); // git worktree add -b await RunGitAsync($"worktree add {worktreePath} -b {branchName}", repoRoot, ct); var wt = new Worktree { Path = worktreePath, Branch = branchName, RepoRoot = repoRoot }; _active.Add(wt); return wt; } // 변경 사항 병합 (승인 시) public async Task MergeAsync(Worktree worktree, string targetBranch, CancellationToken ct) { await RunGitAsync($"merge {worktree.Branch}", worktree.RepoRoot, ct); } // worktree 제거 (거부 시) public async Task DisposeAsync(Worktree worktree, bool keepBranch = false, CancellationToken ct = default) { await RunGitAsync($"worktree remove {worktree.Path} --force", worktree.RepoRoot, ct); if (!keepBranch) await RunGitAsync($"branch -D {worktree.Branch}", worktree.RepoRoot, ct); _active.Remove(worktree); } private Task RunGitAsync(string args, string cwd, CancellationToken ct) { ... } public void Dispose() { /* 남은 worktree 정리 */ } } public record Worktree { public string Path { get; init; } public string Branch { get; init; } public string RepoRoot { get; init; } public DateTime CreatedAt { get; init; } = DateTime.UtcNow; } ``` ### 18-A-3: DelegateAgentTool ```csharp // IAgentTool 구현: 전문 서브에이전트에 작업 위임 public class DelegateAgentTool : IAgentTool { public string Name => "delegate"; public string Description => "전문 에이전트에 작업을 위임합니다"; public JsonObject Parameters => new() { ["type"] = "object", ["properties"] = new JsonObject { ["agent_type"] = new JsonObject { ["type"] = "string", ["enum"] = new JsonArray("researcher", "code-reviewer", "implementer", "security-auditor", "doc-writer"), ["description"] = "위임할 에이전트 유형" }, ["task"] = new JsonObject { ["type"] = "string", ["description"] = "에이전트에게 전달할 작업 설명" }, ["context"] = new JsonObject { ["type"] = "string", ["description"] = "에이전트에게 전달할 추가 컨텍스트" }, ["isolation"] = new JsonObject { ["type"] = "string", ["enum"] = new JsonArray("none", "worktree"), ["description"] = "격리 수준: worktree → git worktree 격리" } }, ["required"] = new JsonArray("agent_type", "task") }; private readonly BackgroundAgentService _backgroundAgents; private readonly WorktreeManager _worktreeManager; private readonly AgentTypeMemoryRepository _agentMemory; public async Task ExecuteAsync(JsonObject args, CancellationToken ct) { var agentType = args["agent_type"]!.GetValue(); var task = args["task"]!.GetValue(); var context = args["context"]?.GetValue() ?? string.Empty; var isolation = args["isolation"]?.GetValue() ?? "none"; // 에이전트 타입별 메모리 로드 var memory = await _agentMemory.LoadMemoryAsync(agentType); // worktree 격리 필요 시 생성 Worktree? worktree = null; if (isolation == "worktree") worktree = await _worktreeManager.CreateAsync( GetCurrentRepoRoot(), $"ax-{agentType}-{DateTime.UtcNow:yyMMddHHmm}", ct); // 서브에이전트 실행 var result = await _backgroundAgents.StartAsync(new AgentTask { AgentType = agentType, Description = task, Context = context + (memory != null ? $"\n\n## 이전 학습:\n{memory}" : string.Empty), WorkingDirectory = worktree?.Path ?? GetCurrentDirectory() }, ct); // 결과 크기 제한: 100,000자 if (result.Length > 100_000) result = result[..100_000] + "\n\n[결과가 100,000자를 초과하여 잘림]"; return ToolResult.Ok(result); } } ``` ### 18-A-4: BackgroundAgentService ```csharp // 비동기 서브에이전트 실행 + 완료 알림 public class BackgroundAgentService { private readonly ConcurrentDictionary _running = new(); // 이벤트: 완료 시 발화 (트레이 알림 연결) public event EventHandler? AgentCompleted; // 비동기 실행 시작 (즉시 반환, 완료 시 이벤트) public async Task StartAsync(AgentTask task, CancellationToken ct) { var id = Guid.NewGuid().ToString("N")[..8]; var bgTask = new BackgroundAgentTask { Id = id, Task = task, StartedAt = DateTime.UtcNow }; _running[id] = bgTask; _ = Task.Run(async () => { try { // 격리된 AgentLoopService 인스턴스 생성 var result = await RunSubAgentLoopAsync(task, ct); bgTask.Result = result; bgTask.Status = BackgroundAgentStatus.Completed; AgentCompleted?.Invoke(this, new AgentCompletedEventArgs { Task = bgTask, Result = result }); } catch (Exception ex) { bgTask.Status = BackgroundAgentStatus.Failed; bgTask.Error = ex.Message; AgentCompleted?.Invoke(this, new AgentCompletedEventArgs { Task = bgTask, Error = ex.Message }); } finally { _running.TryRemove(id, out _); } }, ct); return id; } public IReadOnlyList GetActive() => _running.Values.ToList(); } // App.xaml.cs 에서 트레이 알림 연결 // _backgroundAgents.AgentCompleted += (s, e) => // _trayIcon.ShowBalloonTip(e.Task.AgentType + " 완료", e.Result?.Length > 100 ? e.Result[..100] + "..." : e.Result, ToolTipIcon.Info); ``` ### 18-A-5: AgentTypeMemoryRepository ```csharp // 에이전트 타입별 영속 메모리 // 저장: %APPDATA%\AxCopilot\agent-memory\\MEMORY.md public class AgentTypeMemoryRepository { private readonly string _baseDir; // %APPDATA%\AxCopilot\agent-memory\ public async Task LoadMemoryAsync(string agentType) { var path = GetPath(agentType); return File.Exists(path) ? await File.ReadAllTextAsync(path) : null; } public async Task SaveMemoryAsync(string agentType, string content) { Directory.CreateDirectory(Path.GetDirectoryName(GetPath(agentType))!); await File.WriteAllTextAsync(GetPath(agentType), content); } // 학습 내용 추가 (기존 내용 끝에 append) public async Task AppendLearnAsync(string agentType, string learning) { var existing = await LoadMemoryAsync(agentType) ?? string.Empty; var entry = $"\n- [{DateTime.Now:yyyy-MM-dd HH:mm}] {learning}"; await SaveMemoryAsync(agentType, existing + entry); } private string GetPath(string agentType) => Path.Combine(_baseDir, agentType, "MEMORY.md"); } ``` ### 18-A-6: 코디네이터 모드 UI ```csharp // AgentSessionHeaderViewModel 에 추가 public bool IsCoordinatorMode { get; set; } public ICommand ToggleCoordinatorModeCommand { get; } // AgentSessionHeaderBar.xaml 에 추가: // [코디네이터 모드 토글 버튼] — 활성 시 AccentColor로 강조 // 코디네이터 모드 ON 시: 서브태스크 진행 패널 표시 // CoordinatorPlanViewModel: SubTask 목록 + 진행 상태 표시 public class CoordinatorPlanViewModel : ViewModelBase { public ObservableCollection Tasks { get; } public int CompletedCount { get; } public int TotalCount { get; } public double Progress => TotalCount > 0 ? (double)CompletedCount / TotalCount : 0; } ``` --- ## Phase 18-B — 에이전트 리플레이/디버깅 (v2.0) > **목표**: Phase 17-B의 이벤트 로그를 활용한 세션 재생 및 분기 재실행 ```csharp // 세션 리플레이 서비스 public class AgentReplayService { private readonly AgentEventLog _eventLog; private readonly AgentLoopService _agentLoop; // 특정 시점까지 이벤트 재생 public async Task ReplayToAsync(string sessionId, long upToSeqNo, CancellationToken ct) { await foreach (var evt in _eventLog.ReadFromAsync(0).WithCancellation(ct)) { if (evt.SeqNo > upToSeqNo) break; await ApplyEventAsync(evt); } } // 특정 시점에서 분기 (새 세션 생성) public async Task ForkFromAsync(string sessionId, long forkAtSeqNo, CancellationToken ct) { var newSessionId = Guid.NewGuid().ToString("N"); await ReplayToAsync(sessionId, forkAtSeqNo, ct); // 새 세션 ID로 계속 return newSessionId; } // ViewModel: 타임라인 UI // WorkflowAnalyzerViewModel 에 리플레이 타임라인 패널 추가 } public class ReplayTimelineViewModel : ViewModelBase { public ObservableCollection Events { get; } public long CurrentPosition { get; set; } public ICommand SeekToCommand { get; } // param: long seqNo public ICommand ForkFromCommand { get; } // param: long seqNo public ICommand PlayPauseCommand { get; } } ``` --- ## Phase 18-C — 플러그인 갤러리 + 생태계 확장 (v2.0) ### 18-C-1: 플러그인 갤러리 ```csharp // 플러그인 매니페스트 public record PluginManifest { [JsonPropertyName("id")] public string Id { get; init; } [JsonPropertyName("name")] public string Name { get; init; } [JsonPropertyName("version")] public string Version { get; init; } [JsonPropertyName("description")] public string Description { get; init; } [JsonPropertyName("type")] public PluginType Type { get; init; } // Skill | Tool | Theme [JsonPropertyName("entry")] public string EntryFile { get; init; } // zip 내부 진입점 파일 [JsonPropertyName("min_app_version")] public string MinAppVersion { get; init; } } // 플러그인 설치 서비스 public class PluginInstallService { // zip 파일에서 설치 public async Task InstallFromZipAsync(string zipPath, CancellationToken ct) { // 1. zip 압축 해제 → 임시 디렉토리 // 2. manifest.json 파싱·검증 // 3. %APPDATA%\AxCopilot\plugins\\ 에 복사 // 4. 플러그인 타입에 따라 등록: // - Skill → SkillLoaderService.Reload() // - Tool → ToolRegistry.Register() } // 로컬 레지스트리 갱신 (NAS/Git 기반) public async Task RefreshRegistryAsync(string registryPath, CancellationToken ct) { ... } // 설치된 플러그인 목록 public IReadOnlyList GetInstalled() { ... } // 플러그인 제거 public Task UninstallAsync(string pluginId, CancellationToken ct) { ... } } ``` --- ## Phase L3 — 런처 에코시스템 (v2.0) | # | 기능 | 연결 Phase | 핵심 클래스 | |---|------|-----------|-----------| | L3-1 | 플러그인 갤러리 | 18-C1 | `PluginGalleryViewModel`, `PluginInstallService` | | L3-2 | 웹 검색 AI 요약 | 18-C5 | `WebSearchSummaryHandler`, `ContentExtractor` | | L3-3 | AI 스니펫 | 18-C2 | `AiSnippetHandler`, `SnippetTemplateService` | | L3-4 | 파라미터 퀵링크 | 18-C3 | `QuickLinkHandler`, `UrlTemplateEngine` | | L3-7 | 알림 센터 통합 | 18-A4 | `NotificationCenterService` (Windows Toast API) | --- ## 구현 의존성 그래프 ``` Phase 17 구현 순서: [17-UI-1~4] AgentWindow 골격 + ViewModel ↓ [17-UI-5~6] Sidebar + InlineSettings (SettingsWindow 이전) ↓ [17-B-1~2] TaskState + EventLog (인프라 먼저) ↓ [17-C-1~5] Hook 이벤트/타입 확장 (TaskState 이용) ↓ [17-D-1~5] Skill fork/paths (Hook 인프라 이용) ↓ [17-E-1~4] @include + 경로 규칙 (Skill 시스템과 병렬) ↓ [17-F-1~5] Permission Chain + acceptEdits ↓ [17-A-1~4] Reflexion (모든 인프라 완성 후) ↓ [17-G-1~2] MultiFileDiff + AutoContext (독립, 병렬 가능) Phase 18 구현 순서: [18-A-2] WorktreeManager (git 인프라) ↓ [18-A-4] BackgroundAgentService ↓ [18-A-3] DelegateAgentTool (WorktreeManager + BackgroundAgent 이용) ↓ [18-A-1] CoordinatorAgent (DelegateTool 이용) ↓ [18-A-5] AgentTypeMemoryRepository ↓ [18-B] ReplayService (EventLog Phase 17-B 이용) ↓ [18-C] 플러그인 갤러리 (독립) ``` --- ## 버전별 출시 계획 | 버전 | 코드명 | 포함 Phase | 핵심 신규 클래스 수 | |------|--------|-----------|-----------------| | **v1.8.0** | 에이전트 인프라 | 17-UI, 17-A~G | ~35개 신규 클래스 ✅ 완료 | | **v1.8.1** | 인프라 안정화 | 버그픽스 + 17-* 마이너 | — | | **v2.0** | 에이전트 팀 | 18-A~C, L3 | ~25개 신규 클래스 ✅ 완료 | | **v2.1** | CC 동등성 | 19-A~G (Claude Code 갭 해소) | ~30개 신규 클래스 | | **v2.2** | SDK 프로토콜 | 20-A~C (임베딩 + 외부 연동) | ~15개 신규 클래스 | | **v2.3** | AX Agent UI 전면 개편 | 21 (Claude.ai+Codex 레이아웃) | UI 계층 60% 재작성 | | **v3.0** | 크로스플랫폼 | LP-1~3 (Avalonia) | UI 계층 40% 재작성 | --- ## Phase 19 — Claude Code 동등성 달성 (v2.1) > **목표**: `docs/claude-code-docs-main/` 문서 분석 결과 발견된 AX Agent 갭을 해소하여 Claude Code 수준의 에이전트 인프라를 달성한다. ### 갭 분석 요약 | 기능 영역 | Claude Code | AX Agent 현황 | 우선순위 Phase | |----------|------------|--------------|--------------| | 계층형 메모리 (4층) | 시스템/유저/프로젝트/로컬 + @include | AX.md 단일 파일 | 19-A | | 권한 패턴 매칭 | `git *`, `npm run *` 패턴 + 복합 명령 분해 | 경로 기반 Allow/Deny만 | 19-B | | 훅 이벤트 완성 | 14+ 이벤트, Shell/HTTP/LLM/Agent 타입 | 일부 이벤트, Shell 위주 | 19-C | | 스킬 인라인 실행 | `!``cmd`` ` 실행 치환, 유저급 스킬, 네임스페이스 | 프리셋 파일 치환 없음 | 19-D | | 세션 재개/포크 | `--resume`, `forkSession()`, 태깅 | JSONL 리플레이만 | 19-E | | 출력 예산 관리 | `maxResultSizeChars`, 임시파일 스필오버 | 고정 8000자 절단 | 19-F | | /init 명령 | 프로젝트 분석 → AX.md 자동 생성 | 없음 | 19-G | --- ## Phase 19-A — 계층형 메모리 시스템 (v2.1) > **목표**: AX.md 단일 파일 → 4계층 캐스케이드 메모리 시스템. 경로 기반 규칙 파일 지원. ### 4계층 메모리 아키텍처 | 계층 | 경로 | 범위 | 우선순위 | |------|------|------|---------| | L1 시스템 | `%APPDATA%\AxCopilot\system.md` | 앱 전역 관리자 규칙 | 최하위 | | L2 유저 | `%USERPROFILE%\.axcopilot\AX.md` | 유저 전체 선호 | ↑ | | L3 프로젝트 | `\AX.md` + `\.axcopilot\rules\*.md` | 팀 공유 규칙 | ↑ | | L4 로컬 | `\AX.local.md` | 개인 프로젝트 오버라이드 (gitignore) | 최상위 | ### 핵심 클래스 ```csharp // HierarchicalMemoryLoader.cs public class HierarchicalMemoryLoader { /// CWD에서 루트까지 탐색하며 계층별 메모리 파일 수집. public async Task LoadAsync(string workFolder, CancellationToken ct) { var layers = new List(); layers.Add(await LoadSystemLayerAsync(ct)); // L1 layers.Add(await LoadUserLayerAsync(ct)); // L2 layers.AddRange(await LoadProjectLayersAsync(workFolder, ct)); // L3 (CWD→루트) layers.Add(await LoadLocalLayerAsync(workFolder, ct)); // L4 return new MemoryContext(layers); } /// @include 지시문 처리 (최대 5단계 중첩, 순환 참조 감지). private async Task ResolveIncludesAsync(string content, string basePath, HashSet visited, int depth, CancellationToken ct) { ... } } // PathScopedRuleLoader.cs — .axcopilot/rules/*.md 파일 로드 public class PathScopedRuleLoader { /// 현재 편집 파일 경로와 rules/*.md의 paths 패턴을 매칭하여 주입할 규칙 반환. public IReadOnlyList GetApplicableRules(string editingFilePath, IReadOnlyList ruleFiles) { ... } } // MemoryContext.cs public record MemoryLayer(string Source, string Content, MemoryScope Scope); public enum MemoryScope { System, User, Project, Local } public record MemoryContext(IReadOnlyList Layers) { /// 모든 레이어를 우선순위 순서로 조합한 최종 컨텍스트 문자열. public string Compose() => string.Join("\n\n", Layers.Select(l => l.Content)); } ``` ### AgentLoopService 통합 ```csharp // AgentLoopService.cs — 컨텍스트 조립 부분 교체 private async Task BuildSystemContextAsync(CancellationToken ct) { // 기존: 단일 AX.md 로드 // 변경: 계층형 로더 사용 var memCtx = await _memoryLoader.LoadAsync(_workFolder, ct); var pathRules = _ruleScopeLoader.GetApplicableRules(_currentEditingFile, _cachedRules); return memCtx.Compose() + "\n\n" + string.Join("\n\n", pathRules); } ``` ### 슬래시 명령 추가 ```csharp // /memory 슬래시 명령 → ChatWindow에서 메모리 파일 인라인 에디터 열기 // 현재 로드된 모든 메모리 레이어 표시 + 편집 버튼 public class MemorySlashCommand : ISlashCommand { public string Name => "memory"; public string Description => "로드된 메모리 파일 목록 및 편집"; public Task ExecuteAsync(string args, IChatContext ctx); } ``` ### 설정 항목 ```csharp // AppSettings.LlmSettings public bool EnableHierarchicalMemory { get; set; } = true; public string UserMemoryPath { get; set; } = ""; // 기본: %USERPROFILE%\.axcopilot\AX.md public int MaxIncludeDepth { get; set; } = 5; ``` --- ## Phase 19-B — 권한 패턴 매칭 + 복합 명령 분해 (v2.1) > **목표**: 현재 경로 기반 Allow/Deny → 명령 패턴 매칭 + `&&`/`||`/`;`/`|` 복합 명령 서브 커맨드별 검증. ### 핵심 클래스 ```csharp // CommandPatternMatcher.cs public class CommandPatternMatcher { /// 패턴(glob) 기반 명령 허용 여부 검사. /// 예: "git *" → git commit, git push 모두 허용 /// "npm run *" → npm run build, npm run test 허용 /// "rm -rf *" → 금지 리스트에 명시 가능 public PermissionDecision Match(string command, IReadOnlyList rules) { ... } } // CompoundCommandParser.cs public class CompoundCommandParser { private static readonly string[] Operators = ["&&", "||", ";", "|"]; /// 복합 명령을 분해하여 서브 커맨드 목록 반환. /// 예: "git add . && git commit -m 'msg'" → ["git add .", "git commit -m 'msg'"] public IReadOnlyList Parse(string command) { ... } /// 복합 명령의 모든 서브 커맨드에 대해 권한 검사. /// 하나라도 Deny이면 전체 Deny. public PermissionDecision CheckAll(string compoundCommand, CommandPatternMatcher matcher, IReadOnlyList rules) { ... } } // 항상 차단되는 명령 목록 (하드코딩) public static class AlwaysBlockedCommands { public static readonly string[] Patterns = [ "rd /s", "rmdir /s", // 재귀 폴더 삭제 "format *", // 드라이브 포맷 "reg delete *", // 레지스트리 삭제 "del /f /s /q *", // 강제 재귀 파일 삭제 "netsh * delete *", // 방화벽 규칙 삭제 "schtasks /delete *", // 작업 스케줄러 삭제 ]; } ``` ### PermissionRuleEntry 확장 ```csharp // 기존 PermissionRuleEntry에 패턴 지원 추가 public class PermissionRuleEntry { [JsonPropertyName("tool")] public string Tool { get; set; } = ""; // 도구명 또는 "bash:*" [JsonPropertyName("pattern")] public string Pattern { get; set; } = "*"; // glob 패턴 (bash 명령에만 적용) [JsonPropertyName("decision")] public string Decision { get; set; } = "allow"; // "allow" | "deny" [JsonPropertyName("reason")] public string Reason { get; set; } = ""; // 사용자 표시 이유 } ``` ### 도구 가시성 필터링 ```csharp // AgentLoopService — LLM 호출 전 비활성 도구를 tools 목록에서 제거 // 현재: 도구 이름은 넘기되 실행 시 차단 // 개선: 아예 스키마 자체를 LLM에 전달하지 않음 → 환각 방지 private IReadOnlyList FilterVisibleTools(IReadOnlyList allTools) { return allTools.Where(t => !_settings.DisabledTools.Contains(t.Name) && _permissionSystem.IsToolVisible(t.Name)).ToList(); } ``` --- ## Phase 19-C — 훅 이벤트 완성 (v2.1) > **목표**: 현재 부분 구현된 훅 시스템 → Claude Code의 14+ 이벤트 + 4가지 훅 타입 완성. ### 추가 훅 이벤트 ```csharp // HookTypes.cs 확장 public enum AgentHookEvent { // 기존 PreToolUse, PostToolUse, Stop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, PostCompact, // 신규 추가 PostToolUseFailure, // 도구 실행 실패 시 SubAgentStart, // 서브에이전트 시작 SubAgentStop, // 서브에이전트 완료 PermissionRequest, // 승인 다이얼로그 직전 PermissionDenied, // 거부 후 Notification, // UI 알림 발생 시 CwdChanged, // 작업 폴더 변경 시 FileChanged, // 감시 파일 변경 시 ConfigChange, // 설정 파일 변경 시 } ``` ### 훅 타입 완성 (4종) ```csharp // ExtendedHookRunner.cs 확장 public enum HookCommandType { Shell, Http, Llm, Agent } public class HookEntry { public AgentHookEvent Event { get; set; } public string? Matcher { get; set; } // tool_name 또는 notification_type 패턴 public HookCommandType Type { get; set; } = HookCommandType.Shell; // Shell 훅 public string? Command { get; set; } public int Timeout { get; set; } = 30; public bool Async { get; set; } = false; public bool AsyncRewake { get; set; } = false; // 완료 시 에이전트 재개 // HTTP 훅 public string? Url { get; set; } public Dictionary Headers { get; set; } = new(); // LLM 훅 (검증용 Claude 호출) public string? LlmPrompt { get; set; } public string? LlmModel { get; set; } // Agent 훅 (풀 에이전트 루프) public string? AgentPrompt { get; set; } public string[]? AgentTools { get; set; } } // Exit code semantics // 0 = 성공, 계속 // 2 = 차단/주입 (stderr → Claude에게 전달, 동작 차단 또는 루프 계속) // 기타 = stderr → 사용자에만 표시, 계속 // $AX_ENV_FILE: CwdChanged 훅에서 환경변수 주입 // 예: echo "AX_PROJECT_TYPE=rust" >> $AX_ENV_FILE ``` ### PreToolUse 입력 수정 기능 ```csharp // 훅이 도구 입력을 수정할 수 있도록 허용 public record HookResult { public bool Continue { get; init; } = true; public string? Reason { get; init; } public string? SystemMessage { get; init; } public JsonElement? UpdatedInput { get; init; } // PreToolUse에서 도구 파라미터 수정 public bool SuppressOutput { get; init; } = false; } ``` --- ## Phase 19-D — 스킬 시스템 완성 (v2.1) > **목표**: 스킬 파일의 `!``cmd`` ` 인라인 실행, 유저급 스킬, 네임스페이스, 스킬별 모델 오버라이드 완성. ### 스킬 인라인 명령 실행 ```csharp // SkillService.cs 확장 // 스킬 파일 내 !`command` 블록을 호출 시점에 실행하고 출력으로 치환 public class SkillInlineCommandProcessor { private static readonly Regex InlineCmd = new(@"!\`([^`]+)\`", RegexOptions.Compiled); /// 스킬 본문의 !`cmd` 블록을 실행하고 결과로 치환합니다. /// 예: !`git log --oneline -10` → 최근 10개 커밋 목록으로 치환 public async Task ProcessAsync(string skillContent, string workFolder, CancellationToken ct) { foreach (Match m in InlineCmd.Matches(skillContent)) { var cmd = m.Groups[1].Value; var output = await ExecuteCommandAsync(cmd, workFolder, ct); skillContent = skillContent.Replace(m.Value, output); } return skillContent; } } ``` ### 스킬 프론트매터 확장 ```yaml --- description: 한 줄 설명 (/skills 목록에 표시) argument-hint: "[파일명] [옵션]" # 자동완성 힌트 allowed-tools: [file_read, grep] # 허용 도구 제한 model: claude-haiku-4-5-20251001 # 스킬별 모델 오버라이드 user-invocable: false # /skills 목록 숨김 (Claude만 사용) context: fork # 격리 서브에이전트로 실행 paths: "**/*.cs" # 이 파일 편집 시 자동 활성화 hooks: # 스킬 전용 훅 PostToolUse: - command: "dotnet build" --- ``` ### 유저급 스킬 경로 ``` %USERPROFILE%\.axcopilot\skills\\SKILL.md # 유저 전역 \.axcopilot\skills\\SKILL.md # 프로젝트 \.axcopilot\skills\\\SKILL.md # 네임스페이스 (/:) ``` ### 스킬 네임스페이스 ```csharp // /database:migrate → .axcopilot/skills/database/migrate/SKILL.md // /test:unit → .axcopilot/skills/test/unit/SKILL.md public class SkillNamespaceResolver { public string? ResolveFilePath(string slashCommand, string workFolder) { ... } // "database:migrate" → "/.axcopilot/skills/database/migrate/SKILL.md" } ``` --- ## Phase 19-E — 세션 재개 + 포크 + 태깅 (v2.1) > **목표**: JSONL 리플레이(관찰용) → 실제 세션 재개 + 분기 지점 포크. ### 세션 관리 API ```csharp // AgentSessionManager.cs public class AgentSessionManager { /// 이전 세션을 재개합니다. /// 메모리 파일은 재개 시점 기준으로 재발견합니다. public Task ResumeAsync(string sessionId, CancellationToken ct) { ... } /// 현재 세션의 특정 메시지 인덱스 이후를 분기합니다. /// 원본 세션은 보존됩니다. public Task ForkAsync(string sessionId, int messageIndex, CancellationToken ct) { ... } /// 세션에 태그를 붙입니다 (즐겨찾기, 분류 등). public Task TagAsync(string sessionId, string tag, CancellationToken ct) { ... } /// 세션 이름을 변경합니다. public Task RenameAsync(string sessionId, string newName, CancellationToken ct) { ... } /// 조건에 맞는 세션 목록 반환. public Task> ListAsync( string? tag = null, string? tabType = null, CancellationToken ct = default) { ... } } public record AgentSession(string Id, string Name, string TabType, List Messages); public record AgentSessionMeta(string Id, string Name, string TabType, DateTime CreatedAt, DateTime LastAt, string? Tag, int MessageCount); ``` ### 에이전트 타입별 영속 메모리 경로 ``` %USERPROFILE%\.axcopilot\agent-memory\\MEMORY.md # 유저 범위 \.axcopilot\agent-memory\\MEMORY.md # 프로젝트 범위 \.axcopilot\agent-memory-local\\MEMORY.md # 로컬 범위 ``` ```csharp // AgentTypeMemoryRepository.cs 확장 public class AgentTypeMemoryRepository { public enum MemoryScope { User, Project, Local } public async Task GetAsync(string agentType, string workFolder, MemoryScope scope = MemoryScope.Project, CancellationToken ct = default) { ... } public async Task SaveAsync(string agentType, string workFolder, string content, MemoryScope scope = MemoryScope.Project, CancellationToken ct = default) { ... } } ``` --- ## Phase 19-F — 출력 예산 + 컨텍스트 최적화 (v2.1) > **목표**: 고정 8000자 절단 → 도구별 출력 예산 + 대형 출력 임시파일 스필오버. 컨텍스트 조립 메모이제이션. ### 출력 예산 관리 ```csharp // ToolOutputBudget.cs public static class ToolOutputBudget { // 도구별 기본 최대 출력 크기 private static readonly Dictionary DefaultBudgets = new() { ["file_read"] = 12_000, ["grep"] = 8_000, ["bash"] = 10_000, ["git_tool"] = 6_000, ["http_tool"] = 8_000, ["directory_list"] = 4_000, ["multi_read"] = 20_000, // 기타 도구: 기본 6000자 }; public static int GetBudget(string toolName) => DefaultBudgets.GetValueOrDefault(toolName, 6_000); } // ToolOutputSpillover.cs public class ToolOutputSpillover { private readonly string _tempDir = Path.Combine(Path.GetTempPath(), "AxCopilot", "spill"); /// 출력이 예산 초과 시 임시파일에 저장하고 경로+미리보기 반환. public async Task ProcessAsync(string toolName, string output, CancellationToken ct) { var budget = ToolOutputBudget.GetBudget(toolName); if (output.Length <= budget) return output; var path = Path.Combine(_tempDir, $"{Guid.NewGuid():N}.txt"); Directory.CreateDirectory(_tempDir); await File.WriteAllTextAsync(path, output, ct); var preview = output[..Math.Min(500, output.Length)]; return $"[출력이 {output.Length:N0}자로 예산({budget:N0}자)을 초과하여 임시파일에 저장됨]\n" + $"파일 경로: {path}\n미리보기:\n{preview}\n..."; } } ``` ### 컨텍스트 조립 메모이제이션 ```csharp // ContextAssemblyCache.cs public class ContextAssemblyCache { private string? _cachedGitState; private DateTime _gitStateCachedAt; private MemoryContext? _cachedMemory; private string? _cachedWorkFolder; /// 세션 내 git 상태는 5분 캐시. public async Task GetGitStateAsync(string workFolder, CancellationToken ct) { ... } /// 메모리 파일은 세션 시작 시 한 번만 로드 (변경 감지 시 갱신). public async Task GetMemoryContextAsync(string workFolder, CancellationToken ct) { ... } /// 파일 변경 감지 시 캐시 무효화. public void Invalidate() { _cachedMemory = null; _cachedGitState = null; } } ``` --- ## Phase 19-G — /init 슬래시 명령 (v2.1) > **목표**: 프로젝트 분석 → AX.md 자동 생성 + 스킬/훅 초기 설정 제안. ### 핵심 클래스 ```csharp // ProjectInitAnalyzer.cs public class ProjectInitAnalyzer { /// /// 프로젝트 구조를 분석하여 AX.md 초안을 생성합니다. /// - 언어/프레임워크 탐지 (*.csproj, package.json, Cargo.toml 등) /// - 테스트 프레임워크 탐지 /// - CI/CD 설정 탐지 (.github/workflows 등) /// - 디렉터리 구조 요약 /// - 기존 README.md 요약 (LLM 호출) /// public async Task AnalyzeAsync(string workFolder, CancellationToken ct) { ... } /// 분석 결과를 바탕으로 AX.md 초안 생성. public async Task GenerateAxMdAsync(ProjectAnalysis analysis, LlmService llm, CancellationToken ct) { ... } } public record ProjectAnalysis( string Language, string Framework, string? TestFramework, string? CiSystem, IReadOnlyList KeyDirectories, string? ReadmeSummary); // /init 슬래시 명령 public class InitSlashCommand : ISlashCommand { public string Name => "init"; public string Description => "프로젝트를 분석하여 AX.md를 생성합니다"; public async Task ExecuteAsync(string args, IChatContext ctx) { // 1. 분석 실행 + 진행 상황 스트리밍 // 2. 생성된 AX.md 미리보기 표시 // 3. 사용자 확인 후 파일 저장 // 4. 필요 시 추천 스킬 제안 } } ``` ### 지원 언어/프레임워크 탐지 | 탐지 파일 | 언어 | 프레임워크 | |----------|------|----------| | `*.csproj`, `*.sln` | C# | .NET, ASP.NET, WPF | | `package.json` | JavaScript/TypeScript | React, Vue, Next.js | | `Cargo.toml` | Rust | — | | `pyproject.toml`, `requirements.txt` | Python | Django, FastAPI, Flask | | `pom.xml`, `build.gradle` | Java/Kotlin | Spring | | `go.mod` | Go | — | --- ## Phase 20 — SDK 제어 프로토콜 (v2.2) > **목표**: AX Agent를 외부 툴/스크립트에서 임베드 가능한 JSON 스트리밍 프로토콜로 노출. | # | 기능 | 핵심 클래스 | |---|------|-----------| | 20-A | 양방향 JSON 스트리밍 서버 | `AgentSdkServer`, `SdkControlRequest`, `SdkMessage` | | 20-B | 커스텀 에이전트 정의 (세션별) | `CustomAgentDefinition`, `AgentTypeRegistry` | | 20-C | SDK 훅 콜백 (외부 권한 처리) | `SdkHookCallbackHandler`, `SdkPermissionDelegate` | ```csharp // SdkControlRequest 타입 public enum SdkControlRequestType { Initialize, // 세션 설정, 커스텀 에이전트 정의, 훅 등록 SetPermissionMode, // 권한 모드 변경 SetModel, // 모델 전환 Interrupt, // 현재 턴 취소 CanUseTool, // 권한 처리기 응답 GetContextUsage, // 컨텍스트 사용량 조회 RewindFiles, // 메시지 이후 파일 변경 되돌리기 HookCallback, // SDK측 훅 이벤트 전달 } // SdkMessage (스트리밍 출력) public enum SdkMessageType { AssistantToken, // 스트리밍 텍스트 ToolProgress, // 도구 실행 진행 ToolResult, // 도구 결과 SessionResult, // 최종 결과 HookEvent, // 훅 이벤트 (SDK 처리 요청) Notification, // 알림 } ``` --- ## Phase 21 — AX Agent UI 전면 개편 (v2.3) > **목표**: CLAUDE.md Section 13에 명시된 Claude.ai + Codex 스타일 레이아웃 전면 구현. | # | 기능 | 핵심 컴포넌트 | |---|------|------------| | 21-1 | 3-Pane 레이아웃 골격 | `ChatWindow.xaml` 리팩터링 | | 21-2 | 좌측 사이드바 | `AgentSidebarView` (탭 세그먼트 + 프리셋 + 이력) | | 21-3 | 세션 헤더 바 | `AgentSessionHeaderBar` (모델/Plan/권한/도구 칩) | | 21-4 | 우측 설정 패널 | `AgentSettingsPanel` (SettingsWindow AX Agent 탭 완전 대체) | | 21-5 | 고도화 입력 영역 | `AgentInputArea` (@멘션, /스킬, 하단 칩 열) | | 21-6 | 메시지 버블 개선 | Claude.ai 스타일 (AI=배경 없음, 도구블록=접히는 Border) | | 21-7 | SettingsWindow 정리 | AX Agent 탭 제거, 전역 설정만 유지 | --- ## Phase 22 — 슬래시 명령 체계 완성 (v2.1) ✅ 완료 > **목표**: Claude Code의 16종 슬래시 명령을 AX Agent에 구현. `/init`(19-G)을 제외한 15종 명령 추가. > 기존 스킬 기반 `/` 호출과 공존하되, 내장 명령은 스킬보다 우선 처리. > > **구현 완료** (2026-04-03): ISlashCommand 인터페이스, SlashCommandRegistry, InputRouter, 13종 명령 (/compact, /clear, /memory, /model, /plan, /commit, /review, /mcp, /permissions, /hooks, /config, /skills, /help) + SlashAutoCompleteProvider. ### 22-1: ISlashCommand 인터페이스 + 레지스트리 ```csharp // 슬래시 명령 인터페이스 (Command 패턴) public interface ISlashCommand { string Name { get; } // "compact", "memory" 등 string[] Aliases { get; } // "/settings" → "/config" 별칭 string Description { get; } string? ArgumentHint { get; } // 자동완성 힌트 bool RequiresActiveSession { get; } // 세션 없이도 사용 가능 여부 Task ExecuteAsync(string arguments, IAgentChatContext context, CancellationToken ct); } // 채팅 컨텍스트 인터페이스 (DIP: 명령이 UI에 직접 의존하지 않음) public interface IAgentChatContext { AgentLoopService AgentLoop { get; } SettingsService Settings { get; } SkillLoaderService SkillLoader { get; } HookRunnerService HookRunner { get; } McpManagerService McpManager { get; } AgentSessionRepository SessionRepo { get; } LlmService Llm { get; } string ActiveTab { get; } // "Chat" | "Cowork" | "Code" string WorkFolder { get; } // UI 상호작용 (ViewModel 위임) Task SendSystemMessageAsync(string message); Task PromptUserAsync(string question); // 사용자 입력 대기 Task ShowToastAsync(string message); void ClearMessages(); void UpdateSessionHeader(); // 모델/모드 변경 시 헤더 갱신 } // 슬래시 명령 레지스트리 (Factory + Registry) public class SlashCommandRegistry { private readonly Dictionary _commands = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary _aliases = new(StringComparer.OrdinalIgnoreCase); public void Register(ISlashCommand command) { _commands[command.Name] = command; foreach (var alias in command.Aliases) _aliases[alias] = command.Name; } public ISlashCommand? Resolve(string input) { // "/compact focus on API changes" → name="compact", args="focus on API changes" var name = input.Split(' ', 2)[0].TrimStart('/'); if (_commands.TryGetValue(name, out var cmd)) return cmd; if (_aliases.TryGetValue(name, out var canonical)) return _commands.GetValueOrDefault(canonical); return null; // null → 스킬 검색으로 폴백 } public IReadOnlyList GetAll() => _commands.Values.ToList(); // 기본 명령 등록 public static SlashCommandRegistry CreateDefault(IAgentChatContext ctx) { var reg = new SlashCommandRegistry(); reg.Register(new CompactCommand()); reg.Register(new ClearCommand()); reg.Register(new MemoryCommand()); reg.Register(new ConfigCommand()); reg.Register(new HooksCommand()); reg.Register(new McpCommand()); reg.Register(new PermissionsCommand()); reg.Register(new ModelCommand()); reg.Register(new PlanCommand()); reg.Register(new CommitCommand()); reg.Register(new ReviewCommand()); reg.Register(new SkillsCommand()); reg.Register(new HelpCommand()); reg.Register(new InitCommand()); // 19-G에서 구현 return reg; } } ``` ### 22-2: /compact — 컨텍스트 압축 ```csharp public class CompactCommand : ISlashCommand { public string Name => "compact"; public string[] Aliases => Array.Empty(); public string Description => "대화 기록을 요약하여 컨텍스트 사용량을 줄입니다"; public string? ArgumentHint => "[요약 지시사항]"; public bool RequiresActiveSession => true; public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct) { // 1. PreCompact 훅 실행 var hookResult = await ctx.HookRunner.RunAsync(HookEvent.PreCompact, new HookContext { Event = HookEvent.PreCompact, SessionId = ctx.AgentLoop.CurrentSessionId, }, ct); if (hookResult.AnyBlocked) { await ctx.SendSystemMessageAsync($"[컴팩션 차단됨] {hookResult.CombinedContext}"); return; } // 2. 현재 대화 메시지를 LLM에게 요약 요청 var messages = ctx.AgentLoop.GetCurrentMessages(); var summaryPrompt = string.IsNullOrEmpty(arguments) ? "위 대화를 핵심 결정사항, 작업 파일, 남은 작업 중심으로 간결하게 요약하세요." : $"위 대화를 다음 지시에 따라 요약하세요: {arguments}"; var summary = await ctx.Llm.GenerateAsync( BuildCompactPrompt(messages, summaryPrompt), ct); // 3. TaskState 갱신 (Working Memory 보존) if (ctx.AgentLoop.TaskState != null) await ctx.AgentLoop.TaskState.UpdateContextSummaryAsync(summary); // 4. 메시지 목록을 요약 메시지 1개로 교체 ctx.AgentLoop.ReplaceMessagesWithSummary(summary); // 5. PostCompact 훅 실행 await ctx.HookRunner.RunAsync(HookEvent.PostCompact, new HookContext { Event = HookEvent.PostCompact, }, ct); await ctx.SendSystemMessageAsync($"[컴팩션 완료] 대화가 요약되었습니다. 컨텍스트 사용량이 줄었습니다."); } private string BuildCompactPrompt(IReadOnlyList messages, string instruction) { var sb = new StringBuilder(); sb.AppendLine("# 대화 기록"); foreach (var msg in messages) sb.AppendLine($"[{msg.Role}] {msg.Content[..Math.Min(msg.Content.Length, 500)]}"); sb.AppendLine(); sb.AppendLine(instruction); return sb.ToString(); } } ``` ### 22-3: /clear — 대화 초기화 ```csharp public class ClearCommand : ISlashCommand { public string Name => "clear"; public string[] Aliases => new[] { "reset", "new" }; public string Description => "대화 기록을 지우고 새 세션을 시작합니다"; public string? ArgumentHint => null; public bool RequiresActiveSession => false; public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct) { // 1. 현재 세션 저장 await ctx.SessionRepo.SaveAsync(ctx.AgentLoop.CurrentSession); // 2. UI 메시지 목록 초기화 ctx.ClearMessages(); // 3. 새 세션 시작 await ctx.AgentLoop.StartNewSessionAsync(ctx.ActiveTab, ct); // 4. SessionStart 훅 발화 (source: "clear") await ctx.HookRunner.RunAsync(HookEvent.SessionStart, new HookContext { Event = HookEvent.SessionStart, SessionId = ctx.AgentLoop.CurrentSessionId, // source = "clear" }, ct); await ctx.SendSystemMessageAsync("새 대화가 시작되었습니다."); } } ``` ### 22-4: /memory — 메모리 파일 편집 ```csharp public class MemoryCommand : ISlashCommand { public string Name => "memory"; public string[] Aliases => Array.Empty(); public string Description => "AX.md 메모리 파일을 편집합니다 (전역/프로젝트/로컬)"; public string? ArgumentHint => "[global|project|local]"; public bool RequiresActiveSession => false; public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct) { // 범위 결정 var scope = arguments.Trim().ToLowerInvariant() switch { "global" or "user" => MemoryScope.User, "local" => MemoryScope.Local, _ => MemoryScope.Project, // 기본값 }; // 파일 경로 결정 var path = scope switch { MemoryScope.User => Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".axcopilot", "AX.md"), MemoryScope.Local => Path.Combine(ctx.WorkFolder, "AX.local.md"), _ => Path.Combine(ctx.WorkFolder, "AX.md"), }; // 파일이 없으면 템플릿 생성 if (!File.Exists(path)) { Directory.CreateDirectory(Path.GetDirectoryName(path)!); await File.WriteAllTextAsync(path, $"# AX Copilot 프로젝트 메모리 ({scope})\n\n여기에 지시사항을 작성하세요.\n", ct); } // 외부 에디터 열기 (기본 텍스트 에디터) Process.Start(new ProcessStartInfo(path) { UseShellExecute = true }); await ctx.SendSystemMessageAsync( $"메모리 파일을 열었습니다: `{path}`\n편집 후 저장하면 다음 세션부터 반영됩니다."); } } public enum MemoryScope { User, Project, Local } ``` ### 22-5: /model — 세션 모델 전환 ```csharp public class ModelCommand : ISlashCommand { public string Name => "model"; public string[] Aliases => Array.Empty(); public string Description => "현재 세션의 AI 모델을 변경합니다"; public string? ArgumentHint => "[모델명]"; public bool RequiresActiveSession => true; public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct) { if (string.IsNullOrWhiteSpace(arguments)) { // 현재 모델 + 사용 가능한 모델 목록 표시 var current = ctx.AgentLoop.CurrentModel; var available = ctx.Settings.Settings.Llm.RegisteredModels .Select(m => m.DisplayName).ToList(); await ctx.SendSystemMessageAsync( $"현재 모델: **{current}**\n사용 가능: {string.Join(", ", available)}"); return; } // 모델 변경 var success = ctx.AgentLoop.TrySetModel(arguments.Trim()); if (success) { ctx.UpdateSessionHeader(); await ctx.SendSystemMessageAsync($"모델이 **{arguments.Trim()}**(으)로 변경되었습니다."); } else { await ctx.SendSystemMessageAsync($"모델 '{arguments.Trim()}'을(를) 찾을 수 없습니다."); } } } ``` ### 22-6: /plan — 플랜 모드 토글 ```csharp public class PlanCommand : ISlashCommand { public string Name => "plan"; public string[] Aliases => Array.Empty(); public string Description => "플랜 모드를 토글하거나 계획을 생성합니다"; public string? ArgumentHint => "[open|<계획 설명>]"; public bool RequiresActiveSession => true; public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct) { var arg = arguments.Trim().ToLowerInvariant(); if (arg == "open") { // 현재 계획 표시 var plan = ctx.AgentLoop.CurrentPlan; if (plan == null) await ctx.SendSystemMessageAsync("현재 활성 계획이 없습니다."); else await ctx.SendSystemMessageAsync($"## 현재 계획\n{plan}"); return; } if (string.IsNullOrEmpty(arg)) { // 플랜 모드 토글 var newMode = ctx.AgentLoop.TogglePlanMode(); ctx.UpdateSessionHeader(); await ctx.SendSystemMessageAsync( $"플랜 모드: **{newMode}** ({newMode switch { PlanMode.Off => "변경 사항을 바로 실행합니다", PlanMode.Always => "실행 전 항상 계획을 작성하고 승인을 기다립니다", PlanMode.Auto => "복잡한 작업에만 자동으로 계획을 작성합니다", _ => "" }})"); return; } // 주어진 설명으로 계획 생성 await ctx.AgentLoop.CreatePlanAsync(arg, ct); } } ``` ### 22-7: /commit — AI 커밋 메시지 ```csharp public class CommitCommand : ISlashCommand { public string Name => "commit"; public string[] Aliases => Array.Empty(); public string Description => "AI가 생성한 메시지로 git 커밋을 만듭니다"; public string? ArgumentHint => null; public bool RequiresActiveSession => false; public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct) { // 1. git status + git diff 확인 var statusResult = await ProcessHelper.RunAsync("git", "status --porcelain", ctx.WorkFolder, ct); if (string.IsNullOrWhiteSpace(statusResult.StdOut)) { await ctx.SendSystemMessageAsync("커밋할 변경 사항이 없습니다."); return; } var diffResult = await ProcessHelper.RunAsync("git", "diff --cached", ctx.WorkFolder, ct); var diffAll = string.IsNullOrWhiteSpace(diffResult.StdOut) ? (await ProcessHelper.RunAsync("git", "diff", ctx.WorkFolder, ct)).StdOut : diffResult.StdOut; // 2. LLM에게 커밋 메시지 생성 요청 var prompt = $""" 아래 git diff를 분석하고 간결한 커밋 메시지를 작성하세요. - 첫 줄: 50자 이내 제목 (무엇을 왜) - 빈 줄 후 본문: 변경 이유와 영향 (선택) - 비밀 파일(.env 등) 포함 여부 경고 ```diff {diffAll[..Math.Min(diffAll.Length, 8000)]} ``` git status: {statusResult.StdOut} """; var commitMsg = await ctx.Llm.GenerateAsync(prompt, ct); // 3. 사용자 확인 await ctx.SendSystemMessageAsync($"## 제안된 커밋 메시지\n```\n{commitMsg}\n```\n\n이 메시지로 커밋할까요? (yes/no)"); var answer = await ctx.PromptUserAsync("커밋 확인"); if (answer?.Trim().ToLowerInvariant() is not ("yes" or "y" or "ㅇ")) { await ctx.SendSystemMessageAsync("커밋이 취소되었습니다."); return; } // 4. git add + commit 실행 await ProcessHelper.RunAsync("git", "add -A", ctx.WorkFolder, ct); var commitResult = await ProcessHelper.RunAsync("git", $"commit -m \"{commitMsg.Replace("\"", "\\\"")}\"", ctx.WorkFolder, ct); await ctx.SendSystemMessageAsync( commitResult.ExitCode == 0 ? $"커밋 완료: {commitResult.StdOut}" : $"커밋 실패: {commitResult.StdErr}"); } } ``` ### 22-8: /mcp — MCP 서버 관리 ```csharp public class McpCommand : ISlashCommand { public string Name => "mcp"; public string[] Aliases => Array.Empty(); public string Description => "MCP 서버 상태 확인 및 활성화/비활성화"; public string? ArgumentHint => "[enable|disable [서버명]]"; public bool RequiresActiveSession => false; public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct) { var parts = arguments.Trim().Split(' ', 2, StringSplitOptions.RemoveEmptyEntries); var action = parts.Length > 0 ? parts[0].ToLowerInvariant() : ""; var serverName = parts.Length > 1 ? parts[1] : null; if (string.IsNullOrEmpty(action)) { // 상태 표시 var sb = new StringBuilder("## MCP 서버 상태\n"); foreach (var client in ctx.McpManager.GetAllClients()) { var status = client.IsConnected ? "🟢 연결됨" : "🔴 끊김"; sb.AppendLine($"- **{client.ServerName}** {status} ({client.Tools.Count}개 도구)"); } await ctx.SendSystemMessageAsync(sb.ToString()); return; } switch (action) { case "enable": if (serverName != null) await ctx.McpManager.EnableServerAsync(serverName, ct); else await ctx.McpManager.EnableAllAsync(ct); await ctx.SendSystemMessageAsync($"MCP 서버 활성화: {serverName ?? "전체"}"); break; case "disable": if (serverName != null) ctx.McpManager.DisableServer(serverName); else ctx.McpManager.DisableAll(); await ctx.SendSystemMessageAsync($"MCP 서버 비활성화: {serverName ?? "전체"}"); break; case "reconnect": if (serverName != null) await ctx.McpManager.ReconnectAsync(serverName, ct); await ctx.SendSystemMessageAsync($"MCP 서버 재연결: {serverName}"); break; default: await ctx.SendSystemMessageAsync("사용법: /mcp [enable|disable|reconnect] [서버명]"); break; } } } ``` ### 22-9: /permissions, /hooks, /config, /skills, /review, /help ```csharp // /permissions — 권한 규칙 관리 public class PermissionsCommand : ISlashCommand { public string Name => "permissions"; public string[] Aliases => new[] { "allowed-tools" }; public string Description => "도구 허용/차단 규칙을 관리합니다"; public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct) { // 현재 권한 모드 + allow/deny 규칙 목록 표시 // "allow Bash(git *)" / "deny Bash(rm -rf *)" 형식 인수 처리 var settings = ctx.Settings.Settings.Llm; var sb = new StringBuilder("## 권한 설정\n"); sb.AppendLine($"현재 모드: **{settings.PermissionMode}**\n"); sb.AppendLine("### 허용 규칙"); foreach (var rule in settings.AllowRules) sb.AppendLine($"- ✅ `{rule}`"); sb.AppendLine("\n### 차단 규칙"); foreach (var rule in settings.DenyRules) sb.AppendLine($"- ❌ `{rule}`"); await ctx.SendSystemMessageAsync(sb.ToString()); } } // /hooks — 훅 설정 표시 public class HooksCommand : ISlashCommand { public string Name => "hooks"; public string[] Aliases => Array.Empty(); public string Description => "활성화된 훅 설정을 표시합니다"; public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct) { var hooks = ctx.HookRunner.GetAllDefinitions(); var sb = new StringBuilder("## 활성 훅\n"); foreach (var (eventName, defs) in hooks) { sb.AppendLine($"### {eventName}"); foreach (var def in defs) { var matcher = string.IsNullOrEmpty(def.Matcher) ? "(전체)" : def.Matcher; sb.AppendLine($"- [{def.Type}] {matcher}: `{def.Command ?? def.Prompt ?? def.Url}`"); } } await ctx.SendSystemMessageAsync(sb.ToString()); } } // /config — 설정 열기 public class ConfigCommand : ISlashCommand { public string Name => "config"; public string[] Aliases => new[] { "settings" }; public string Description => "설정 화면을 엽니다"; public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct) { // 인라인 설정 패널 토글 (Phase 21 UI) 또는 SettingsWindow 열기 ctx.ToggleSettingsPanel(); } } // /skills — 스킬 목록 public class SkillsCommand : ISlashCommand { public string Name => "skills"; public string[] Aliases => Array.Empty(); public string Description => "사용 가능한 스킬 목록을 표시합니다"; public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct) { var skills = ctx.SkillLoader.GetUserInvocableSkills(); var sb = new StringBuilder("## 사용 가능한 스킬\n"); foreach (var skill in skills) { var hint = string.IsNullOrEmpty(skill.Frontmatter.ArgumentHint) ? "" : $" `{skill.Frontmatter.ArgumentHint}`"; sb.AppendLine($"- **/{skill.Name}**{hint} — {skill.Frontmatter.Description}"); } await ctx.SendSystemMessageAsync(sb.ToString()); } } // /review — PR 리뷰 public class ReviewCommand : ISlashCommand { public string Name => "review"; public string[] Aliases => Array.Empty(); public string Description => "AI 코드 리뷰를 실행합니다"; public string? ArgumentHint => "[PR 번호 또는 diff]"; public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct) { // git diff 가져오기 string diff; if (!string.IsNullOrWhiteSpace(arguments) && int.TryParse(arguments.Trim(), out var prNum)) { // GitHub PR diff 가져오기 (gh CLI 사용) var result = await ProcessHelper.RunAsync("gh", $"pr diff {prNum}", ctx.WorkFolder, ct); diff = result.StdOut; } else { // 현재 브랜치 diff var result = await ProcessHelper.RunAsync("git", "diff main...HEAD", ctx.WorkFolder, ct); diff = result.StdOut; } if (string.IsNullOrWhiteSpace(diff)) { await ctx.SendSystemMessageAsync("리뷰할 변경 사항이 없습니다."); return; } // LLM 코드 리뷰 요청 var prompt = $""" 아래 코드 변경 사항을 리뷰하세요: 1. 변경 사항 개요 2. 코드 품질 및 스타일 3. 잠재적 버그 또는 보안 문제 4. 개선 제안 ```diff {diff[..Math.Min(diff.Length, 12000)]} ``` """; var review = await ctx.Llm.GenerateAsync(prompt, ct); await ctx.SendSystemMessageAsync($"## 코드 리뷰\n{review}"); } } // /help — 도움말 public class HelpCommand : ISlashCommand { public string Name => "help"; public string[] Aliases => Array.Empty(); public string Description => "사용 가능한 명령 목록을 표시합니다"; public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct) { var registry = ctx.SlashCommandRegistry; var sb = new StringBuilder("## AX Agent 명령\n\n"); foreach (var cmd in registry.GetAll().OrderBy(c => c.Name)) { var hint = cmd.ArgumentHint != null ? $" {cmd.ArgumentHint}" : ""; var aliases = cmd.Aliases.Length > 0 ? $" (별칭: {string.Join(", ", cmd.Aliases.Select(a => $"/{a}"))})" : ""; sb.AppendLine($"- **/{cmd.Name}**{hint} — {cmd.Description}{aliases}"); } sb.AppendLine("\n스킬 명령은 `/skills`로 확인하세요."); await ctx.SendSystemMessageAsync(sb.ToString()); } } ``` ### 22-10: 입력 라우팅 통합 ```csharp // AgentLoopService 또는 AgentWindowViewModel에서 입력 처리 public class InputRouter { private readonly SlashCommandRegistry _slashCommands; private readonly SkillLoaderService _skillLoader; /// 사용자 입력을 슬래시 명령 → 스킬 → 일반 메시지 순으로 라우팅. public async Task RouteAsync(string input, IAgentChatContext ctx, CancellationToken ct) { if (!input.StartsWith("/")) return InputRouteResult.Message(input); // 일반 메시지 var spaceIdx = input.IndexOf(' '); var commandPart = spaceIdx > 0 ? input[..spaceIdx] : input; var argsPart = spaceIdx > 0 ? input[(spaceIdx + 1)..] : ""; // 1. 내장 슬래시 명령 확인 var cmd = _slashCommands.Resolve(commandPart); if (cmd != null) { await cmd.ExecuteAsync(argsPart, ctx, ct); return InputRouteResult.Handled; } // 2. 스킬 검색 var skillName = commandPart.TrimStart('/'); var skill = _skillLoader.FindByName(skillName); if (skill != null) return InputRouteResult.Skill(skill, argsPart); // 3. 미인식 → 일반 메시지로 전달 return InputRouteResult.Message(input); } } public record InputRouteResult(InputRouteType Type, string? Message = null, LoadedSkill? Skill = null, string? SkillArgs = null) { public static InputRouteResult Handled => new(InputRouteType.Handled); public static InputRouteResult Message(string msg) => new(InputRouteType.Message, msg); public static InputRouteResult Skill(LoadedSkill skill, string args) => new(InputRouteType.Skill, Skill: skill, SkillArgs: args); } public enum InputRouteType { Handled, Message, Skill } ``` --- ## Phase 19-D 확장 — 스킬 시스템 CC 동등성 보완 (v2.1) > **기존 17-D에 없는 3가지 핵심 기능 추가**: 인라인 셸 커맨드, 네임스페이싱, 명명된 인수 ### 19-D-EXT-1: 인라인 셸 커맨드 (`!`backtick`) ```csharp // SkillInlineCommandProcessor.cs public class SkillInlineCommandProcessor { // !`command` 패턴을 찾아 실행하고 출력으로 교체 private static readonly Regex InlineCmdPattern = new(@"!\`([^`]+)\`", RegexOptions.Compiled); /// /// 스킬 본문에서 !`command` 패턴을 찾아 셸 실행 결과로 교체합니다. /// 호출 시점: 스킬이 로드된 후, LLM에 프롬프트로 전달하기 직전. /// public async Task ProcessAsync(string skillBody, string workFolder, CancellationToken ct) { var matches = InlineCmdPattern.Matches(skillBody); if (matches.Count == 0) return skillBody; var result = skillBody; foreach (Match match in matches.Reverse()) // 뒤에서부터 교체 (인덱스 유지) { var command = match.Groups[1].Value; var output = await ExecuteCommandAsync(command, workFolder, ct); result = result.Remove(match.Index, match.Length) .Insert(match.Index, output); } return result; } private async Task ExecuteCommandAsync(string command, string workFolder, CancellationToken ct) { try { using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct); cts.CancelAfter(TimeSpan.FromSeconds(10)); // 인라인 명령 타임아웃 10초 var psi = new ProcessStartInfo { FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "cmd.exe" : "bash", Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? $"/c {command}" : $"-c \"{command.Replace("\"", "\\\"")}\"", WorkingDirectory = workFolder, UseShellExecute = false, RedirectStandardOutput = true, RedirectStandardError = true, CreateNoWindow = true, }; using var proc = Process.Start(psi); if (proc == null) return "[실행 실패]"; var stdout = await proc.StandardOutput.ReadToEndAsync(cts.Token); await proc.WaitForExitAsync(cts.Token); return stdout.Trim(); } catch (OperationCanceledException) { return "[타임아웃]"; } catch (Exception ex) { return $"[오류: {ex.Message}]"; } } } ``` ### 19-D-EXT-2: 스킬 네임스페이싱 (서브디렉토리 콜론 구분) ```csharp // SkillLoaderService 확장 public class SkillLoaderService { // 기존 메서드 + 네임스페이싱 로직 /// /// 스킬 디렉토리 구조를 콜론 구분 네임스페이스로 변환합니다. /// .claude/skills/database/migrate/SKILL.md → "database:migrate" /// .claude/skills/deploy/SKILL.md → "deploy" /// private string ResolveSkillName(string skillDir, string baseSkillsDir) { var relative = Path.GetRelativePath(baseSkillsDir, skillDir); // Windows 경로 구분자 → 콜론 return relative.Replace(Path.DirectorySeparatorChar, ':') .Replace(Path.AltDirectorySeparatorChar, ':'); } /// 콜론 구분 이름으로 스킬 검색 (부분 일치 지원). public LoadedSkill? FindByName(string name) { // 정확한 일치 var exact = _skills.FirstOrDefault(s => s.Name.Equals(name, StringComparison.OrdinalIgnoreCase)); if (exact != null) return exact; // 마지막 세그먼트 일치 (database:migrate → "migrate"로도 검색 가능) var lastSegment = name.Contains(':') ? name : name; return _skills.FirstOrDefault(s => s.Name.EndsWith($":{lastSegment}", StringComparison.OrdinalIgnoreCase) || s.Name.Equals(lastSegment, StringComparison.OrdinalIgnoreCase)); } // 디렉토리 스캔 시 재귀적으로 SKILL.md 검색 private void ScanSkillDirectory(string dir, string baseDir, List results) { var skillFile = Path.Combine(dir, "SKILL.md"); if (File.Exists(skillFile)) { var name = ResolveSkillName(dir, baseDir); var content = File.ReadAllText(skillFile); var (frontmatter, body) = ParseFrontmatter(content); results.Add(new LoadedSkill(name, frontmatter, body, skillFile)); } foreach (var subDir in Directory.GetDirectories(dir)) ScanSkillDirectory(subDir, baseDir, results); } } ``` ### 19-D-EXT-3: 명명된 인수 ($name 구문) ```csharp // SkillArgumentSubstitution.cs public static class SkillArgumentSubstitution { /// /// 스킬 본문에서 $ARGUMENTS와 명명된 인수를 치환합니다. /// - $ARGUMENTS → 전체 인수 문자열 /// - $name → 프론트매터 arguments: [name, directory] 기반 위치 매핑 /// - {0}, {1} → 인덱스 기반 인수 /// public static string Substitute(string body, SkillFrontmatter frontmatter, string userInput) { var result = body; // 1. $ARGUMENTS → 전체 인수 result = result.Replace("$ARGUMENTS", userInput); // 2. 명명된 인수 (프론트매터에 arguments가 정의된 경우) if (frontmatter.Arguments.Count > 0) { var parts = SplitArguments(userInput); for (var i = 0; i < frontmatter.Arguments.Count; i++) { var argName = frontmatter.Arguments[i]; var argValue = i < parts.Count ? parts[i] : ""; result = result.Replace($"${argName}", argValue); } } // 3. 인덱스 기반 인수 ({0}, {1}, ...) var indexParts = SplitArguments(userInput); for (var i = 0; i < indexParts.Count; i++) result = result.Replace($"{{{i}}}", indexParts[i]); return result; } private static List SplitArguments(string input) { // 따옴표로 감싼 인수 지원: "hello world" foo bar var parts = new List(); var current = new StringBuilder(); var inQuote = false; foreach (var ch in input) { if (ch == '"') { inQuote = !inQuote; continue; } if (ch == ' ' && !inQuote && current.Length > 0) { parts.Add(current.ToString()); current.Clear(); continue; } current.Append(ch); } if (current.Length > 0) parts.Add(current.ToString()); return parts; } } ``` ### 19-D-EXT-4: SkillExecutionPipeline 통합 ```csharp // 스킬 실행 파이프라인: 인수 치환 → 인라인 셸 → LLM 전달 public class SkillExecutionPipeline { private readonly SkillInlineCommandProcessor _inlineCmd; private readonly PathBasedSkillActivator _pathActivator; private readonly ForkContextSkillRunner _forkRunner; private readonly HookRunnerService _hookRunner; public async Task ExecuteAsync( LoadedSkill skill, string userInput, string workFolder, AgentLoopService agentLoop, CancellationToken ct) { // 1. PreSkillExecute 훅 await _hookRunner.RunAsync(HookEvent.PreSkillExecute, new HookContext { Event = HookEvent.PreSkillExecute, ToolName = skill.Name, UserMessage = userInput, }, ct); // 2. 인수 치환 ($ARGUMENTS, $name, {0}) var body = SkillArgumentSubstitution.Substitute( skill.Body, skill.Frontmatter, userInput); // 3. 인라인 셸 명령 실행 (!`command`) body = await _inlineCmd.ProcessAsync(body, workFolder, ct); // 4. 모델 오버라이드 확인 var model = skill.Frontmatter.Model; // 5. 컨텍스트 분기 여부 확인 if (skill.Frontmatter.Context == SkillContext.Fork) { var result = await _forkRunner.ExecuteInForkAsync(skill, body, ct); // PostSkillExecute 훅 await _hookRunner.RunAsync(HookEvent.PostSkillExecute, new HookContext { Event = HookEvent.PostSkillExecute, ToolName = skill.Name, ToolOutput = result.Output, }, ct); return result; } // 6. 메인 컨텍스트에서 실행 (body를 시스템 프롬프트에 주입) return new SkillResult { IsSuccess = true, Output = body, // AgentLoopService가 이를 프롬프트로 사용 }; } } ``` --- ## Phase 19-B 확장 — 복합 Bash 명령 파싱 (v2.1) > **기존 19-B에 누락된 복합 명령 파싱 상세 구현** ### 19-B-EXT-1: CompoundCommandParser ```csharp // 복합 Bash 명령을 서브커맨드로 분리하여 각각 권한 검사 public class CompoundCommandParser { // &&, ||, ;, | 로 분리 (따옴표, 서브셸 내부는 분리하지 않음) public static IReadOnlyList Parse(string command) { var result = new List(); var current = new StringBuilder(); var inSingleQuote = false; var inDoubleQuote = false; var parenDepth = 0; for (var i = 0; i < command.Length; i++) { var ch = command[i]; var next = i + 1 < command.Length ? command[i + 1] : '\0'; // 따옴표 토글 if (ch == '\'' && !inDoubleQuote) { inSingleQuote = !inSingleQuote; current.Append(ch); continue; } if (ch == '"' && !inSingleQuote) { inDoubleQuote = !inDoubleQuote; current.Append(ch); continue; } // 서브셸 if (!inSingleQuote && !inDoubleQuote) { if (ch == '(') parenDepth++; if (ch == ')') parenDepth--; } // 분리자 검출 (따옴표/서브셸 밖에서만) if (!inSingleQuote && !inDoubleQuote && parenDepth == 0) { if ((ch == '&' && next == '&') || (ch == '|' && next == '|')) { if (current.Length > 0) result.Add(current.ToString().Trim()); current.Clear(); i++; // 두 글자 연산자 건너뜀 continue; } if (ch == ';' || ch == '|') { if (current.Length > 0) result.Add(current.ToString().Trim()); current.Clear(); continue; } } current.Append(ch); } if (current.Length > 0) result.Add(current.ToString().Trim()); return result; } } // PermissionEvaluator 확장 public class PermissionEvaluator { private readonly IReadOnlyList _allowRules; private readonly IReadOnlyList _denyRules; /// /// 복합 명령의 각 서브커맨드를 독립적으로 검사합니다. /// 하나의 서브커맨드라도 deny되면 전체 명령이 차단됩니다. /// public PermissionDecision EvaluateCompound(string toolName, string command) { if (toolName != "bash" && toolName != "process") return EvaluateSingle(toolName, command); var subCommands = CompoundCommandParser.Parse(command); foreach (var sub in subCommands) { var decision = EvaluateSingle(toolName, sub.Trim()); if (decision == PermissionDecision.Deny) return PermissionDecision.Deny; } // 모든 서브커맨드가 allow이면 allow, 아니면 ask return subCommands.All(sub => EvaluateSingle(toolName, sub) == PermissionDecision.Allow) ? PermissionDecision.Allow : PermissionDecision.Ask; } // 항상 차단되는 위험 패턴 private static readonly string[] AlwaysEscalate = new[] { "sed -i", // 파일 인플레이스 편집 "rm -rf /", // 루트 삭제 "chmod 777", // 위험한 권한 "> ~/.bashrc", // 셸 설정 변경 "> ~/.zshrc", ".claude/", // 설정 디렉토리 조작 ".git/", // git 내부 조작 }; } ``` --- ## Phase 17-C 확장 — 훅 환경변수 시스템 (v2.1) > **CC의 훅 환경변수 체계를 구현**: $CLAUDE_FILE_PATH, $CLAUDE_TOOL_NAME 등 ### 17-C-EXT-1: HookEnvironmentBuilder ```csharp // 훅 실행 시 셸 환경변수를 자동 주입 public static class HookEnvironmentBuilder { /// HookContext에서 셸 환경변수 딕셔너리를 생성합니다. public static Dictionary Build(HookContext context) { var env = new Dictionary(StringComparer.OrdinalIgnoreCase); // 공통 변수 env["AX_SESSION_ID"] = context.SessionId; env["AX_HOOK_EVENT"] = context.Event.ToString(); env["AX_CWD"] = context.WorkFolder ?? ""; env["AX_PERMISSION_MODE"] = context.PermissionMode ?? "default"; // 도구 관련 변수 (PreToolUse, PostToolUse) if (!string.IsNullOrEmpty(context.ToolName)) { env["AX_TOOL_NAME"] = context.ToolName; env["CLAUDE_TOOL_NAME"] = context.ToolName; // CC 호환 } if (!string.IsNullOrEmpty(context.ToolInput)) { env["AX_TOOL_INPUT"] = context.ToolInput; env["CLAUDE_TOOL_INPUT"] = context.ToolInput; // CC 호환 } // 파일 경로 변수 (Write, Edit 도구 시) if (!string.IsNullOrEmpty(context.FilePath)) { env["AX_FILE_PATH"] = context.FilePath; env["CLAUDE_FILE_PATH"] = context.FilePath; // CC 호환 } // 파일 변경 변수 (FileChanged) if (!string.IsNullOrEmpty(context.ChangedFilePath)) env["AX_CHANGED_FILE"] = context.ChangedFilePath; // CwdChanged 전용: CLAUDE_ENV_FILE if (context.Event == HookEvent.CwdChanged) { var envFile = Path.Combine(Path.GetTempPath(), "AxCopilot", $"env_{context.SessionId}.txt"); Directory.CreateDirectory(Path.GetDirectoryName(envFile)!); env["CLAUDE_ENV_FILE"] = envFile; env["AX_ENV_FILE"] = envFile; } // 사용자 프롬프트 (UserPromptSubmit) if (!string.IsNullOrEmpty(context.UserMessage)) env["AX_USER_PROMPT"] = context.UserMessage; return env; } } // CommandHookExecutor에서 환경변수 주입 public class CommandHookExecutor : IHookExecutor { public async Task ExecuteAsync(HookContext context, CancellationToken ct) { var psi = new ProcessStartInfo { ... }; // 환경변수 주입 foreach (var (key, value) in HookEnvironmentBuilder.Build(context)) psi.Environment[key] = value; // stdin에 JSON 입력 전달 (CC 호환) var inputJson = JsonSerializer.Serialize(context, _jsonOpts); // ... 프로세스 실행, stdin에 inputJson 쓰기 ... } } ``` --- ## Phase 23 — 최종 CC 동등성 검증 + 누락 기능 (v2.1) ✅ 완료 > **목표**: Phase 19~22 이후 남은 소규모 갭을 해소하고, 전체 기능 매트릭스 검증 > > **구현 완료** (2026-04-03): AutoCompactMonitor, IConditionalTool + ToolEnvironmentContext, SlashAutoCompleteProvider. 또한 Phase 19-D-EXT(SkillArgumentSubstitution, SkillInlineCommandProcessor → SkillService 통합), Phase 19-B-EXT(CompoundCommandParser → PermissionDecisionService 통합), Phase 17-C-EXT(HookEnvironmentBuilder → ExtendedHookRunner 통합) 모두 완료. ### 23-1: argument-hint 자동완성 UI ```csharp // AgentInputArea에서 / 입력 시 자동완성 팝업에 hint 표시 public class SlashAutoCompleteProvider { private readonly SlashCommandRegistry _commands; private readonly SkillLoaderService _skills; public IReadOnlyList GetSuggestions(string input) { if (!input.StartsWith("/")) return Array.Empty(); var prefix = input.TrimStart('/'); var results = new List(); // 내장 명령 foreach (var cmd in _commands.GetAll()) { if (cmd.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) results.Add(new AutoCompleteItem($"/{cmd.Name}", cmd.Description, cmd.ArgumentHint)); } // 스킬 foreach (var skill in _skills.GetUserInvocableSkills()) { if (skill.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) results.Add(new AutoCompleteItem($"/{skill.Name}", skill.Frontmatter.Description, skill.Frontmatter.ArgumentHint)); } return results.OrderBy(r => r.Name).ToList(); } } public record AutoCompleteItem(string Name, string Description, string? ArgumentHint); ``` ### 23-2: 세션 /compact 자동 트리거 ```csharp // 컨텍스트 크기 모니터링 → 자동 컴팩션 public class AutoCompactMonitor { private readonly TokenEstimatorService _tokenEstimator; private readonly int _thresholdPercent; // 기본 80% /// 매 에이전트 반복 후 호출 → 컨텍스트가 임계치 초과 시 자동 컴팩션. public async Task CheckAndCompactAsync( AgentLoopService agentLoop, LlmService llm, CancellationToken ct) { var usage = _tokenEstimator.EstimateTokenCount(agentLoop.GetCurrentMessages()); var maxTokens = agentLoop.CurrentModelMaxTokens; var usagePercent = (int)((double)usage / maxTokens * 100); if (usagePercent < _thresholdPercent) return false; LogService.Info($"컨텍스트 {usagePercent}% 사용 → 자동 컴팩션 시작"); // CompactCommand와 동일한 로직 (DRY: 공유 서비스로 분리) await CompactionService.CompactAsync(agentLoop, llm, instruction: null, ct); return true; } } // 설정 추가 public class LlmSettings { [JsonPropertyName("auto_compact_enabled")] public bool AutoCompactEnabled { get; set; } = true; [JsonPropertyName("auto_compact_threshold_percent")] public int AutoCompactThresholdPercent { get; set; } = 80; } ``` ### 23-3: 도구별 isEnabled() 자가 비활성화 ```csharp // IAgentTool 인터페이스 확장 (ISP 준수: 옵션 인터페이스) public interface IConditionalTool { /// 현재 환경에서 이 도구가 사용 가능한지 확인합니다. bool IsEnabled(ToolEnvironmentContext env); } public record ToolEnvironmentContext( AppSettings Settings, string ActiveTab, // "Chat" | "Cowork" | "Code" string? WorkFolder, bool HasGitRepo, bool HasNodeRuntime, bool HasPythonRuntime); // ToolRegistry에서 도구 목록 생성 시 필터링 public class ToolRegistry { public IReadOnlyList GetActiveTools(ToolEnvironmentContext env) { return _tools.Where(t => { if (t is IConditionalTool conditional) return conditional.IsEnabled(env); return true; // 조건 없는 도구는 항상 활성 }).ToList(); } } ``` ### 23-4: CC 기능 매트릭스 최종 검증표 | CC 기능 | AX Phase | 상태 | |---------|----------|------| | 4-layer memory (managed/user/project/local) | 19-A | ✅ Phase 22 구현 | | @include 5단계 | 기존 | ✅ 구현됨 | | rules/*.md + paths frontmatter | 기존 | ✅ 구현됨 | | 18종 훅 이벤트 | 25 | ✅ Phase 25 구현 (PostToolUseFailure, Stop 포함) | | 4종 훅 타입 (command/http/prompt/agent) | 25 | ✅ Phase 25 구현 | | 훅 환경변수 ($CLAUDE_FILE_PATH 등) | 25 | ✅ Phase 25 HookEnvironmentBuilder | | 훅 if 조건부 실행 | 25 | ✅ Phase 25 EvaluateCondition (==, !=, contains, &&) | | 훅 once/async/asyncRewake | 25 | ✅ Phase 25 AsyncRewakeTriggered 이벤트 | | 스킬 $ARGUMENTS 치환 | 24 | ✅ Phase 24 SkillArgumentSubstitution | | 스킬 인라인 셸 실행 | 24 | ✅ Phase 24 SkillInlineCommandProcessor | | 스킬 네임스페이싱 (colon) | 24 | ✅ Phase 24 LoadSkillsRecursive | | 스킬 명명된 인수 ($name) | 24 | ✅ Phase 24 SkillDefinition.Arguments | | 스킬 context:fork | 24 | ✅ Phase 24 IsForkContext frontmatter | | 스킬 paths 자동 활성화 | 24 | ✅ Phase 24 PathBasedSkillActivator | | 스킬 model 오버라이드 | 24 | ✅ Phase 24 ModelOverride frontmatter | | 스킬 user-invocable | 24 | ✅ Phase 24 UserInvocable 필터링 | | 스킬 범위 훅 | 24 | ✅ Phase 24 ScopedHooks frontmatter | | 4종 권한 모드 (default/acceptEdits/plan/bypass) | 22 | ✅ Phase 22 구현 | | 복합 Bash 명령 파싱 (&&/\|\|/;/\|) | 22 | ✅ Phase 22 CompoundCommandParser | | glob 패턴 권한 매칭 (Bash(git *)) | 기존 | ✅ 구현됨 | | MCP 도구 차단 (mcp__server 규칙) | 기존 | ✅ 구현됨 | | 도구 가시성 필터링 | 기존 | ✅ 구현됨 | | 슬래시 명령 14종 | 22 + 26 | ✅ Phase 22/26 SlashCommandRegistry | | 출력 예산 + 스필오버 | 27 | ✅ Phase 27 ToolResultSizer | | 세션 fork/resume/tag | 28 | ✅ Phase 28 SessionManager | | 자동 컴팩션 | 29-A | ✅ Phase 29 AutoCompactMonitor 통합 | | 도구 isEnabled() 자가 비활성화 | 29-B | ✅ Phase 29 IConditionalTool + ToolEnvironmentContext | | /init 프로젝트 초기화 | 26 | ✅ Phase 26 InitCommand | | @include AxMd 지시어 | 27 | ✅ Phase 27 AxMdIncludeResolver | | 멀티에이전트 + worktree | 기존 | ✅ SubAgentTool + DelegateAgentTool + WorktreeManager | | 에이전트 타입별 메모리 | 기존 | ✅ AgentTypeMemoryRepository + MemoryTool + /memory 명령 | | 4-layer 계층 메모리 통합 조회 | 30-C | ✅ Phase 30 HierarchicalMemoryService | | SDK 제어 프로토콜 (JSON 스트리밍) | 31 | ✅ Phase 31 AgentSdkServer + SdkProtocol | | 커스텀 에이전트 정의 (SDK) | 31-C | ✅ Phase 31 CustomAgentDefinition + AgentTypeRegistry | | SDK 훅 콜백 (외부 권한 핸들링) | 31-D | ✅ Phase 31 SdkHookCallbackHandler | | 3-Pane Claude.ai UI (사이드바+헤더+설정 패널+입력) | 32 | ✅ Phase 32 AgentSettingsPanel/SidebarView/HeaderBar/InputArea | --- ## 구현 의존성 그래프 (Phase 19+ 갱신) ``` [19-A] HierarchicalMemory ─────────────────────────────────────────┐ ↓ │ [19-B] PermissionPatternMatch ← [19-B-EXT] CompoundParsing │ ↓ ↓ [19-C] HookEvents ← [17-C-EXT] HookEnvVars │ ↓ │ [19-D] SkillSystem ← [19-D-EXT] InlineCmd + Namespace + NamedArgs │ ↓ ↓ │ [19-E] SessionManager ──────────────────────────────────────────────┘ ↓ [19-F] OutputBudget ↓ [19-G] /init ← [22] SlashCommands (15종) ← [23] AutoCompact + isEnabled ↓ [20-A] SDK ──→ [20-B] CustomAgent ──→ [20-C] SDK 훅 콜백 ↓ [21-1~7] UI 전면 개편 ``` --- ## 버전별 출시 계획 (갱신) | 버전 | 코드명 | 포함 Phase | 핵심 신규 클래스 수 | |------|--------|-----------|-----------------| | **v1.8.0** | 에이전트 인프라 | 17-UI, 17-A~G | ~35개 신규 클래스 ✅ 완료 | | **v1.8.1** | 인프라 안정화 | 버그픽스 + 17-* 마이너 | — | | **v2.0** | 에이전트 팀 | 18-A~C, L3 | ~25개 신규 클래스 ✅ 완료 | | **v2.1** | CC 완전 동등성 | 19-A~G + 19-EXT + 22 + 23 | **~50개 신규 클래스** | | **v2.2** | SDK 프로토콜 | 20-A~C (임베딩 + 외부 연동) | ~15개 신규 클래스 | | **v2.3** | AX Agent UI 전면 개편 | 21 (Claude.ai+Codex 레이아웃) | UI 계층 60% 재작성 | | **v3.0** | 크로스플랫폼 | LP-1~3 (Avalonia) | UI 계층 40% 재작성 | ### v2.1 세부 구현 순서 (권장) ``` Week 1-2: [19-A] 4-layer Memory + [19-B + EXT] Permission + CompoundParsing Week 3: [17-C-EXT] HookEnvVars + [19-C] HookEvents 완성 Week 4: [19-D-EXT] Skill InlineCmd + Namespace + NamedArgs Week 5: [22] SlashCommands 16종 (ISlashCommand + Registry + 구현체) Week 6: [19-E] SessionManager (fork/resume/tag) Week 7: [19-F] OutputBudget + Spillover + [19-G] /init Week 8: [23] AutoCompact + isEnabled + 최종 검증 ``` --- --- ## Phase 24 — 스킬 고급 기능 (v2.1) ✅ 완료 > **목표**: CC 스킬 시스템의 나머지 갭 해소 — 네임스페이싱, 고급 프론트매터, 실행 격리. > > **구현 완료** (2026-04-03): ### 24-A: 콜론 네임스페이싱 - `SkillService.LoadSkillsRecursive()` — 하위 디렉토리를 재귀 탐색, `database/migrate/SKILL.md` → `database:migrate` 자동 변환 - 중간 디렉토리(SKILL.md 없음)는 건너뛰고 더 깊이 탐색 ### 24-B: 고급 프론트매터 필드 - `context`: "fork"이면 격리 서브에이전트에서 실행 (플래그 설정, 실행은 AgentLoopService에서 처리) - `model`: 스킬별 모델 오버라이드 - `user-invocable`: false면 `/` 자동완성에서 숨김 (AI만 사용) - `paths`: 파일 경로 글로브 — 매칭 파일 터치 시 자동 제안 - `hooks`: 스킬 스코프 훅 정의 (JSON) - `arguments`: 명명된 인수 리스트 (`[$name, $dir]`) - `when_to_use`: AI 선제적 사용 힌트 - `version`: 스킬 버전 ### 24-C: SkillDefinition 확장 - 10개 새 프로퍼티 추가 (`Context`, `ModelOverride`, `UserInvocable`, `Paths`, `ScopedHooks`, `Arguments`, `WhenToUse`, `Version`, `IsForkContext`) - `ParseSkillFile()` 2-param 오버로드 (네임스페이스 이름 주입) - `MatchSlashCommand()` — `UserInvocable` 필터 적용 - `PrepareSkillBodyAsync()` — `Arguments` 필드 연동 --- ## Phase 25 — 훅 고급 기능 + 도구 결과 크기 제한 (v2.1) ✅ 완료 > **목표**: 훅 조건부 실행, asyncRewake, 누락 이벤트 추가. 도구 결과 오버플로우 방지. > > **구현 완료** (2026-04-03): ### 25-A: Hook `if` 조건부 실행 - `EvaluateCondition()` 완전 구현 — `"field == value"`, `"field != value"`, `"field contains value"` 지원 - `&&` 연결로 복합 조건 지원 - 지원 필드: `tool_name`, `event`, `session_id`, `work_folder`, `user_message`, `changed_file` ### 25-B: Hook asyncRewake + 이벤트 확장 - `ExtendedHookEntry.AsyncRewake` 필드 추가 - asyncRewake 훅: 비동기 실행 후 exit code 2 시 `AsyncRewakeTriggered` 이벤트 발생 - `AsyncRewakeEventArgs` — HookName, AdditionalContext, SessionId - `HookEventKind` 확장: `PostToolUseFailure`, `Stop` 추가 (총 17개, CC 동등) ### 25-C: 도구 결과 크기 제한 (ToolResultSizer) - `ToolResultSizer.Apply()` — 50,000자 초과 시 임시 파일 저장 + head/tail 프리뷰 반환 - `ToolResultSizeInfo` — Output, WasTruncated, SpilloverFilePath, OriginalLength - `CleanupOldResults()` — 24시간 지난 임시 결과 자동 정리 - CC의 `maxResultSizeChars` + temp file spillover 패턴 완전 재현 --- ## Phase 26 — /init 프로젝트 초기화 (v2.1) ✅ 완료 > **목표**: CC의 `/init` 명령 동등 — 코드베이스 분석 → AX.md 자동 생성. > > **구현 완료** (2026-04-03): ### InitCommand (/init) - 프로젝트 구조 트리 (3레벨, node_modules/.git 등 제외) - Git 정보 수집 (브랜치, 리모트, 최근 커밋) - 기술 스택 자동 감지 (Node.js/TypeScript/.NET/Rust/Go/Python/Java/Docker/GitHub Actions) - 주요 설정 파일 내용 수집 (package.json, *.csproj, go.mod 등) - LLM에게 프로젝트 분석 → AX.md 5섹션 자동 생성 - 프로젝트 개요, 코드 컨벤션, 빌드/테스트, 작업 가이드라인, 주의사항 - 기존 AX.md 존재 시 덮어쓰기 방지 --- --- ## Phase 27 — 미통합 인프라 연결 (v2.1) ✅ 완료 > **목표**: 이미 정의되었으나 미사용 상태인 인프라(ToolResultSizer, PathBasedSkillActivator, AxMdIncludeResolver)를 실제 코드에 통합. > > **구현 완료** (2026-04-03): ### 27-A: ToolResultSizer → AgentLoopService 통합 - `AgentLoopService.cs:870` — 기존 `TruncateOutput(result.Output, 4000)` → `ToolResultSizer.Apply(result.Output, call.ToolName)` 교체 - 50,000자 초과 결과 → 임시 파일 + head/tail 프리뷰 → LLM에 전달 ### 27-B: PathBasedSkillActivator → 시스템 프롬프트 통합 - `ChatWindow.xaml.cs` — `BuildPathBasedSkillSection()` 추가 - 최근 5개 메시지에서 파일 경로 패턴 추출 → `PathBasedSkillActivator.GetActiveSkillsForFile()` → 시스템 프롬프트 자동 주입 - `SkillDefinitionExtensions.GetPathPatterns()` — Phase 24의 `Paths` 프로퍼티 우선 사용, ExtensionStore 폴백 ### 27-C: AxMdIncludeResolver → AX.md 로딩 통합 - `LoadProjectContext()` — AX.md 내용에 `@` 포함 시 `AxMdIncludeResolver.ResolveAsync()` 실행 - `@./relative/path`, `@~/home`, `@/absolute` 5레벨 깊이 재귀 해석 - 순환 참조 감지, 파일 미존재 시 HTML 주석으로 대체 --- ## Phase 28 — 세션 매니저 (v2.1) ✅ 완료 > **목표**: CC의 세션 관리(save/resume/fork/tag)와 동등한 세션 영속성 시스템. > > **구현 완료** (2026-04-03): ### SessionManager - `SaveSessionAsync()` / `LoadSessionAsync()` — JSON 기반 세션 저장/복원 - `ForkSessionAsync()` — 기존 세션에서 분기 (메시지 복사 + "forked" 태그 + ParentSessionId) - `TagSessionAsync()` / `RenameSessionAsync()` — 태그/이름 관리 - `ListSessions()` — 최신순 목록 (최대 50개) - `FindByTag()` — 태그 기반 검색 - `GetMostRecent()` — CC의 `--continue` 동등 (탭별 최근 세션) - `CleanupOldSessions()` — 30일 보존 기간 경과 시 자동 삭제 - `AgentSession` — Id, Name, Tab, WorkFolder, Model, Messages, Tags, ParentSessionId, CreatedAt/UpdatedAt - `AgentSessionSummary` — 목록 표시용 경량 DTO --- ## Phase 29 — 최종 통합 & 검증 (v2.1) ✅ 완료 > **목표**: Phase 22~28에서 생성된 클래스들을 실제 코드 경로에 연결하고, CC 기능 매트릭스를 검증. > > **구현 완료** (2026-04-03): ### 29-A: AutoCompactMonitor → AgentLoopService 통합 - `AgentLoopService.RunAsync` 메인 루프에서 `LastTokenUsage` 기반 컨텍스트 사용량 모니터링 - `AutoCompactThreshold` (기본 80%) 초과 시 `ContextCondenser.CondenseIfNeededAsync()` 자동 실행 - `AppSettings.LlmSettings.AutoCompactThreshold` 설정 추가 ### 29-B: IConditionalTool + ToolEnvironmentContext 통합 - `AgentLoopService` line 339: `ToolEnvironmentContext` 빌드 후 `GetActiveTools(disabled, env)` 오버로드 사용 - `.git` 폴더 존재 여부로 `HasGitRepo` 자동 감지 - `GitTool` → `IConditionalTool` 구현 (HasGitRepo=false 시 도구 목록에서 자동 제외) ### 29-C: SessionManager → AgentLoopService 통합 - `RunAsync` finally 블록에서 세션 자동 저장 (`SessionManager.SaveSessionAsync`) - 사용자 질의 첫 40자를 세션 이름으로 자동 설정 - 현재 탭, 모델, 작업 폴더, 메시지를 `AgentSession`으로 영속화 ### 29-D: CC 기능 매트릭스 상태 갱신 - 33개 기능 항목 중 **29개 ✅ 구현 완료**, 4개 📋 계획 (SDK, 멀티에이전트, 에이전트 메모리, 3-Pane UI) - Phase 22~29 구현 결과 반영 --- ## Phase 30 — 아키텍처 정제 & 계층 메모리 (v2.1) ✅ 완료 > **목표**: DIP 원칙 적용, CC 4-layer 계층 메모리 통합, 기존 구현 누락 항목 매트릭스 반영. > > **구현 완료** (2026-04-03): ### 30-A: CC 기능 매트릭스 재갱신 - 기존 멀티에이전트(SubAgentTool, DelegateAgentTool, WorktreeManager) 및 에이전트 메모리(AgentTypeMemoryRepository, MemoryTool, /memory) 확인 → ✅ 반영 - 35개 항목 중 **33개 ✅ 구현 완료**, 2개 📋 계획 (SDK, 3-Pane UI) ### 30-B: IAgentMemoryRepository 인터페이스 추출 - `IAgentMemoryRepository` 인터페이스 신규 — LoadMemoryAsync, SaveMemoryAsync, AppendLearnAsync, GetAgentTypes, DeleteMemory - `AgentTypeMemoryRepository` → `IAgentMemoryRepository` 구현 - `DelegateAgentTool` → 구체 클래스 대신 `IAgentMemoryRepository` 인터페이스에 의존 (DIP) ### 30-C: HierarchicalMemoryService — 4-layer 메모리 통합 조회 - 4단계 우선순위: Managed(%PROGRAMDATA%) → User(%APPDATA%) → Project(WorkFolder/AX.md + .ax/rules/) → Local(AX.local.md) - `CollectAll()` — 모든 레벨의 MemoryFile 수집 - `BuildMergedContext()` — 단일 문자열 병합 (maxChars 제한, 시스템 프롬프트 삽입용) - `GetByLevel()` — 특정 레벨만 필터 조회 - `ChatWindow.LoadProjectContext()` → HierarchicalMemoryService 우선 사용, 실패 시 레거시 폴백 ### 30-D: DelegateAgentTool에 IConditionalTool 적용 - AI 비활성화(`AiEnabled=false`) 시 delegate 도구 자동 비활성화 --- ## Phase 31 — SDK 제어 프로토콜 (v2.2) ✅ 완료 > **목표**: CC의 JSON 스트리밍 SDK와 동등한 프로그래밍 방식 에이전트 제어 프로토콜. > 외부 IDE, 스크립트, CI/CD에서 AX Agent를 stdin/stdout JSON으로 제어. > > **구현 완료** (2026-04-03): ### 31-A: SDK 메시지 타입 정의 (`Services/Agent/Sdk/SdkProtocol.cs`) - `SdkControlRequestType` — 9종: Initialize, UserMessage, SetPermissionMode, SetModel, Interrupt, GetContextUsage, RewindFiles, CanUseToolResponse, Terminate - `SdkControlRequest` — Host → CLI 요청 (Type + Id + Payload) - `SdkMessageType` — 11종: InitializeResult, AssistantToken, AssistantMessage, ToolUseStart, ToolProgress, ToolResult, SessionResult, HookEvent, CanUseTool, Notification, Error - `SdkMessage` — CLI → Host 응답/이벤트 - 데이터 클래스: SdkInitializePayload, SdkUserMessagePayload, SdkTokenData, SdkToolUseStartData, SdkToolResultData, SdkSessionResultData, SdkCanUseToolData, SdkErrorData ### 31-B: AgentSdkServer (`Services/Agent/Sdk/AgentSdkServer.cs`) - stdin/stdout 기반 양방향 JSON 스트리밍 서버 - Initialize → 설정 적용 + 커스텀 에이전트 등록 + 훅 콜백 등록 - UserMessage → AgentLoopService.RunAsync 실행 + 스트리밍 이벤트 전달 - SetModel/SetPermissionMode → 런타임 설정 변경 - Interrupt → 현재 턴 취소 - GetContextUsage → 토큰 사용량 조회 - AgentEvent → SdkMessage 자동 변환 (이벤트 구독) - Thread-safe 출력 (SemaphoreSlim) ### 31-C: CustomAgentDefinition + AgentTypeRegistry (`Services/Agent/Sdk/AgentTypeRegistry.cs`) - `CustomAgentDefinition` — name, description, prompt, model, allowedTools, disallowedTools, maxTurns, permissionMode - `AgentTypeRegistry` — CRUD + 목록 조회, Initialize 시 등록 ### 31-D: SdkHookCallbackHandler (`Services/Agent/Sdk/SdkHookCallbackHandler.cs`) - 도구 사용 허가 요청 → Host로 CanUseTool 전송 → 30초 타임아웃 대기 → Allow/Deny - 훅 이벤트 알림 → 등록된 이벤트만 Host에 전달 - TaskCompletionSource 기반 비동기 응답 대기 --- ## Phase 32 — 3-Pane Claude.ai UI 기반 구조 (v2.3) ✅ 완료 > **목표**: CC의 Claude.ai + Codex 레이아웃과 동등한 3-Pane UI 구조를 수립. > 새 UserControl 컴포넌트 4종 + ChatWindow 통합. > > **구현 완료** (2026-04-03): ### 32-A: AgentSettingsPanel (`Views/Controls/AgentSettingsPanel.xaml/.cs`) - 우측 슬라이드인 설정 패널 (TranslateTransform X 애니메이션 200ms/150ms) - **모델 & 서비스**: LLM 서비스 선택, 모델 선택, API 엔드포인트 입력 - **에이전트 동작**: 최대 반복 횟수(Slider), 오류 재시도(Slider), 병렬 도구 실행(Toggle) - **탭 전용 설정**: Cowork(검증 강제) / Code(LSP, 코드 검증) 자동 분기 - **도구 관리**: 등록된 모든 도구의 개별 Toggle 동적 생성 - **고급**: 자동 컴팩션 임계치(Slider), 프로젝트 규칙, 개발자 모드 - 설정 변경 즉시 저장 (`SettingsService.Save()`) ### 32-B: AgentSidebarView (`Views/Controls/AgentSidebarView.xaml/.cs`) - Claude.ai 스타일 좌측 사이드바 (260px, 접기 시 48px 애니메이션) - 탭 세그먼트 (Chat|Cowork|Code) — AccentColor 배경 강조 - 새 대화 버튼, 검색 필드 - 날짜 그룹별 대화 이력 (오늘/어제/이전 7일/30일) - 호버 효과, 대화 선택/삭제 이벤트 ### 32-C: AgentSessionHeaderBar (`Views/Controls/AgentSessionHeaderBar.xaml/.cs`) - Codex 스타일 세션 헤더 바 (42px) - 모델 칩 (클릭 → 모델 변경), Plan 칩 (3-state 순환), 권한 칩 (4-state 순환) - 설정 패널 토글 버튼 - 모든 칩: Border + MouseLeftButtonUp + 호버 효과 (CLAUDE.md 원칙 준수) ### 32-D: AgentInputArea (`Views/Controls/AgentInputArea.xaml/.cs`) - Claude.ai 스타일 입력 영역 - 상단 툴바: @ 멘션 / / 스킬 / 첨부 버튼 - 멀티라인 TextBox (40~200px 자동 확장, Ctrl+Enter 전송) - 하단 칩 열: 모델명, 권한, Plan 상태 표시 - 전송/중단 버튼 전환 (스트리밍 상태 기반) - 첨부 파일 칩 (추가/제거) ### 32-E: ChatWindow.xaml 3-Pane 통합 - Grid Column 5 추가 (`SettingsPanelColumn`, Auto) - `AgentSettingsPanel` 배치 — Shift+설정 버튼 클릭으로 토글 - `xmlns:ctrl` 네임스페이스 등록 - 기존 UI 완전 호환 유지 (기존 사이드바/메인 영역 변경 없음) --- ## Phase 33 — 코드 품질 리팩터링 (v2.3) ✅ 완료 > **목표**: Claude Code 동등 수준의 코드 품질 달성. 기능 파라티가 아닌 구조적 품질 파라티. > AgentLoopService (925줄 RunAsync, 매직 넘버, 레이스 컨디션) + ChatWindow.xaml.cs (176건 브러시 중복, 46건 폰트 중복) 정리. ### 33-A: AgentLoopService — 매직 넘버 상수화 - `Defaults` 내부 정적 클래스 신설 (16개 상수) - MaxIterations, MaxRetryOnError, MaxTestFixIterations, MaxPlanRegenerationRetries 등 - ToolCallThresholdForExpansion, ContextCompressionTargetRatio, AutoCompactThresholdPercent - QueryNameMaxLength, QueryTitleMaxLength, ThinkingTextMaxLength, VerificationSummaryMaxLength 등 ### 33-B: AgentLoopService — RunAsync 분해 (ProcessSingleToolCallAsync 추출) - `ProcessSingleToolCallAsync()` 메서드로 단일 도구 실행 로직 약 250줄 추출 - `ToolExecutionState` 클래스: 루프 상태 공유 (카운터, 통계, 플랜 메타데이터) - `ToolCallAction` enum: Continue/Break/Return 루프 제어 - `RunToolHooksAsync()` 헬퍼: Pre/Post 훅 실행 통합 (중복 코드 제거) - RunAsync: 925줄 → 약 680줄 (약 27% 감소) ### 33-C: AutoCompactMonitor + ContextCondenser 통합 - 기존: 별도 실행되어 이중 압축 가능성 - 통합: `!condensed` 가드로 단일 흐름 보장 - 1단계: ContextCondenser 기본 압축 → 2단계: AutoCompactMonitor 임계치 기반 적극적 압축 ### 33-D: SessionManager DI 기반 개선 - 기존: `new SessionManager()` + `_ = SaveSessionAsync()` fire-and-forget - 개선: 생성자 주입 (`SessionManager? sessionManager = null`) + `await` + 에러 로깅 ### 33-E: ChatWindow — ThemeResourceHelper 통합 적용 - `ThemeResourceHelper` 정적 헬퍼 (Phase 32에서 생성) 실제 적용 - `TryFindResource("XXX") as Brush ?? Brushes.YYY` 패턴 ~162건 → `ThemeResourceHelper.Primary(this)` 등으로 교체 - `new FontFamily("Segoe MDL2 Assets")` 46건 → `ThemeResourceHelper.SegoeMdl2` 캐시 참조로 교체 ### 33-F: ActiveTab 레이스 컨디션 + 에러 핸들링 정리 - `activeTabSnapshot` 캡처 — RunAsync 진입 시 스냅샷, 루프 전체에서 일관 사용 - `BuildContext(tabOverride)` — 스냅샷 기반 컨텍스트 생성 - RunPostToolVerificationAsync: `ActiveTab` → `context.ActiveTab` 참조 - ExecuteToolsInParallelAsync: 감사 로그 `ActiveTab` → `context.ActiveTab` - bare `catch {}` 5건 → `catch (Exception ex) { LogService.Warn(...) }` 패턴으로 교체 - bare `catch { break; }` → 오류 메시지 로깅 추가 --- ## Phase 34 — ChatWindow God Class 분해 (v2.3) ✅ 완료 > **목표**: 10,372줄 ChatWindow.xaml.cs의 구조적 개선. 중복 제거, 대형 메서드 분해, 헬퍼 추출. ### 34-A: SendMessageAsync Cowork/Code 중복 제거 - 기존: Cowork(35줄)과 Code(35줄) 블록이 거의 동일 — DRY 위반 - `RunAgentLoopAsync(tab, sendMessages, ct)` 단일 메서드로 통합 (~70줄 → ~40줄) - 탭별 시스템 프롬프트 분기, 완료 알림 탭별 라벨 통합 ### 34-B: ThemeResourceHelper 완전 적용 - 잔여 14건 `TryFindResource` 교체 (HintBackground, HintText, SeparatorColor) - ThemeResourceHelper에 `Hint()`, `HintFg()`, `Separator()` 메서드 추가 - **TryFindResource 잔여: 0건** (전량 ThemeResourceHelper로 이전 완료) - `new FontFamily("Consolas")` 8건 → `ThemeResourceHelper.Consolas` 캐시 참조 ### 34-C: PopupMenuHelper 정적 헬퍼 생성 (`Views/PopupMenuHelper.cs`) - ChatWindow에서 6회 이상 반복되는 `Popup + Border + StackPanel + DropShadow` 패턴 통합 - `Create(target, owner, ...)` — 테마 적용된 팝업 메뉴 구조 생성 - `MenuItem(text, fg, hoverBg, onClick, isChecked, icon)` — 메뉴 항목 생성 - `Separator()`, `SectionHeader()` — 구분선/섹션 제목 - 기존 팝업 메서드들의 점진적 마이그레이션 기반 확보 ### 34-D: AgentLoopService 잔여 정리 - 마지막 bare `catch {}` → `catch (Exception)` + 주석 명시 (document_plan JSON 파싱) --- ## Phase 35 — 코드 품질 심층 정리 (v2.3) ✅ 완료 > **목표**: 전체 코드베이스의 bare catch, 하드코딩 색상, 서비스 로케이터, 체인 액세스 패턴을 체계적으로 정리. ### 35-A: Bare catch 전량 정리 (109개 파일) - `catch { }` 및 `catch { return X; }` 등 **모든 bare catch** → `catch (Exception)` + 상황별 주석 - 전체 코드베이스 109개 .cs 파일에 PowerShell 일괄 치환 적용 - 빈 catch, return 포함 catch, 값 할당 catch 등 모든 변형 포함 ### 35-B: ColorConverter.ConvertFromString 헬퍼 추출 - `ThemeResourceHelper.HexBrush(hex)` / `HexColor(hex)` 정적 메서드 추가 - ChatWindow.xaml.cs 47건 `new SolidColorBrush((Color)ColorConverter.ConvertFromString(...))` → `ThemeResourceHelper.HexBrush(...)` 일괄 치환 - SettingsWindow, ShortcutHelpWindow, HelpDetailWindow, StatisticsViewModel 등 7개 추가 파일 34건 치환 - 총 **81건** 장황한 색상 변환 패턴 제거 ### 35-C: ChatWindow `_settings.Settings.Llm` 체인 액세스 캐싱 - `private LlmSettings Llm => _settings.Settings.Llm;` 편의 프로퍼티 추가 - 92개 `_settings.Settings.Llm.` 체인 → `Llm.` 단축 치환 - 가독성 향상 + 향후 리팩터링 시 단일 변경 지점 확보 ### 35-D: AgentContext.Settings 주입 — 서비스 로케이터 제거 - `AgentContext`에 `Settings` (AppSettings?) + `Llm` (LlmSettings?) 프로퍼티 추가 - `AgentLoopService.BuildContext()`에서 Settings 주입 - 에이전트 도구 11개 파일의 `Application.Current as App` 서비스 로케이터 패턴 제거: - BuildRunTool, CodeSearchTool, CodeReviewTool, MemoryTool, DocumentPlannerTool, HttpTool, LspTool, SkillManagerTool, SubAgentTool, SnippetRunnerTool, TestLoopTool - `context.Llm?.X` 패턴으로 교체 — DIP(의존성 역전 원칙) 준수 --- ## Phase 36 — 서비스 로케이터 제거 + PopupMenuHelper 적용 (v2.3) ✅ 완료 > **목표**: `Application.Current as App` 서비스 로케이터 패턴을 전체 코드베이스에서 제거하고, PopupMenuHelper를 실제 팝업 생성 코드에 적용. ### 36-A: `Application.Current as App` → `CurrentApp` 정적 프로퍼티 일원화 - SettingsWindow (17건), ChatWindow (4건), 기타 12개 파일에 `private static App? CurrentApp` 프로퍼티 추가 - 모든 인라인 `Application.Current as App` 호출을 `CurrentApp`으로 교체 - ChatWindow의 설정 접근은 기존 `Llm` 프로퍼티 + `_settings.Save()` 패턴으로 직접 대체 - **39건 → 0건** (프로퍼티 정의 제외) 서비스 로케이터 인라인 호출 제거 ### 36-B: PopupMenuHelper 실제 적용 (ChatWindow 3개 팝업) - `BtnCategoryDrop_Click` — `Popup+Border+StackPanel+DropShadow` 30줄 → `PopupMenuHelper.Create()` 2줄 - `ShowConversationMenu` — 팝업 구조 + `CreateMenuItem` 로컬 함수 80줄 → `PopupMenuHelper.Create()` + `PopupMenuHelper.MenuItem()` 래핑 10줄 - `ShowFileTreeContextMenu` — 팝업 구조 + `AddItem`/`AddSep` 로컬 함수 60줄 → `PopupMenuHelper` 대체 5줄 - 모델 선택 팝업 (`BtnModelSelector`) — 팝업 구조 35줄 → `PopupMenuHelper.Create()` 1줄 (커스텀 애니메이션 항목은 유지) - ChatWindow: **10,353줄 → 10,184줄** (169줄 감소) --- ## Phase 37 — ChatWindow God Class 파셜 클래스 분할 (v2.3) ✅ 완료 > **목표**: 10,184줄 ChatWindow.xaml.cs를 7개 파셜 클래스 파일로 분할하여 메인 파일을 4,767줄로 축소 (53.2% 감소). ### 분할 결과 | # | 파일명 | 줄 수 | 추출 내용 | |---|--------|-------|----------| | 1 | `ChatWindow.MessageRendering.cs` | 522 | 메시지 렌더링, 체크 아이콘, 액션 버튼 | | 2 | `ChatWindow.SlashCommands.cs` | 579 | 슬래시 명령 팝업, 드래그앤드롭 AI 액션 | | 3 | `ChatWindow.AgentSupport.cs` | 475 | 에이전트 루프, 시스템 프롬프트, 워크플로우 | | 4 | `ChatWindow.TaskDecomposition.cs` | 1,170 | Plan UI, Diff 뷰, 진행률, 이벤트 배너 | | 5 | `ChatWindow.Presets.cs` | 1,280 | 주제 버튼, 프리셋 관리, 하단바, 설정 토글 | | 6 | `ChatWindow.ModelSelector.cs` | 395 | 모델 선택, 프롬프트 템플릿, 대화 관리 | | 7 | `ChatWindow.PreviewAndFiles.cs` | 1,105 | 미리보기 패널, 진행률 바, 파일 탐색기 | | — | **ChatWindow.xaml.cs (메인)** | **4,767** | 생성자, 탭 전환, 전송, 대화 목록, 권한 등 | ### 핵심 성과 - **메인 파일**: 10,184줄 → 4,767줄 (**53.2% 감소**) - **총 코드**: 10,293줄 (리다이렉트 주석 + 파셜 파일 헤더 오버헤드 ~1%) - **빌드**: 경고 0, 오류 0 - **각 파셜 파일**: 독립적 섹션으로 응집도 높은 메서드 그룹 - **필드 이동**: 각 섹션에 속하는 필드를 함께 이동하여 관련 코드 근접 배치 --- ## Phase 38 — SettingsWindow 파셜 클래스 분할 (v2.3) ✅ 완료 > **목표**: 3,216줄 SettingsWindow.xaml.cs를 3개 파셜 클래스 파일로 분할. | 파일 | 줄 수 | 내용 | |------|-------|------| | `SettingsWindow.xaml.cs` (메인) | 373 | 생성자, 필드, 저장/닫기, 스니펫 이벤트 | | `SettingsWindow.UI.cs` | 802 | 섹션 헬퍼, 탭 전환, 독바, 스토리지, 핫키, 버전 | | `SettingsWindow.Tools.cs` | 875 | 도구/커넥터 카드 UI, AX Agent 탭, 도구 관리 | | `SettingsWindow.AgentConfig.cs` | 1,202 | 모델 등록, 스킬, 템플릿, AI토글, 네트워크모드, 훅, MCP | - **메인 파일**: 3,216줄 → 373줄 (**88.4% 감소**) - **빌드**: 경고 0, 오류 0 --- ## Phase 39 — FontFamily 캐싱 + LauncherWindow 파셜 분할 (v2.3) ✅ 완료 > **목표**: 89개 `new FontFamily(...)` 반복 생성 제거 + LauncherWindow 파셜 분할. ### FontFamily 캐싱 (25개 파일) ThemeResourceHelper에 5개 정적 필드 추가: - `SegoeMdl2` — `new FontFamily("Segoe MDL2 Assets")` (기존) - `Consolas` — `new FontFamily("Consolas")` (기존) - `CascadiaCode` — `new FontFamily("Cascadia Code, Consolas, monospace")` (신규) - `ConsolasCode` — `new FontFamily("Consolas, Cascadia Code, Segoe UI")` (신규) - `ConsolasCourierNew` — `new FontFamily("Consolas, Courier New")` (신규) 총 89개 `new FontFamily(...)` 호출 → 정적 캐시 필드 참조로 교체 (25개 파일) ### LauncherWindow 파셜 분할 | 파일 | 줄 수 | 내용 | |------|-------|------| | `LauncherWindow.xaml.cs` (메인) | 578 | Win32 P/Invoke, 생성자, Show(), 아이콘 20종 애니메이션 | | `LauncherWindow.Theme.cs` | 116 | ApplyTheme, 테마 빌드, BuildCustomDictionary, IsSystemDarkMode | | `LauncherWindow.Animations.cs` | 153 | 무지개 글로우, 애니메이션 헬퍼, CenterOnScreen, AnimateIn | | `LauncherWindow.Keyboard.cs` | 593 | IME 검색, PreviewKeyDown, KeyDown 20여 단축키, ShowToast | | `LauncherWindow.Shell.cs` | 177 | Shell32 P/Invoke, SendToRecycleBin, ShowLargeType, 클릭 핸들러 | - **메인 파일**: 1,563줄 → 578줄 (**63.0% 감소**) - **빌드**: 경고 0, 오류 0 --- ## Phase 40 — ChatWindow 2차 파셜 클래스 분할 (v2.3) ✅ 완료 > **목표**: 4,767줄 ChatWindow.xaml.cs (1차 분할 후 잔여)를 7개 파셜 파일로 추가 분할. | 파일 | 줄 수 | 내용 | |------|-------|------| | `ChatWindow.xaml.cs` (메인) | 262 | 필드, 생성자, OnClosing, ForceClose, ConversationMeta | | `ChatWindow.Controls.cs` | 595 | 사용자 정보, 스크롤, 제목 편집, 카테고리 드롭다운, 탭 전환 | | `ChatWindow.WorkFolder.cs` | 359 | 작업 폴더 메뉴, 폴더 설정, 컨텍스트 메뉴 | | `ChatWindow.PermissionMenu.cs` | 498 | 권한 팝업, 데이터 활용 메뉴, 파일 첨부, 사이드바 토글 | | `ChatWindow.ConversationList.cs` | 747 | 대화 목록, 그룹 헤더, 제목 편집, 검색, 날짜 포맷 | | `ChatWindow.Sending.cs` | 720 | 편집 모드, 타이머, SendMessageAsync, BtnSend_Click | | `ChatWindow.HelpCommands.cs` | 157 | /help 도움말 창, AddHelpSection | | `ChatWindow.ResponseHandling.cs` | 1,494 | 응답재생성, 스트리밍, 내보내기, 팁, 토스트, 상태바, 키보드 | - **메인 파일**: 4,767줄 → 262줄 (**94.5% 감소**) - **전체 ChatWindow 파셜 파일 수**: 15개 (1차 7개 + 2차 7개 + 메인 1개) - **빌드**: 경고 0, 오류 0 --- ## Phase 41 — SettingsViewModel·AgentLoopService 파셜 분할 (v2.3) ✅ 완료 > **목표**: SettingsViewModel (1,855줄)·AgentLoopService (1,823줄) 파셜 클래스 분할. ### SettingsViewModel 분할 | 파일 | 줄 수 | 내용 | |------|-------|------| | `SettingsViewModel.cs` (메인) | 320 | 클래스 선언, 필드, 이벤트, 생성자 | | `SettingsViewModel.Properties.cs` | 837 | 바인딩 프로퍼티 전체 (LLM, 런처, 기능토글, 테마 등) | | `SettingsViewModel.Methods.cs` | 469 | Save, Browse, AddShortcut, AddSnippet 등 메서드 | | `SettingsViewModelModels.cs` | 265 | ThemeCardModel, ColorRowModel, SnippetRowModel 등 6개 모델 클래스 | - **메인 파일**: 1,855줄 → 320줄 (**82.7% 감소**) ### AgentLoopService 분할 | 파일 | 줄 수 | 내용 | |------|-------|------| | `AgentLoopService.cs` (메인) | 1,334 | 상수, 필드, 생성자, RunAsync 루프, 헬퍼 | | `AgentLoopService.Execution.cs` | 498 | TruncateOutput, ReadOnlyTools, ClassifyToolCalls, ToolExecutionState, ParallelState, ExecuteToolsInParallelAsync | - **메인 파일**: 1,823줄 → 1,334줄 (**26.8% 감소**) - **빌드**: 경고 0, 오류 0 --- ## Phase 42 — ChatWindow.ResponseHandling·LlmService 파셜 분할 (v2.3) ✅ 완료 > **목표**: ChatWindow.ResponseHandling (1,494줄)·LlmService (1,010줄) 추가 분할. ### ChatWindow.ResponseHandling 분할 | 파일 | 줄 수 | 내용 | |------|-------|------| | `ChatWindow.ResponseHandling.cs` | 741 | 응답 재생성, 스트리밍 완료 마크다운, 중지, 대화 분기, 팔레트 | | `ChatWindow.MessageActions.cs` | 277 | 버튼 이벤트, 메시지 내 검색(Ctrl+F), 에러 복구 | | `ChatWindow.StatusAndUI.cs` | 498 | 우클릭 메뉴, 팁, AX.md, 무지개 글로우, 토스트, 하단바, 헬퍼 | - **원본 대비**: 1,494줄 → 741줄 (**50.3% 감소**) ### LlmService 분할 | 파일 | 줄 수 | 내용 | |------|-------|------| | `LlmService.cs` (메인) | 263 | 필드, 생성자, 라우팅, 시스템 프롬프트, 비스트리밍 | | `LlmService.Streaming.cs` | 516 | StreamAsync, TestConnectionAsync, 백엔드별 구현 | | `LlmService.Helpers.cs` | 252 | 메시지 빌드, HTTP 재시도, 토큰 파싱, Dispose | - **메인 파일**: 1,010줄 → 263줄 (**74.0% 감소**) - **빌드**: 경고 0, 오류 0 --- ## Phase 43 — 4개 대형 파일 파셜 분할 (v2.3) ✅ 완료 > **목표**: SettingsWindow.AgentConfig·ChatWindow.Presets·PreviewAndFiles·WorkflowAnalyzerWindow 동시 분할. | 원본 파일 | 원본 | 메인 | 신규 파일 | 신규 줄 수 | |----------|------|------|----------|----------| | SettingsWindow.AgentConfig.cs | 1,202 | 608 | AgentHooks.cs | 605 | | ChatWindow.Presets.cs | 1,280 | 315 | CustomPresets.cs | 978 | | ChatWindow.PreviewAndFiles.cs | 1,105 | 709 | FileBrowser.cs | 408 | | WorkflowAnalyzerWindow.xaml.cs | 929 | 274 | Charts.cs | 667 | - **총 신규 파일**: 4개 - **빌드**: 경고 0, 오류 0 --- ## Phase 44 — LauncherViewModel·SettingsWindow.Tools·MarkdownRenderer 파셜 분할 (v2.3) ✅ 완료 > **목표**: LauncherViewModel(805줄)·SettingsWindow.Tools(875줄)·MarkdownRenderer(825줄) 분할. | 원본 파일 | 원본 | 메인 | 신규 파일 | 신규 줄 수 | |----------|------|------|----------|----------| | LauncherViewModel.cs | 805 | 402 | FileAction.cs | 154 | | LauncherViewModel.cs | — | — | Commands.cs | 273 | | SettingsWindow.Tools.cs | 875 | 605 | ToolCards.cs | 283 | | MarkdownRenderer.cs | 825 | 621 | Highlighting.cs | 215 | - **LauncherViewModel 메인**: 805줄 → 402줄 (50.1% 감소) - **총 신규 파일**: 4개 - **빌드**: 경고 0, 오류 0 --- ## Phase 45 — AppSettings.cs 클래스 파일 분리 (v2.3) ✅ 완료 > **목표**: 31개 클래스가 혼재된 AppSettings.cs (1,320줄)를 3개 파일로 분리. | 파일 | 줄 수 | 내용 | |------|-------|------| | `AppSettings.cs` | 564 | AppSettings·LauncherSettings·CustomThemeColors 등 17개 클래스 | | `AppSettings.LlmSettings.cs` | 481 | LlmSettings(408줄)·CodeSettings | | `AppSettings.AgentConfig.cs` | 284 | 권한·훅·이벤트·모델·프리셋 등 12개 설정 클래스 | - **메인 파일**: 1,320줄 → 564줄 (**57.3% 감소**) - **빌드**: 경고 0, 오류 0 --- ## Phase 46 — AgentLoopService·ChatWindow·SettingsViewModel·TemplateService·PlanViewerWindow 분리 (v2.3) ✅ 완료 > **목표**: 6개 대형 파일을 16개 파셜 파일로 분리. | 원본 파일 | 원본 줄 | 신규 파일 | 내용 | |-----------|---------|-----------|------| | `AgentLoopService.cs` | 1,334 → 846 | `AgentLoopService.HtmlReport.cs` (151) | HTML 자동저장, 텍스트→HTML 변환 | | | | `AgentLoopService.Verification.cs` (349) | 도구 실행 후 검증, 문서 생성 감지 | | `ChatWindow.TaskDecomposition.cs` | 1,170 → 307 | `ChatWindow.PlanViewer.cs` (474) | Plan 카드, 결정 버튼, 플랜 뷰어 제어 | | | | `ChatWindow.EventBanner.cs` (411) | 에이전트 이벤트 배너, 파일 퀵액션 | | `SettingsViewModel.Properties.cs` | 837 → 427 | `SettingsViewModel.LlmProperties.cs` (417) | LLM/에이전트 설정 바인딩 프로퍼티 | | `TemplateService.cs` | 734 → 179 | `TemplateService.Css.cs` (559) | 11개 CSS 스타일 상수 | | `PlanViewerWindow.cs` | 931 → 324 | `PlanViewerWindow.StepRenderer.cs` (616) | 단계 목록 렌더링, 드래그, 편집 | - **빌드**: 경고 0, 오류 0 --- ## Phase 47 — ChatWindow 추가 분리 (v2.3) ✅ 완료 > **목표**: ChatWindow 관련 대형 파일 추가 분리. | 원본 파일 | 원본 줄 | 신규 파일 | 내용 | |-----------|---------|-----------|------| | `ChatWindow.ResponseHandling.cs` | 741 → 269 | `ChatWindow.StreamingUI.cs` (303) | 스트리밍 컨테이너, 파싱, 토큰 | | | | `ChatWindow.ConversationExport.cs` (188) | 대화 분기, 커맨드 팔레트, 내보내기 | | `ChatWindow.PreviewAndFiles.cs` | 709 → ~340 | `ChatWindow.PreviewPopup.cs` (~230) | 프리뷰 탭 컨텍스트 메뉴, 팝업 | | `HelpDetailWindow.xaml.cs` | 673 → 254 | `HelpDetailWindow.Shortcuts.cs` (168) | 단축키 항목 목록 빌드 | | | | `HelpDetailWindow.Navigation.cs` (266) | 테마, 상단 메뉴, 카테고리 바, 페이지 이동 | | `SkillService.cs` | 661 → 386 | `SkillService.Import.cs` (203) | 스킬 내보내기/가져오기/도구명 매핑 | | `SkillDefinition.cs` | (신규) | `SkillDefinition.cs` (81) | SkillDefinition 클래스 독립 파일 | - **빌드**: 경고 0, 오류 0 --- ## Phase 48 — ChatWindow·WorkflowAnalyzer·SkillGallery 분리 (v2.3) ✅ 완료 > **목표**: 탭 전환, 드롭 액션, 워크플로우 타임라인, 스킬 상세 분리. | 원본 파일 | 원본 줄 | 신규 파일 | 내용 | |-----------|---------|-----------|------| | `ChatWindow.Controls.cs` | 595 → 372 | `ChatWindow.TabSwitching.cs` (232) | 탭 전환, Plan 모드 UI, 대화 복원 | | `ChatWindow.SlashCommands.cs` | 579 → 406 | `ChatWindow.DropActions.cs` (160) | 드래그&드롭 액션 팝업, 코드/데이터 확장자 | | `WorkflowAnalyzerWindow.Charts.cs` | 667 → 397 | `WorkflowAnalyzerWindow.Timeline.cs` (281) | 타임라인 노드, 배지, 상세, 요약 카드 | | `SkillGalleryWindow.xaml.cs` | 631 → ~430 | `SkillGalleryWindow.SkillDetail.cs` (197) | 스킬 상세 팝업 | - **빌드**: 경고 0, 오류 0 --- ## Phase 49 — ChatWindow.Sending·MarkdownRenderer·MessageRendering 분리 (v2.3) ✅ 완료 > **목표**: 전송 로직, 마크다운 렌더러, 메시지 렌더링 분리. | 원본 파일 | 원본 줄 | 신규 파일 | 내용 | |-----------|---------|-----------|------| | `ChatWindow.Sending.cs` | 720 → ~300 | `ChatWindow.MessageEdit.cs` (~180) | 편집 모드 진입, SubmitEditAsync | | | | `ChatWindow.StreamingTimers.cs` (~58) | 커서/경과시간/타이핑 타이머 | | `MarkdownRenderer.cs` | 621 → 405 | `MarkdownRenderer.CodeBlock.cs` (218) | 코드 블록 UI, 파일저장, 전체화면 | | `ChatWindow.MessageRendering.cs` | 522 → 220 | `ChatWindow.Animations.cs` (172) | 애니메이션 헬퍼, 체크 아이콘 | | | | `ChatWindow.FeedbackButtons.cs` (121) | 액션 버튼, 좋아요/싫어요 피드백 | - **빌드**: 경고 0, 오류 0 --- ## Phase 50 — PlanViewerWindow·SettingsWindow 추가 분리 (v2.3) ✅ 완료 > **목표**: 버튼 빌더, AI 토글, MCP 고급 설정 분리. | 원본 파일 | 원본 줄 | 신규 파일 | 내용 | |-----------|---------|-----------|------| | `PlanViewerWindow.StepRenderer.cs` | 616 → 425 | `PlanViewerWindow.EditButtons.cs` (197) | 승인/실행/닫기 버튼, 편집 입력 | | `SettingsWindow.AgentConfig.cs` | 608 → 303 | `SettingsWindow.AiToggle.cs` (316) | AI 활성화·네트워크 모드 토글 | | `SettingsWindow.AgentHooks.cs` | 605 → 334 | `SettingsWindow.McpAdvanced.cs` (271) | MCP 서버 관리, 폴백 모델, 고급 설정 | - **빌드**: 경고 0, 오류 0 --- ## Phase 51 — 9개 대형 파일 → 19개 파일 분리 (v2.3) ✅ 완료 > **목표**: 핸들러·서비스·모델 대규모 분리. | 원본 파일 | 원본 줄 | 신규 파일 | 내용 | |-----------|---------|-----------|------| | `SettingsWindow.Tools.cs` | 605 → 238 | `SettingsWindow.SkillListPanel.cs` (295) | 스킬 목록 섹션, 스킬 그룹 카드 | | `LauncherWindow.Keyboard.cs` | 593 → 454 | `LauncherWindow.ShortcutHelp.cs` (139) | 단축키 도움말, 토스트, 특수 액션 | | `CalculatorHandler.cs` | 566 → ~240 | `UnitConverter.cs` (152) | 단위 변환 클래스 독립 파일 | | | | `MathEvaluator.cs` (183) | 수식 파서 클래스 독립 파일 | | `EmojiHandler.cs` | 553 → 70 | `EmojiHandler.Data.cs` (490) | 이모지 데이터베이스 배열 | | `DocumentPlannerTool.cs` | 598 → 324 | `DocumentPlannerTool.Generators.cs` (274) | HTML/DOCX/Markdown 생성, 섹션 계획 | | `DocumentReaderTool.cs` | 571 → 338 | `DocumentReaderTool.Formats.cs` (233) | BibTeX/RIS/DOCX/XLSX/Text/Helpers | | `CodeIndexService.cs` | 588 → 285 | `CodeIndexService.Search.cs` (199) | 검색, TF-IDF, 토큰화, Dispose | | `ClipboardHistoryService.cs` | 575 → 458 | `ClipboardHistoryService.ImageCache.cs` (120) | 이미지 캐시 유틸리티 | | `IndexService.cs` | 568 → 412 | `IndexService.Helpers.cs` (163) | 경로 탐색 헬퍼, 검색 캐시 | - **빌드**: 경고 0, 오류 0 --- ## Phase 52 — 7개 파일 추가 분리 (v2.3) ✅ 완료 > **목표**: 스트리밍, 스킬, 핸들러, 모델, 뷰 분리 마무리. | 원본 파일 | 원본 줄 | 신규 파일 | 내용 | |-----------|---------|-----------|------| | `LlmService.Streaming.cs` | 516 → 256 | `LlmService.GeminiClaude.cs` (260) | Gemini·Claude 스트리밍 메서드 | | `DocxSkill.cs` | 543 → 158 | `DocxSkill.Builders.cs` (290) | 11개 Word 문서 빌더 헬퍼 | | `ChartSkill.cs` | 537 → 174 | `ChartSkill.Renderers.cs` (280) | 7개 차트 렌더러, 헬퍼, Dataset | | `ScreenCaptureHandler.cs` | 637 → 241 | `ScreenCaptureHandler.Helpers.cs` (310) | 스크롤·영역 캡처, 이미지 처리 헬퍼 | | `SystemInfoHandler.cs` | 509 → 352 | `SystemInfoHandler.Helpers.cs` (161) | 8개 시스템 정보 헬퍼, StarInfoHandler | | `AppSettings.cs` | 564 → 309 | `AppSettings.Models.cs` (233) | 14개 설정 모델 클래스 | | `SkillEditorWindow.xaml.cs` | 528 → 303 | `SkillEditorWindow.PreviewSave.cs` (226) | 미리보기·저장·편집 모드 로드 | | `PreviewWindow.xaml.cs` | 505 → 317 | `PreviewWindow.Content.cs` (170) | 콘텐츠 로드, 타이틀바 핸들러 | ### 코드 품질 최종 결과 (Phase 46~52) | 구분 | Phase 45 이전 | Phase 52 완료 후 | |------|-------------|----------------| | 500줄 이상 파일 | 40+ | **3개** (AgentLoopService 846줄·LauncherWindow.xaml 578줄·TemplateService.Css 559줄) | | 허용 이유 | — | RunAsync 679줄 단일 메서드 / 345줄 애니메이션 메서드 / CSS 데이터 상수 | | 신규 파일 수 | — | Phase 46~52 동안 **45개** 신규 partial 파일 생성 | - **빌드**: 경고 0, 오류 0 --- --- ## Phase 17-UI-A — AgentSettingsPanel 완전 통합 (v1.8.0) ✅ 완료 > **목표**: 채팅 창 우측 AgentSettingsPanel을 주 설정 엔트리로 승격. > 기어 버튼 클릭 → 인라인 패널 직접 열기 (Shift+Click 불필요). > ToolRegistry 의존 제거, 탭 전환 연동 완성. ### 변경 파일 | 파일 | 변경 내용 | |------|----------| | `ChatWindow.MoodMenu.cs` | `BtnSettings_Click`: Shift 조건 제거 → 직접 `ToggleSettingsPanel()` 호출; Ctrl+클릭만 SettingsWindow 열기 | | `ChatWindow.MoodMenu.cs` | `ToggleSettingsPanel()`: `_toolRegistry` 패널에 전달; `SettingsChanged` 이벤트 연결 → `UpdateModelLabel()` + `UpdateAnalyzerButtonVisibility()` 반영 | | `ChatWindow.MoodMenu.cs` | `OnSettingsPanelChanged()` 신규: 패널 설정 변경 시 ChatWindow UI 즉시 갱신 | | `ChatWindow.TabSwitching.cs` | `UpdateTabUI()`: 설정 패널 열림 상태이면 `UpdateActiveTab()` 자동 호출 | | `ChatWindow.xaml` | 설정 버튼 ToolTip: "설정 패널 (Ctrl+클릭: 전역 설정)" | | `AgentSettingsPanel.xaml.cs` | `LoadFromSettings()`: `ToolRegistry? toolRegistry` 매개변수 추가 | | `AgentSettingsPanel.xaml.cs` | `BuildToolToggles()`: 외부 레지스트리 우선, 없으면 `ToolRegistry.CreateDefault()` 폴백 | ### 개선 효과 - ⚙ 버튼 한 번 클릭으로 에이전트 설정 패널 바로 접근 - 탭 전환(Chat/Cowork/Code) 시 패널 탭별 설정(검증 강제, LSP 등) 자동 반영 - 설정 변경 즉시 ChatWindow 모델 라벨·분석기 버튼 상태 갱신 - 도구 토글 패널: 실제 ChatWindow ToolRegistry 참조로 정확한 목록 표시 - **빌드**: 경고 0, 오류 0 --- ## Phase 17-UI-B — 헤더 바 모델·권한 칩 추가 (v1.8.0) ✅ 완료 > **목표**: 서브 헤더 바(Row 1)에 현재 모델과 권한 모드를 항상 노출. > 클릭 시 각각 모델 선택기·권한 팝업을 인라인으로 열어 UX 일관성 강화. ### 변경 파일 | 파일 | 변경 내용 | |------|----------| | `ChatWindow.xaml` | 서브 바 우측에 `ModelHeaderChip` Button 추가: 브레인 아이콘 + `ModelHeaderLabel` TextBlock | | `ChatWindow.xaml` | 서브 바 우측에 `PermissionHeaderChip` Button 추가: 잠금 아이콘(#4FC3F7) + `PermissionHeaderLabel` TextBlock | | `ChatWindow.ModelSelector.cs` | `UpdateModelLabel()`: `ModelHeaderLabel` 동기 갱신 코드 추가 | | `ChatWindow.PermissionMenu.cs` | `UpdatePermissionUI()`: `PermissionHeaderLabel` 동기 갱신 코드 추가 | | `ChatWindow.PermissionMenu.cs` | `PermissionHeaderChip_Click()` 신규: 팝업 PlacementTarget을 헤더 칩으로 교체 후 기존 권한 팝업 호출 | | `ChatWindow.xaml.cs` | `Loaded` 핸들러: `UpdatePermissionUI()` 초기 호출 추가 | ### 개선 효과 - 서브 헤더 바에서 현재 모델(예: "Claude · Sonnet 4.6")과 권한 모드(예: "Ask") 상시 확인 가능 - `ModelHeaderChip` 클릭 → 기존 모델 선택 팝업 즉시 열림 - `PermissionHeaderChip` 클릭 → 권한 팝업이 헤더 칩 기준 하단으로 올바르게 열림 - 탭 전환·설정 변경 시 양쪽 칩(폴더 바 + 헤더 바) 동시 갱신 - **빌드**: 경고 0, 오류 0 --- ## Phase 17-UI-C — AgentSessionHeaderBar UserControl 통합 (v1.8.0) ✅ 완료 > **목표**: 기존 Row 1 서브 바의 모델·Plan·권한 칩을 `AgentSessionHeaderBar` UserControl로 교체. > 투명 배경으로 기존 레이아웃에 임베드. 이벤트 기반 위임으로 ChatWindow 기존 로직 재사용. ### 변경 파일 | 파일 | 변경 내용 | |------|----------| | `AgentSessionHeaderBar.xaml` | 외부 Border를 `Background="Transparent" BorderThickness="0"`으로 수정. 높이 42→38px. `x:Name="PlanIcon"`, `x:Name="PermIcon"` 추가. | | `AgentSessionHeaderBar.xaml.cs` | `SetPlanMode()`: `PlanIcon` x:Name 직접 참조. `ChipPermission_Click`: 이벤트 발행만 (팝업은 ChatWindow 처리). `SetPermissionMode()`: PermIcon 색상 업데이트. | | `ChatWindow.xaml` | Row 1 우측에서 `ModelHeaderChip`, `BtnPlanMode`, `PermissionHeaderChip` 제거 → `` 추가. | | `ChatWindow.SessionHeaderBar.cs` | 신규: `InitSessionHeaderBar()` — ModelChipClicked·PlanModeChanged·PermissionModeChanged·SettingsRequested 이벤트 구독. | | `ChatWindow.ModelSelector.cs` | `UpdateModelLabel()`: `SessionHeaderBar?.SetModel()` 동기화. | | `ChatWindow.PermissionMenu.cs` | `UpdatePermissionUI()`: `SessionHeaderBar?.SetPermissionMode()` 동기화. | | `ChatWindow.TabSwitching.cs` | `UpdateTabUI()`: `SessionHeaderBar?.SetTabLabel()` 동기화. `UpdatePlanModeUI()`: SessionHeaderBar 전용으로 단순화. | | `ChatWindow.xaml.cs` | Loaded: `InitSessionHeaderBar()` 호출 추가. | - **빌드**: 경고 0, 오류 0 --- ## Phase 17-UI-D — AgentSidebarView UserControl 통합 (v1.8.0) ✅ 완료 > **목표**: 기존 `SidebarPanel` Border(인라인 XAML 110줄)를 `AgentSidebarView` UserControl로 교체. > 34개 기존 ChatWindow 파셜 파일이 사이드바 내부 요소를 무수정으로 참조할 수 있도록 프록시 패턴 적용. ### 변경 파일 | 파일 | 변경 내용 | |------|----------| | `AgentSidebarView.xaml` | 7행 구조로 재작성: 헤더(로고+새대화) / 탭세그먼트 / 검색 / 카테고리드롭다운 / 대화목록 / 삭제 / 사용자계정. | | `AgentSidebarView.xaml.cs` | 완전 재작성: `internal` 프록시 프로퍼티 8개 + `SidebarTabChanged`/`NewChatRequested`/`DeleteAllRequested`/`CategoryDropClicked`/`SearchTextChanged` 이벤트. | | `ChatWindow.xaml` | `` 인라인 110줄 → `` 1줄 대체. | | `ChatWindow.SidebarCompat.cs` | 신규: 8개 계산 프로퍼티(`ConversationPanel`, `SearchBox`, `CategoryIcon`, `CategoryLabel`, `BtnCategoryDrop`, `UserInitialSidebar`, `UserNameText`, `UserPcText`) + `InitSidebarEvents()`. | | `ChatWindow.PermissionMenu.cs` | `BtnToggleSidebar_Click()`: `SidebarPanel.Visibility` → `Sidebar.Visibility`. `PermissionHeaderChip_Click()`: `PermissionHeaderChip` → `SessionHeaderBar`. | | `ChatWindow.TabSwitching.cs` | 탭 핸들러 3개: `Sidebar?.SetActiveTab()` 동기화 추가. `UpdatePlanModeUI()`: 죽은 코드 제거. | | `ChatWindow.xaml.cs` | Loaded: `InitSidebarEvents()` 호출 추가. | - **빌드**: 경고 0, 오류 0 --- ## Phase 17-UI-E — AgentInputArea 툴바 통합 (v1.8.0) ✅ 완료 > **목표**: `AgentInputArea` UserControl에 `IsToolbarOnly` 모드 추가. > 기존 InputBox 위에 @ / 스킬 / 첨부 툴바 행을 추가하여 Claude.ai 스타일 입력 경험 제공. ### 변경 파일 | 파일 | 변경 내용 | |------|----------| | `AgentInputArea.xaml` | `x:Name="OuterBorder"`, `x:Name="InputTextBoxRow"`, `x:Name="ChipsSendRow"` 추가. | | `AgentInputArea.xaml.cs` | `IsToolbarOnly` 의존성 프로퍼티 추가. `ApplyToolbarOnlyMode()`: TextBox/칩 행 Collapsed, 외부 Border 투명화. | | `ChatWindow.xaml` | InputBorder Grid에 Row 0 추가(`ctrl:AgentInputArea x:Name="InputToolbar" IsToolbarOnly="True"`). 기존 Row 0→1, 1→2, 2→3 시프트. | | `ChatWindow.InputToolbar.cs` | 신규: `InitInputToolbar()` — MentionRequested(@삽입), SkillRequested(/삽입), AttachRequested(BtnAttach_Click 위임). | | `ChatWindow.xaml.cs` | Loaded: `InitInputToolbar()` 호출 추가. | - **빌드**: 경고 0, 오류 0 --- ## Phase 17-A — Reflexion 강화 (v1.8.0) ✅ 완료 > **목표**: 성공·실패 모두 구조화된 자기평가 저장 → 동일 작업 유형 재실행 시 자동 참고 ### 변경 파일 | 파일 | 변경 내용 | |------|----------| | `ReflexionService.cs` | `ReflexionEvaluatorService` 신규 추가: LLM 기반 자기평가(점수·강점·약점·교훈 추출), 규칙 기반 폴백 엔트리. `ReflexionRepository.BuildContextPromptAsync()` maxEntries 파라미터 추가. | | `AgentLoopService.Reflexion.cs` (신규, 82줄) | `InjectReflexionContextAsync()` — 세션 시작 시 과거 교훈을 시스템 메시지에 주입. `FireAndForgetReflexionEval()` — 세션 완료 후 비동기 자기평가 저장. | | `AgentLoopService.cs` | `RunAsync()` 루프 시작 전: `InjectReflexionContextAsync()` 호출. `finally` 블록: `FireAndForgetReflexionEval()` 호출. | | `AgentSettingsPanel.xaml` | "자기성찰 메모리" 섹션 추가: 활성화 토글, 성공 시만 평가 토글, 최대 참고 교훈 수 슬라이더. | | `AgentSettingsPanel.xaml.cs` | `LoadFromSettings()`: Reflexion 설정 초기화. `ChkReflexion_Changed()`, `ChkReflexionSuccessOnly_Changed()`, `SliderReflexionMaxEntries_ValueChanged()` 핸들러 추가. | ### 구현 세부사항 - **ReflexionEvaluatorService**: `$$"""..."""` raw string으로 평가 프롬프트 생성, JSON 블록 추출 후 역직렬화 - **작업 유형 분류**: `TaskTypeClassifier.Classify()` — 8개 카테고리 (code_generation, file_refactor, document, analysis, search, test, git, debug, general) - **저장 위치**: `%APPDATA%\AxCopilot\reflexion\.jsonl` (JSONL 형식, 최신 N개 역순 조회) - **비동기 처리**: fire-and-forget — 루프 취소(`CancellationToken`) 영향 없음 - **빌드**: 경고 0, 오류 0 --- ## Phase 17-B — 구조화된 태스크 상태 + 이벤트 로그 통합 (v1.8.0) ✅ 완료 > **목표**: 대화 압축 시에도 유지되는 Working Memory + JSONL 이벤트 로그 완전 통합 ### 변경 파일 | 파일 | 변경 내용 | |------|----------| | `AgentLoopService.TaskState.cs` (신규, 96줄) | `InitTaskStateAsync()` — 세션 시작 시 TaskState 초기화·현재 작업 기록. `TrackToolFile()` — 도구 성공 시 파일 경로 참조 목록 추가(fire-and-forget). `InjectTaskStateContext()` — 압축 전 Working Memory를 시스템 메시지에 in-place 주입. `UpdateTaskStateSummaryAsync()` — 압축 완료 후 컨텍스트 요약 갱신(fire-and-forget). | | `AgentLoopService.cs` | `RunAsync()`: UserMessage 이벤트 기록 + `InitTaskStateAsync()` 호출. 압축 블록: `InjectTaskStateContext()` 호출 + CompactionCompleted/CompactionTriggered 이벤트 로그. LLM 응답 후: AssistantMessage 이벤트 기록. | | `AgentLoopService.Execution.cs` | 도구 성공 시 `TrackToolFile(result.FilePath)` 호출 — 파일 경로 Working Memory 추적. | ### 구현 세부사항 - **TaskState 지연 초기화**: `_taskState ??= new TaskStateService()` — 사용 시 첫 생성, 세션 간 `InitializeAsync(sessionId)`로 상태 재로드 - **In-place 주입**: `InjectTaskStateContext()`가 `## 현재 작업 상태 (Working Memory)` 마커로 기존 섹션 탐지 후 교체 → 중복 방지 - **이벤트 커버리지**: SessionStart/End(기존), UserMessage, AssistantMessage, ToolRequest/Result(기존), CompactionTriggered, CompactionCompleted 모두 JSONL 기록 - **설정 연동**: `LlmSettings.EnableTaskState` + `LlmSettings.EventLog.Enabled` 체크 후 동작 - **저장 위치**: `%APPDATA%\AxCopilot\sessions\{sessionId}\task_state.json` + `events.jsonl` - **빌드**: 경고 0, 오류 0 --- --- ## Phase 17-C — 훅 시스템 고도화 (v1.8.0) ✅ 완료 > **목표**: ExtendedHookRunner를 에이전트 라이프사이클 전 구간에 연결 + Prompt 모드 구현 ### 변경 파일 | 파일 | 변경 내용 | |------|----------| | `AppSettings.AgentConfig.cs` | `ExtendedHooksConfig`에 4개 이벤트 추가: `preToolUse`, `postToolUse`, `postToolUseFailure`, `agentStop` | | `AgentLoopService.ExtendedHooks.cs` (신규, 150줄) | `RunExtendedEventAsync()` — 이벤트 훅 실행·결과 적용. `GetExtendedHooks()` — 설정에서 런타임 엔트리 조회. `ConvertToRuntimeEntries()` — ExtendedHookEntryConfig → ExtendedHookEntry 변환. `ApplyExtendedHookResult()` — additionalContext 시스템 메시지 in-place 주입. | | `ExtendedHookRunner.cs` | `RunEventAsync()` + `ExecuteSingleAsync()`에 `LlmService? llm` 파라미터 추가. `RunPromptHookAsync()` 신규 구현: `{{tool_name}}` 등 변수 치환, LLM 호출, 차단 신호 감지. `using AxCopilot.Models` 추가. | | `AgentLoopService.cs` | TaskState init 후: `SessionStart` 훅(fire-and-forget) + `UserPromptSubmit` 훅(차단 시 즉시 반환). 적극적 압축 직전: `PreCompact` 훅. 압축 완료 후: `PostCompact` 훅(fire-and-forget). `finally` 블록: `SessionEnd` + `AgentStop` 훅(fire-and-forget). | | `AgentLoopService.Execution.cs` | `RunToolHooksAsync()` 개선: 레거시 AgentHookRunner 유지 + ExtendedHookRunner `PreToolUse`/`PostToolUse`/`PostToolUseFailure` 이벤트 추가 실행. | ### 구현 세부사항 - **이벤트 커버리지**: SessionStart, SessionEnd, AgentStop, UserPromptSubmit, PreToolUse, PostToolUse, PostToolUseFailure, PreCompact, PostCompact, FileChanged, PermissionRequest — 11종 이벤트 완전 연결 - **실행 모드**: Command(bat/ps1), Http(POST webhook), Prompt(LLM 평가) 구현. Agent 모드는 Phase 18-A 예정. - **Prompt 모드**: `{{tool_name}}`, `{{tool_input}}`, `{{tool_output}}`, `{{user_message}}`, `{{event}}`, `{{session_id}}` 변수 치환. 응답에 block/deny/차단 포함 시 Block=true. - **차단 처리**: UserPromptSubmit 훅 Block=true 시 즉시 `"⚠ 요청이 훅 정책에 의해 차단되었습니다."` 반환 - **HookFired 이벤트 로그**: 훅 실행 시 JSONL에 eventKind, hookCount, blocked 기록 - **하위 호환**: 레거시 AgentHooks(command 스크립트) 병행 유지 - **빌드**: 경고 0, 오류 0 --- --- ## Phase 17-D — 스킬 시스템 고도화 (v1.8.0) ✅ 완료 > **목표**: paths: glob 패턴 기반 스킬 자동 주입 + context:fork 서브에이전트 격리 실행 ### 변경 파일 | 파일 | 변경 내용 | |------|----------| | `AgentLoopService.Skills.cs` (신규, 95줄) | `InjectPathBasedSkills()` — 파일 경로 매칭 스킬 시스템 메시지 in-place 주입, SkillActivated 이벤트 로그. `RunSkillInForkAsync()` — context:fork 스킬 격리 LLM 실행, SkillCompleted 이벤트 로그. | | `SkillManagerTool.cs` | `SetForkRunner()` 콜백 필드 추가. `exec` 액션 + `arguments` 파라미터 추가. `ExecSkillAsync()`: PrepareSkillBodyAsync 준비 → IsForkContext 시 fork runner 호출 → 일반 스킬은 시스템 프롬프트 반환. | | `AgentLoopService.cs` | 생성자: `SkillManagerTool`에 `RunSkillInForkAsync` fork runner 주입. | | `AgentLoopService.Execution.cs` | 도구 성공 후 `InjectPathBasedSkills(result.FilePath, messages)` 호출. | ### 구현 세부사항 - **paths: 자동 주입**: 파일 도구(`file_read`, `file_write` 등) 성공 후 `result.FilePath`로 GlobMatcher 매칭 → 해당 스킬의 시스템 프롬프트를 메시지에 자동 주입. `## 현재 파일에 자동 적용된 스킬` 마커로 in-place 교체. - **context:fork**: `SkillManagerTool.exec` 호출 시 `skill.IsForkContext == true`이면 격리된 LLM 컨텍스트(도구 없음)에서 실행. 결과를 `[Fork 스킬 결과]` 형식으로 반환. - **이벤트 로그**: `SkillActivated`(paths: 매칭 시), `SkillCompleted`(fork 실행 완료 시) JSONL 기록 - **DI 패턴**: `DelegateAgentTool.SetSubAgentRunner` 패턴 동일 적용 — AgentLoopService 생성자에서 콜백 주입 - **빌드**: 경고 0, 오류 0 --- 최종 업데이트: 2026-04-04 (Phase 22~52 + Phase 17-UI-A~E + Phase 17-A~D 구현 완료)