Files
AX-Copilot/docs/NEXT_ROADMAP.md
lacvet 0c997f0149 [Phase 39] FontFamily 캐싱 + LauncherWindow 파셜 클래스 분할
- ThemeResourceHelper에 CascadiaCode/ConsolasCode/ConsolasCourierNew 정적 필드 추가
- 25개 파일, 89개 new FontFamily(...) 호출을 정적 캐시 참조로 교체
- LauncherWindow.xaml.cs (1,563줄) → 5개 파셜 파일로 분할 (63% 감소)
  - LauncherWindow.Theme.cs (116줄): ApplyTheme, 커스텀 딕셔너리 빌드
  - LauncherWindow.Animations.cs (153줄): 무지개 글로우, 애니메이션 헬퍼
  - LauncherWindow.Keyboard.cs (593줄): 단축키 20종, ShowToast, IME 검색
  - LauncherWindow.Shell.cs (177줄): Shell32, SendToRecycleBin, 클릭 핸들러
- 빌드: 경고 0, 오류 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-03 18:54:35 +09:00

172 KiB

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 레이아웃 구조

<!-- 전체 레이아웃: Grid 3열 -->
<Window x:Class="AxCopilot.Views.AgentWindow.AgentWindow"
        Style="{StaticResource ThemedWindow}">
  <Grid>
    <Grid.ColumnDefinitions>
      <!-- 사이드바: 240px 또는 48px (애니메이션) -->
      <ColumnDefinition x:Name="SidebarColumn" Width="240"/>
      <!-- 메인 영역 -->
      <ColumnDefinition Width="*"/>
      <!-- 인라인 설정 패널: 0 또는 300px (슬라이드) -->
      <ColumnDefinition x:Name="SettingsColumn" Width="0"/>
    </Grid.ColumnDefinitions>

    <!-- 좌측 사이드바 -->
    <views:AgentSidebarView Grid.Column="0"
        DataContext="{Binding Sidebar}"
        IsExpanded="{Binding IsSidebarExpanded}"/>

    <!-- 메인 영역: 상단 헤더 + 채팅 + 입력 -->
    <Grid Grid.Column="1">
      <Grid.RowDefinitions>
        <RowDefinition Height="48"/>   <!-- 세션 헤더 바 -->
        <RowDefinition Height="*"/>    <!-- 채팅 뷰 -->
        <RowDefinition Height="Auto"/> <!-- 입력 영역 -->
      </Grid.RowDefinitions>
      <views:AgentSessionHeaderBar Grid.Row="0"
          DataContext="{Binding SessionHeader}"/>
      <views:AgentChatView Grid.Row="1"
          ItemsSource="{Binding Messages}"/>
      <views:AgentInputArea Grid.Row="2"
          DataContext="{Binding InputArea}"/>
    </Grid>

    <!-- 우측 인라인 설정 패널 -->
    <views:AgentInlineSettingsPanel Grid.Column="2"
        DataContext="{Binding InlineSettings}"
        Visibility="{Binding IsSettingsPanelOpen, Converter={...}}"/>
  </Grid>
</Window>

애니메이션 스펙:

  • 사이드바 확장/축소: 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

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<ChatMessageViewModel> 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

public class AgentSessionHeaderViewModel : ViewModelBase
{
    // ── 모델 선택 ──
    public ObservableCollection<string> 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):

<!-- 탭 선택 -->
<Border Style="{StaticResource TabHeaderStyle}" MouseLeftButtonUp="...">
  <StackPanel Orientation="Horizontal">
    <TextBlock Text="Chat" FontSize="13"
        Foreground="{DynamicResource PrimaryText}"/>
  </StackPanel>
</Border>

<!-- 플랜 모드 토글 버튼 -->
<Border Style="{StaticResource HeaderButtonStyle}"
        MouseLeftButtonUp="{...}"
        Background="{Binding PlanMode, Converter={StaticResource PlanModeToColorConverter}}">
  <TextBlock Text="{Binding PlanMode, Converter={StaticResource PlanModeToLabelConverter}}"
             FontSize="12" Foreground="{DynamicResource PrimaryText}"/>
</Border>

<!-- 실행 상태 스피너 + 텍스트 -->
<StackPanel Orientation="Horizontal"
            Visibility="{Binding IsRunning, Converter={StaticResource BoolToVisConverter}}">
  <controls:SpinnerIcon Width="16" Height="16"/>
  <TextBlock Text="{Binding StatusText}" FontSize="12"
             Foreground="{DynamicResource SecondaryText}" Margin="6,0,0,0"/>
</StackPanel>

17-UI-5: AgentSidebarViewModel

public class AgentSidebarViewModel : ViewModelBase
{
    // ── 대화 이력 ──
    public ObservableCollection<SessionSummaryViewModel> RecentSessions { get; }
    public SessionSummaryViewModel? SelectedSession { get; set; }

    // ── 프리셋 목록 ──
    public ObservableCollection<PresetViewModel> 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 설정
- 암호화/보안 설정
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<SkillChipViewModel> ActiveSkills { get; }
    public ICommand ManageSkillsCommand { get; }

    // ── MCP 상태 요약 ──
    public ObservableCollection<McpStatusChipViewModel> McpServers { get; }

    // 설정 변경 즉시 SettingsService.Save() 호출
    // 탭 전환 시 해당 탭 설정으로 바인딩 교체
    public void SwitchToTab(AgentTab tab) { ... }
}

17-UI-7: AgentInputArea

public class AgentInputAreaViewModel : ViewModelBase
{
    public string InputText { get; set; }
    public bool IsMultiline { get; set; }           // Shift+Enter로 전환
    public ObservableCollection<AttachedFileViewModel> AttachedFiles { get; }
    public bool IsAtMentionOpen { get; set; }        // @ 입력 시 팝업
    public bool IsSlashMenuOpen { get; set; }        // / 입력 시 스킬 메뉴
    public ObservableCollection<SkillSuggestionViewModel> 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) 핵심:

<Border Background="{DynamicResource ItemBackground}" CornerRadius="12"
        Margin="12,0,12,12" Padding="12,8">
  <Grid>
    <Grid.RowDefinitions>
      <RowDefinition Height="Auto"/>  <!-- 첨부파일 칩 행 -->
      <RowDefinition Height="Auto"/>  <!-- 텍스트 입력 행 -->
      <RowDefinition Height="Auto"/>  <!-- 하단 버튼 행 -->
    </Grid.RowDefinitions>

    <!-- 첨부파일 칩 (파일 있을 때만 표시) -->
    <ItemsControl Grid.Row="0" ItemsSource="{Binding AttachedFiles}"
                  Visibility="{Binding AttachedFiles.Count, Converter={...}}">
      <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate><WrapPanel/></ItemsPanelTemplate>
      </ItemsControl.ItemsPanel>
    </ItemsControl>

    <!-- 텍스트 입력 -->
    <TextBox Grid.Row="1" x:Name="InputBox"
             Text="{Binding InputText, UpdateSourceTrigger=PropertyChanged}"
             AcceptsReturn="{Binding IsMultiline}"
             MaxHeight="200" TextWrapping="Wrap"
             Background="Transparent" BorderThickness="0"
             Foreground="{DynamicResource PrimaryText}" FontSize="14"
             PreviewKeyDown="InputBox_PreviewKeyDown"/>

    <!-- 하단 버튼 행: 📎 파일 | /스킬 | @ 멘션 | [모델칩] | [보내기] -->
    <Grid Grid.Row="2">
      <StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
        <!-- 파일 첨부 -->
        <Border Style="{StaticResource InputActionButton}"
                MouseLeftButtonUp="...">
          <TextBlock Text="&#xE16D;" FontFamily="Segoe MDL2 Assets"
                     Foreground="#F59E0B"/>
        </Border>
        <!-- 슬래시 스킬 -->
        <Border Style="{StaticResource InputActionButton}"
                MouseLeftButtonUp="...">
          <TextBlock Text="/" FontSize="16" FontWeight="Bold"
                     Foreground="{DynamicResource AccentColor}"/>
        </Border>
      </StackPanel>

      <!-- 모델 칩 + 보내기 -->
      <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
        <Border Style="{StaticResource ModelChipStyle}">
          <TextBlock Text="{Binding ModelName}" FontSize="11"
                     Foreground="{DynamicResource SecondaryText}"/>
        </Border>
        <Border Style="{StaticResource SendButtonStyle}"
                MouseLeftButtonUp="...">
          <TextBlock Text="&#xE724;" FontFamily="Segoe MDL2 Assets"
                     Foreground="{DynamicResource AccentColor}"/>
        </Border>
      </StackPanel>
    </Grid>
  </Grid>
</Border>

17-UI-8: 통합 포인트

기존 클래스 변경 내용
AgentChatWindow.xaml.cs AgentWindow로 교체 (리네임 + 재구성). App.xaml.csOpenAgentWindow() 호출부 교체
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: 핵심 신규 클래스

// 반성 메모리 엔트리
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<IReadOnlyList<ReflexionEntry>> GetByTaskTypeAsync(string taskType, int limit = 5);
    Task<string> BuildContextPromptAsync(string taskType);
    // 저장 위치: %APPDATA%\AxCopilot\reflexion\<taskType>.jsonl
}

// 반성 메모리 구현
public class JsonlReflexionRepository : IReflexionRepository
{
    private readonly string _baseDir; // %APPDATA%\AxCopilot\reflexion\
    public Task SaveAsync(ReflexionEntry entry) { ... }
    public Task<IReadOnlyList<ReflexionEntry>> GetByTaskTypeAsync(string taskType, int limit = 5) { ... }
    public Task<string> BuildContextPromptAsync(string taskType)
    {
        // 최근 5개 엔트리를 시스템 프롬프트 삽입용 텍스트로 조합
        // "이전 유사 작업에서 배운 점: ..."
    }
}

// 자기평가 생성 서비스
public class ReflexionEvaluatorService
{
    private readonly ILlmClient _llm;
    private readonly IReflexionRepository _repo;

    // AgentLoopService.Completed 이벤트에서 호출
    public Task<ReflexionEntry> 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<string, string[]> _keywords = ...;
}

17-A-2: AgentLoopService 통합 포인트

// AgentLoopService.cs 에 추가
public class AgentLoopService
{
    // 기존 필드에 추가
    private readonly ReflexionEvaluatorService _reflexion;
    private readonly IReflexionRepository _reflexionRepo;
    private readonly TaskTypeClassifier _taskClassifier;

    // RunAsync() 시작 시: 반성 컨텍스트 주입
    private async Task<string> 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)

// 기존 PostToolVerificationService 확장
public class PostToolVerificationService
{
    // 기존: bash, process 도구 후 검증
    // 추가: script_create, file_edit 도구 후 검증
    private static readonly HashSet<string> _verifiableTools = new()
    {
        "bash", "process",
        "script_create",   // 신규
        "file_edit",       // 신규
        "file_write",      // 신규
    };

    // 검증 체크리스트 (사용자 편집 가능)
    // 저장: %APPDATA%\AxCopilot\verification_checklist.json
    public Task<VerificationChecklist> 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: 설정 스키마 변경

// 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)

// 세션 전체에 걸쳐 유지되는 구조화된 작업 상태
public class TaskState
{
    public string SessionId { get; init; }
    public string CurrentTask { get; set; }          // 현재 수행 중인 작업 요약
    public List<string> ReferencedFiles { get; set; } // 이번 세션에서 참조한 파일들
    public List<DecisionLogEntry> DecisionLog { get; set; } // 의사결정 이력
    public Dictionary<string, string> KeyFacts { get; set; } // 핵심 사실 (key→value)
    public string ContextSummary { get; set; }        // 대화 압축 시 갱신되는 요약

    // 직렬화: %APPDATA%\AxCopilot\sessions\<sessionId>\task_state.json
    public Task SaveAsync(string baseDir) { ... }
    public static Task<TaskState> 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 이벤트 로그

// 모든 에이전트 이벤트의 구조화된 기록
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\<sessionId>\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<AgentEventRecord> ReadAllAsync() { ... }
    public IAsyncEnumerable<AgentEventRecord> 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: 설정 스키마 변경

// 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: 훅 이벤트 열거형 확장

// 기존 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 패턴)

// 훅 실행기 인터페이스 (Strategy)
public interface IHookExecutor
{
    HookType Type { get; }
    Task<HookResult> 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<string, string> 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<string>? 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<HookResult> 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<HookResult> 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<HookResult> ExecuteAsync(HookContext context, CancellationToken ct)
    {
        // 격리된 미니 에이전트 루프 실행
        // 파일 읽기·명령 실행·검증 가능
        // 최대 반복: 3회 (하드코딩 또는 설정)
        // 결과를 HookResult로 변환
    }
}

// 훅 실행기 팩토리 (Factory 패턴)
public class HookExecutorFactory
{
    private readonly IReadOnlyDictionary<HookType, IHookExecutor> _executors;

    public HookExecutorFactory(
        CommandHookExecutor cmd,
        HttpHookExecutor http,
        PromptHookExecutor prompt,
        AgentHookExecutor agent)
    {
        _executors = new Dictionary<HookType, IHookExecutor>
        {
            [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: 훅 속성 모델 확장

// 기존 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 확장

public class HookRunnerService
{
    private readonly HookExecutorFactory _executorFactory;
    private readonly HookConfigRepository _configRepo;
    private readonly FileWatcherService _fileWatcher; // FileChanged 훅용

    // ── 기존 메서드 시그니처 유지 + 새 이벤트 지원 ──

    // 훅 실행 (모든 이벤트 공통)
    public async Task<HookRunResult> 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<string> 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 통합

// AgentLoopService.cs 에서 새 이벤트 발화 위치:
public class AgentLoopService
{
    // 1. 사용자 메시지 수신 시 → UserPromptSubmit 훅
    private async Task<string?> 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<PermissionDecision> 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: 설정 스키마 변경

// AppSettings.cs → LlmSettings에 추가
public class LlmSettings
{
    [JsonPropertyName("hooks")]
    public HooksConfig Hooks { get; set; } = new();
}

public class HooksConfig
{
    [JsonPropertyName("user_prompt_submit")]
    public List<HookDefinition> UserPromptSubmit { get; set; } = new();

    [JsonPropertyName("pre_compact")]
    public List<HookDefinition> PreCompact { get; set; } = new();

    [JsonPropertyName("post_compact")]
    public List<HookDefinition> PostCompact { get; set; } = new();

    [JsonPropertyName("file_changed")]
    public List<HookDefinition> FileChanged { get; set; } = new();

    [JsonPropertyName("session_start")]
    public List<HookDefinition> SessionStart { get; set; } = new();

    [JsonPropertyName("session_end")]
    public List<HookDefinition> SessionEnd { get; set; } = new();

    [JsonPropertyName("permission_request")]
    public List<HookDefinition> PermissionRequest { get; set; } = new();

    // ... 기존 PreToolUse, PostToolUse, AgentStop 유지 ...
}

17-C-7: 훅 편집기 UI (SettingsWindow에 잔류)

// 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 확장

// 기존 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<string> Arguments { get; init; } = Array.Empty<string>();

    // ── 신규 (Phase 17-D) ──
    [JsonPropertyName("context")]
    public SkillContext Context { get; init; } = SkillContext.Default;
    // "fork" → 격리된 서브에이전트 컨텍스트

    [JsonPropertyName("paths")]
    public IReadOnlyList<string> Paths { get; init; } = Array.Empty<string>();
    // 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<HookDefinition> PreSkillExecute { get; init; } = new();

    [JsonPropertyName("post_skill_execute")]
    public List<HookDefinition> PostSkillExecute { get; init; } = new();

    [JsonPropertyName("post_tool_use")]
    public List<HookDefinition> PostToolUse { get; init; } = new();
}

17-D-2: PathBasedSkillActivator

// 파일 경로에 따라 적용할 스킬을 자동 선택
public class PathBasedSkillActivator
{
    private readonly SkillLoaderService _skillLoader;

    // 현재 작업 중인 파일 경로 기반으로 활성 스킬 목록 반환
    public IReadOnlyList<LoadedSkill> GetActiveSkillsForFile(string filePath)
    {
        return _skillLoader.GetAllSkills()
            .Where(s => s.Frontmatter.Paths.Any(pattern =>
                GlobMatcher.IsMatch(filePath, pattern)))
            .ToList();
    }

    // 활성 스킬의 컨텍스트를 시스템 프롬프트 주입용 텍스트로 빌드
    public string BuildSkillContextInjection(IReadOnlyList<LoadedSkill> 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

// fork 컨텍스트 스킬: 격리된 서브에이전트 루프에서 실행
public class ForkContextSkillRunner
{
    private readonly AgentLoopService _parentLoop;

    public async Task<SkillResult> 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 확장

public class SkillLoaderService
{
    // 기존 메서드 유지 +

    // 신규: UserInvocable=false 스킬을 슬래시 메뉴에서 필터
    public IReadOnlyList<LoadedSkill> GetUserInvocableSkills()
        => GetAllSkills().Where(s => s.Frontmatter.UserInvocable).ToList();

    // 신규: paths 패턴이 있는 스킬 목록 (PathBasedSkillActivator용)
    public IReadOnlyList<LoadedSkill> GetPathScopedSkills()
        => GetAllSkills().Where(s => s.Frontmatter.Paths.Count > 0).ToList();
}

17-D-5: AgentLoopService 통합

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<string> 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

// AX.md 및 rules/*.md에서 @파일경로 지시어를 재귀 해석
public class AxMdIncludeResolver
{
    private const int MaxDepth = 5;
    private const long MaxFileSize = 40_000; // 40,000자 경고 임계값

    // @파일경로 지시어 모두 해석하여 최종 텍스트 반환
    public async Task<string> ResolveAsync(
        string content,
        string basePath,
        int depth = 0,
        HashSet<string>? visited = null)
    {
        if (depth >= MaxDepth) return content; // 최대 깊이 초과
        visited ??= new HashSet<string>(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<string> visited)
    {
        if (visited.Contains(filePath))
            throw new InvalidOperationException($"순환 참조 감지: {filePath}");
        visited.Add(filePath);
    }
}

17-E-2: PathScopedRuleInjector

// .ax/rules/*.md의 paths: 프론트매터를 읽어 현재 파일에 맞는 규칙만 주입
public class PathScopedRuleInjector
{
    private readonly string _rulesDir; // 프로젝트/.ax/rules/
    private IReadOnlyList<RuleFile>? _cachedRules;

    // 캐시 로드 (FileSystemWatcher로 변경 시 무효화)
    public async Task LoadRulesAsync() { ... }

    // 현재 파일 경로에 맞는 규칙 파일 필터링
    public IReadOnlyList<RuleFile> GetActiveRulesForFile(string currentFilePath)
    {
        return (_cachedRules ?? Array.Empty<RuleFile>())
            .Where(r => r.Frontmatter.Paths.Count == 0 || // paths 없으면 항상 적용
                        r.Frontmatter.Paths.Any(p => GlobMatcher.IsMatch(currentFilePath, p)))
            .ToList();
    }

    // 시스템 프롬프트 주입용 텍스트 빌드
    public string BuildInjection(IReadOnlyList<RuleFile> 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<string> Paths { get; init; } = Array.Empty<string>();
    // 빈 배열 → 항상 적용
    // ["src/**/*.py"] → Python 파일 작업 시만 적용
}

17-E-3: ContextCompactionService + /compact 명령

// 컨텍스트 압축 서비스
public class ContextCompactionService
{
    private readonly AgentLoopService _agentLoop;
    private readonly HookRunnerService _hookRunner;
    private readonly TaskStateService _taskState;

    // /compact 슬래시 명령 처리
    public async Task<CompactionResult> 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 -- <files> 또는 내부 백업에서 복원
    }
}

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 슬래시 명령 등록

// SlashCommandRegistry 에 등록
public class CompactSlashCommand : ISlashCommand
{
    public string Name => "compact";
    public string Description => "대화 컨텍스트를 수동으로 압축합니다";
    public bool UserInvocable => true;

    private readonly ContextCompactionService _compaction;

    public async Task<SlashCommandResult> 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

// WorkflowAnalyzerViewModel 또는 AgentSessionHeaderViewModel 에 추가
public bool HasOversizedRules { get; set; }  // 40,000자 초과 시 true
public string OversizedRuleWarning { get; set; }  // "python.md가 40,000자를 초과합니다. 파일 분리를 권장합니다."

// AgentSessionHeaderBar.xaml에 경고 배지 표시
// <Border Visibility="{Binding HasOversizedRules, ...}" Background="#EF4444" ...>
//   <TextBlock Text="규칙 크기 경고" FontSize="11"/>
// </Border>

Phase 17-F — 권한 시스템 고도화 (v1.8.0)

목표: acceptEdits 모드 추가 + 패턴 기반 허용/차단 규칙 + MCP 도구 권한

17-F-1: PermissionMode 열거형 확장

// 기존 AgentDecisionLevel → PermissionMode 로 개념 통합
public enum PermissionMode
{
    Default,           // 기존: 잠재적으로 위험한 작업에 확인 요청
    AcceptEdits,       // 신규: 파일 편집 자동승인, bash/process 확인 유지
    Plan,              // 기존: 읽기 전용, 쓰기 차단
    BypassPermissions  // 기존: 모든 확인 건너뜀 (자동화 전용)
}

17-F-2: PermissionRule + Chain (Chain of Responsibility 패턴)

// 권한 규칙 하나
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<PermissionRule> _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<PermissionRule> _allowRules;
    public override PermissionDecision? Handle(string toolName, string input) { ... }
}

// 구현: AcceptEdits 모드 핸들러
public class AcceptEditsHandler : PermissionHandler
{
    private readonly PermissionMode _mode;
    private static readonly HashSet<string> _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 도구 권한

// 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: 설정 스키마 변경

// 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<PermissionRule> AllowRules { get; set; } = new();
    // 예: [{"tool":"process","pattern":"git *","behavior":"allow"}]

    [JsonPropertyName("deny")]
    public List<PermissionRule> 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__외부서버 ] [패턴: (전체) ] [차단 ▾] [삭제]
[+ 규칙 추가]
// PermissionsTabViewModel (SettingsWindow 내 탭)
public class PermissionsTabViewModel : ViewModelBase
{
    public PermissionMode SelectedMode { get; set; }
    public ObservableCollection<PermissionRuleViewModel> AllowRules { get; }
    public ObservableCollection<PermissionRuleViewModel> 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 전송 추가

// McpClientService.cs 에 전송 타입 추가
public enum McpTransport { Stdio, Http, Sse }

public class McpServerConfig
{
    // 기존
    [JsonPropertyName("command")]
    public string? Command { get; init; }

    [JsonPropertyName("args")]
    public IReadOnlyList<string> Args { get; init; } = Array.Empty<string>();

    // 신규
    [JsonPropertyName("type")]
    public McpTransport Transport { get; init; } = McpTransport.Stdio;

    [JsonPropertyName("url")]
    public string? Url { get; init; }  // HTTP/SSE 전용

    [JsonPropertyName("headers")]
    public Dictionary<string, string> Headers { get; init; } = new();
    // $VAR 환경변수 치환 지원
}

// McpClientService 에 HTTP/SSE 클라이언트 추가
public class McpClientService
{
    // 기존: stdio 클라이언트

    // 신규: HTTP 전송 클라이언트
    private async Task<McpResponse> SendHttpAsync(McpServerConfig config, McpRequest req, CancellationToken ct)
    {
        // headers의 $VAR → 환경변수 치환
        // POST {config.Url} + Authorization 헤더
    }

    // 신규: SSE 전송 클라이언트
    private async Task<McpResponse> SubscribeSseAsync(McpServerConfig config, McpRequest req, CancellationToken ct)
    {
        // Server-Sent Events 수신
    }
}

Phase 17-G — 멀티파일 Diff + 자동 컨텍스트 수집 (v1.8.0)

17-G-1: 멀티파일 통합 Diff 뷰

// 기존 DiffPanel 확장
public class MultiFileDiffViewModel : ViewModelBase
{
    public ObservableCollection<FileDiffViewModel> 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<DiffHunkViewModel> 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 핵심:

<!-- AgentDiffPanel.xaml -->
<Grid>
  <!-- 상단: 변경 요약 + 전체 수락/거부 -->
  <Border Grid.Row="0" Height="40">
    <DockPanel>
      <TextBlock Text="{Binding TotalChangedFiles, StringFormat='{}{0}개 파일 변경'}"
                 Foreground="{DynamicResource PrimaryText}"/>
      <StackPanel Orientation="Horizontal" DockPanel.Dock="Right">
        <Border Style="{StaticResource AcceptButtonStyle}"
                MouseLeftButtonUp="...">
          <TextBlock Text="전체 수락" Foreground="#22C55E"/>
        </Border>
        <Border Style="{StaticResource RejectButtonStyle}"
                MouseLeftButtonUp="...">
          <TextBlock Text="전체 거부" Foreground="#EF4444"/>
        </Border>
      </StackPanel>
    </DockPanel>
  </Border>

  <!-- 파일별 diff -->
  <ScrollViewer Grid.Row="1">
    <ItemsControl ItemsSource="{Binding FileDiffs}">
      <ItemsControl.ItemTemplate>
        <DataTemplate DataType="{x:Type vm:FileDiffViewModel}">
          <!-- 파일 헤더: 경로 + 수락/거부 -->
          <!-- Hunk 목록 -->
        </DataTemplate>
      </ItemsControl.ItemTemplate>
    </ItemsControl>
  </ScrollViewer>
</Grid>

17-G-2: 자동 컨텍스트 수집

// 사용자 메시지에서 파일명 감지 → 자동 읽기
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<IReadOnlyList<FileContext>> CollectAsync(
        string userMessage, string projectRoot, CancellationToken ct)
    {
        var matches = _filePatterns.Matches(userMessage);
        var results = new List<FileContext>();

        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: 코디네이터 에이전트

// 코디네이터 모드: 계획/라우팅 전담
public interface IAgentCoordinator
{
    Task<CoordinatorPlan> CreatePlanAsync(string userRequest, CancellationToken ct);
    Task<string> ExecutePlanAsync(CoordinatorPlan plan, IProgress<PlanProgress> progress, CancellationToken ct);
}

public class CoordinatorAgent : IAgentCoordinator
{
    private readonly AgentLoopService _loop;
    private readonly DelegateAgentTool _delegateTool;

    public async Task<CoordinatorPlan> CreatePlanAsync(string userRequest, CancellationToken ct)
    {
        // 전용 시스템 프롬프트로 계획 생성
        // 출력: JSON 형태의 SubtaskList
        // 각 서브태스크: agentType, description, dependencies[]
    }

    public async Task<string> ExecutePlanAsync(CoordinatorPlan plan, IProgress<PlanProgress> progress, CancellationToken ct)
    {
        // 의존성 없는 서브태스크 병렬 실행
        // 완료된 서브태스크 결과를 다음 서브태스크 컨텍스트에 주입
        // 최종 결과 병합
    }
}

public record CoordinatorPlan
{
    public string OriginalRequest { get; init; }
    public IReadOnlyList<SubTask> 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<string> 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

// git worktree 기반 격리 실행 환경
public class WorktreeManager : IDisposable
{
    private readonly List<Worktree> _active = new();

    // 새 worktree 생성 (임시 브랜치)
    public async Task<Worktree> CreateAsync(
        string repoRoot,
        string branchName,
        CancellationToken ct)
    {
        var worktreePath = Path.Combine(Path.GetTempPath(), "ax-worktree-" + Guid.NewGuid().ToString("N")[..8]);
        // git worktree add <path> -b <branch>
        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

// 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<ToolResult> ExecuteAsync(JsonObject args, CancellationToken ct)
    {
        var agentType = args["agent_type"]!.GetValue<string>();
        var task = args["task"]!.GetValue<string>();
        var context = args["context"]?.GetValue<string>() ?? string.Empty;
        var isolation = args["isolation"]?.GetValue<string>() ?? "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

// 비동기 서브에이전트 실행 + 완료 알림
public class BackgroundAgentService
{
    private readonly ConcurrentDictionary<string, BackgroundAgentTask> _running = new();

    // 이벤트: 완료 시 발화 (트레이 알림 연결)
    public event EventHandler<AgentCompletedEventArgs>? AgentCompleted;

    // 비동기 실행 시작 (즉시 반환, 완료 시 이벤트)
    public async Task<string> 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<BackgroundAgentTask> 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

// 에이전트 타입별 영속 메모리
// 저장: %APPDATA%\AxCopilot\agent-memory\<agentType>\MEMORY.md
public class AgentTypeMemoryRepository
{
    private readonly string _baseDir; // %APPDATA%\AxCopilot\agent-memory\

    public async Task<string?> 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

// AgentSessionHeaderViewModel 에 추가
public bool IsCoordinatorMode { get; set; }
public ICommand ToggleCoordinatorModeCommand { get; }

// AgentSessionHeaderBar.xaml 에 추가:
// [코디네이터 모드 토글 버튼] — 활성 시 AccentColor로 강조
// 코디네이터 모드 ON 시: 서브태스크 진행 패널 표시

// CoordinatorPlanViewModel: SubTask 목록 + 진행 상태 표시
public class CoordinatorPlanViewModel : ViewModelBase
{
    public ObservableCollection<SubTaskViewModel> 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의 이벤트 로그를 활용한 세션 재생 및 분기 재실행

// 세션 리플레이 서비스
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<string> 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<TimelineEventViewModel> 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: 플러그인 갤러리

// 플러그인 매니페스트
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<InstallResult> InstallFromZipAsync(string zipPath, CancellationToken ct)
    {
        // 1. zip 압축 해제 → 임시 디렉토리
        // 2. manifest.json 파싱·검증
        // 3. %APPDATA%\AxCopilot\plugins\<id>\ 에 복사
        // 4. 플러그인 타입에 따라 등록:
        //    - Skill → SkillLoaderService.Reload()
        //    - Tool → ToolRegistry.Register()
    }

    // 로컬 레지스트리 갱신 (NAS/Git 기반)
    public async Task RefreshRegistryAsync(string registryPath, CancellationToken ct) { ... }

    // 설치된 플러그인 목록
    public IReadOnlyList<InstalledPlugin> 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 프로젝트 <cwd>\AX.md + <cwd>\.axcopilot\rules\*.md 팀 공유 규칙
L4 로컬 <cwd>\AX.local.md 개인 프로젝트 오버라이드 (gitignore) 최상위

핵심 클래스

// HierarchicalMemoryLoader.cs
public class HierarchicalMemoryLoader
{
    /// <summary>CWD에서 루트까지 탐색하며 계층별 메모리 파일 수집.</summary>
    public async Task<MemoryContext> LoadAsync(string workFolder, CancellationToken ct)
    {
        var layers = new List<MemoryLayer>();
        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);
    }

    /// <summary>@include 지시문 처리 (최대 5단계 중첩, 순환 참조 감지).</summary>
    private async Task<string> ResolveIncludesAsync(string content, string basePath,
        HashSet<string> visited, int depth, CancellationToken ct) { ... }
}

// PathScopedRuleLoader.cs — .axcopilot/rules/*.md 파일 로드
public class PathScopedRuleLoader
{
    /// <summary>현재 편집 파일 경로와 rules/*.md의 paths 패턴을 매칭하여 주입할 규칙 반환.</summary>
    public IReadOnlyList<string> GetApplicableRules(string editingFilePath,
        IReadOnlyList<RuleFile> ruleFiles) { ... }
}

// MemoryContext.cs
public record MemoryLayer(string Source, string Content, MemoryScope Scope);
public enum MemoryScope { System, User, Project, Local }
public record MemoryContext(IReadOnlyList<MemoryLayer> Layers)
{
    /// <summary>모든 레이어를 우선순위 순서로 조합한 최종 컨텍스트 문자열.</summary>
    public string Compose() => string.Join("\n\n", Layers.Select(l => l.Content));
}

AgentLoopService 통합

// AgentLoopService.cs — 컨텍스트 조립 부분 교체
private async Task<string> 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);
}

슬래시 명령 추가

// /memory 슬래시 명령 → ChatWindow에서 메모리 파일 인라인 에디터 열기
// 현재 로드된 모든 메모리 레이어 표시 + 편집 버튼
public class MemorySlashCommand : ISlashCommand
{
    public string Name => "memory";
    public string Description => "로드된 메모리 파일 목록 및 편집";
    public Task ExecuteAsync(string args, IChatContext ctx);
}

설정 항목

// 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 → 명령 패턴 매칭 + &&/||/;/| 복합 명령 서브 커맨드별 검증.

핵심 클래스

// CommandPatternMatcher.cs
public class CommandPatternMatcher
{
    /// <summary>패턴(glob) 기반 명령 허용 여부 검사.
    /// 예: "git *" → git commit, git push 모두 허용
    ///     "npm run *" → npm run build, npm run test 허용
    ///     "rm -rf *" → 금지 리스트에 명시 가능</summary>
    public PermissionDecision Match(string command, IReadOnlyList<PermissionRuleEntry> rules) { ... }
}

// CompoundCommandParser.cs
public class CompoundCommandParser
{
    private static readonly string[] Operators = ["&&", "||", ";", "|"];

    /// <summary>복합 명령을 분해하여 서브 커맨드 목록 반환.
    /// 예: "git add . && git commit -m 'msg'" → ["git add .", "git commit -m 'msg'"]</summary>
    public IReadOnlyList<string> Parse(string command) { ... }

    /// <summary>복합 명령의 모든 서브 커맨드에 대해 권한 검사.
    /// 하나라도 Deny이면 전체 Deny.</summary>
    public PermissionDecision CheckAll(string compoundCommand,
        CommandPatternMatcher matcher,
        IReadOnlyList<PermissionRuleEntry> 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 확장

// 기존 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; } = "";        // 사용자 표시 이유
}

도구 가시성 필터링

// AgentLoopService — LLM 호출 전 비활성 도구를 tools 목록에서 제거
// 현재: 도구 이름은 넘기되 실행 시 차단
// 개선: 아예 스키마 자체를 LLM에 전달하지 않음 → 환각 방지
private IReadOnlyList<IAgentTool> FilterVisibleTools(IReadOnlyList<IAgentTool> allTools)
{
    return allTools.Where(t =>
        !_settings.DisabledTools.Contains(t.Name) &&
        _permissionSystem.IsToolVisible(t.Name)).ToList();
}

Phase 19-C — 훅 이벤트 완성 (v2.1)

목표: 현재 부분 구현된 훅 시스템 → Claude Code의 14+ 이벤트 + 4가지 훅 타입 완성.

추가 훅 이벤트

// HookTypes.cs 확장
public enum AgentHookEvent
{
    // 기존
    PreToolUse, PostToolUse, Stop, SessionStart, SessionEnd,
    UserPromptSubmit, PreCompact, PostCompact,

    // 신규 추가
    PostToolUseFailure,     // 도구 실행 실패 시
    SubAgentStart,          // 서브에이전트 시작
    SubAgentStop,           // 서브에이전트 완료
    PermissionRequest,      // 승인 다이얼로그 직전
    PermissionDenied,       // 거부 후
    Notification,           // UI 알림 발생 시
    CwdChanged,             // 작업 폴더 변경 시
    FileChanged,            // 감시 파일 변경 시
    ConfigChange,           // 설정 파일 변경 시
}

훅 타입 완성 (4종)

// 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<string, string> 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 입력 수정 기능

// 훅이 도구 입력을 수정할 수 있도록 허용
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`` 인라인 실행, 유저급 스킬, 네임스페이스, 스킬별 모델 오버라이드 완성.

스킬 인라인 명령 실행

// SkillService.cs 확장
// 스킬 파일 내 !`command` 블록을 호출 시점에 실행하고 출력으로 치환

public class SkillInlineCommandProcessor
{
    private static readonly Regex InlineCmd = new(@"!\`([^`]+)\`", RegexOptions.Compiled);

    /// <summary>스킬 본문의 !`cmd` 블록을 실행하고 결과로 치환합니다.
    /// 예: !`git log --oneline -10` → 최근 10개 커밋 목록으로 치환</summary>
    public async Task<string> 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;
    }
}

스킬 프론트매터 확장

---
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-name>\SKILL.md   # 유저 전역
<cwd>\.axcopilot\skills\<skill-name>\SKILL.md            # 프로젝트
<cwd>\.axcopilot\skills\<ns>\<skill-name>\SKILL.md       # 네임스페이스 (/<ns>:<skill>)

스킬 네임스페이스

// /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" → "<workFolder>/.axcopilot/skills/database/migrate/SKILL.md"
}

Phase 19-E — 세션 재개 + 포크 + 태깅 (v2.1)

목표: JSONL 리플레이(관찰용) → 실제 세션 재개 + 분기 지점 포크.

세션 관리 API

// AgentSessionManager.cs
public class AgentSessionManager
{
    /// <summary>이전 세션을 재개합니다.
    /// 메모리 파일은 재개 시점 기준으로 재발견합니다.</summary>
    public Task<AgentSession> ResumeAsync(string sessionId, CancellationToken ct) { ... }

    /// <summary>현재 세션의 특정 메시지 인덱스 이후를 분기합니다.
    /// 원본 세션은 보존됩니다.</summary>
    public Task<AgentSession> ForkAsync(string sessionId, int messageIndex, CancellationToken ct) { ... }

    /// <summary>세션에 태그를 붙입니다 (즐겨찾기, 분류 등).</summary>
    public Task TagAsync(string sessionId, string tag, CancellationToken ct) { ... }

    /// <summary>세션 이름을 변경합니다.</summary>
    public Task RenameAsync(string sessionId, string newName, CancellationToken ct) { ... }

    /// <summary>조건에 맞는 세션 목록 반환.</summary>
    public Task<IReadOnlyList<AgentSessionMeta>> ListAsync(
        string? tag = null, string? tabType = null, CancellationToken ct = default) { ... }
}

public record AgentSession(string Id, string Name, string TabType, List<ChatMessage> Messages);
public record AgentSessionMeta(string Id, string Name, string TabType,
    DateTime CreatedAt, DateTime LastAt, string? Tag, int MessageCount);

에이전트 타입별 영속 메모리 경로

%USERPROFILE%\.axcopilot\agent-memory\<type>\MEMORY.md      # 유저 범위
<cwd>\.axcopilot\agent-memory\<type>\MEMORY.md              # 프로젝트 범위
<cwd>\.axcopilot\agent-memory-local\<type>\MEMORY.md        # 로컬 범위
// AgentTypeMemoryRepository.cs 확장
public class AgentTypeMemoryRepository
{
    public enum MemoryScope { User, Project, Local }

    public async Task<string?> 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자 절단 → 도구별 출력 예산 + 대형 출력 임시파일 스필오버. 컨텍스트 조립 메모이제이션.

출력 예산 관리

// ToolOutputBudget.cs
public static class ToolOutputBudget
{
    // 도구별 기본 최대 출력 크기
    private static readonly Dictionary<string, int> 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");

    /// <summary>출력이 예산 초과 시 임시파일에 저장하고 경로+미리보기 반환.</summary>
    public async Task<string> 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...";
    }
}

컨텍스트 조립 메모이제이션

// ContextAssemblyCache.cs
public class ContextAssemblyCache
{
    private string? _cachedGitState;
    private DateTime _gitStateCachedAt;
    private MemoryContext? _cachedMemory;
    private string? _cachedWorkFolder;

    /// <summary>세션 내 git 상태는 5분 캐시.</summary>
    public async Task<string> GetGitStateAsync(string workFolder, CancellationToken ct) { ... }

    /// <summary>메모리 파일은 세션 시작 시 한 번만 로드 (변경 감지 시 갱신).</summary>
    public async Task<MemoryContext> GetMemoryContextAsync(string workFolder, CancellationToken ct) { ... }

    /// <summary>파일 변경 감지 시 캐시 무효화.</summary>
    public void Invalidate() { _cachedMemory = null; _cachedGitState = null; }
}

Phase 19-G — /init 슬래시 명령 (v2.1)

목표: 프로젝트 분석 → AX.md 자동 생성 + 스킬/훅 초기 설정 제안.

핵심 클래스

// ProjectInitAnalyzer.cs
public class ProjectInitAnalyzer
{
    /// <summary>
    /// 프로젝트 구조를 분석하여 AX.md 초안을 생성합니다.
    /// - 언어/프레임워크 탐지 (*.csproj, package.json, Cargo.toml 등)
    /// - 테스트 프레임워크 탐지
    /// - CI/CD 설정 탐지 (.github/workflows 등)
    /// - 디렉터리 구조 요약
    /// - 기존 README.md 요약 (LLM 호출)
    /// </summary>
    public async Task<ProjectAnalysis> AnalyzeAsync(string workFolder, CancellationToken ct) { ... }

    /// <summary>분석 결과를 바탕으로 AX.md 초안 생성.</summary>
    public async Task<string> GenerateAxMdAsync(ProjectAnalysis analysis,
        LlmService llm, CancellationToken ct) { ... }
}

public record ProjectAnalysis(
    string Language,
    string Framework,
    string? TestFramework,
    string? CiSystem,
    IReadOnlyList<string> 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
// 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 인터페이스 + 레지스트리

// 슬래시 명령 인터페이스 (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<string?> PromptUserAsync(string question);  // 사용자 입력 대기
    Task ShowToastAsync(string message);
    void ClearMessages();
    void UpdateSessionHeader();             // 모델/모드 변경 시 헤더 갱신
}

// 슬래시 명령 레지스트리 (Factory + Registry)
public class SlashCommandRegistry
{
    private readonly Dictionary<string, ISlashCommand> _commands = new(StringComparer.OrdinalIgnoreCase);
    private readonly Dictionary<string, string> _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<ISlashCommand> 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 — 컨텍스트 압축

public class CompactCommand : ISlashCommand
{
    public string Name => "compact";
    public string[] Aliases => Array.Empty<string>();
    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<ChatMessage> 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 — 대화 초기화

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 — 메모리 파일 편집

public class MemoryCommand : ISlashCommand
{
    public string Name => "memory";
    public string[] Aliases => Array.Empty<string>();
    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 — 세션 모델 전환

public class ModelCommand : ISlashCommand
{
    public string Name => "model";
    public string[] Aliases => Array.Empty<string>();
    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 — 플랜 모드 토글

public class PlanCommand : ISlashCommand
{
    public string Name => "plan";
    public string[] Aliases => Array.Empty<string>();
    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 커밋 메시지

public class CommitCommand : ISlashCommand
{
    public string Name => "commit";
    public string[] Aliases => Array.Empty<string>();
    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 서버 관리

public class McpCommand : ISlashCommand
{
    public string Name => "mcp";
    public string[] Aliases => Array.Empty<string>();
    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

// /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<string>();
    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<string>();
    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<string>();
    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<string>();
    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: 입력 라우팅 통합

// AgentLoopService 또는 AgentWindowViewModel에서 입력 처리
public class InputRouter
{
    private readonly SlashCommandRegistry _slashCommands;
    private readonly SkillLoaderService _skillLoader;

    /// <summary>사용자 입력을 슬래시 명령 → 스킬 → 일반 메시지 순으로 라우팅.</summary>
    public async Task<InputRouteResult> 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`)

// SkillInlineCommandProcessor.cs
public class SkillInlineCommandProcessor
{
    // !`command` 패턴을 찾아 실행하고 출력으로 교체
    private static readonly Regex InlineCmdPattern = new(@"!\`([^`]+)\`", RegexOptions.Compiled);

    /// <summary>
    /// 스킬 본문에서 !`command` 패턴을 찾아 셸 실행 결과로 교체합니다.
    /// 호출 시점: 스킬이 로드된 후, LLM에 프롬프트로 전달하기 직전.
    /// </summary>
    public async Task<string> 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<string> 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: 스킬 네임스페이싱 (서브디렉토리 콜론 구분)

// SkillLoaderService 확장
public class SkillLoaderService
{
    // 기존 메서드 + 네임스페이싱 로직

    /// <summary>
    /// 스킬 디렉토리 구조를 콜론 구분 네임스페이스로 변환합니다.
    /// .claude/skills/database/migrate/SKILL.md → "database:migrate"
    /// .claude/skills/deploy/SKILL.md → "deploy"
    /// </summary>
    private string ResolveSkillName(string skillDir, string baseSkillsDir)
    {
        var relative = Path.GetRelativePath(baseSkillsDir, skillDir);
        // Windows 경로 구분자 → 콜론
        return relative.Replace(Path.DirectorySeparatorChar, ':')
                       .Replace(Path.AltDirectorySeparatorChar, ':');
    }

    /// <summary>콜론 구분 이름으로 스킬 검색 (부분 일치 지원).</summary>
    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<LoadedSkill> 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 구문)

// SkillArgumentSubstitution.cs
public static class SkillArgumentSubstitution
{
    /// <summary>
    /// 스킬 본문에서 $ARGUMENTS와 명명된 인수를 치환합니다.
    /// - $ARGUMENTS → 전체 인수 문자열
    /// - $name → 프론트매터 arguments: [name, directory] 기반 위치 매핑
    /// - {0}, {1} → 인덱스 기반 인수
    /// </summary>
    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<string> SplitArguments(string input)
    {
        // 따옴표로 감싼 인수 지원: "hello world" foo bar
        var parts = new List<string>();
        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 통합

// 스킬 실행 파이프라인: 인수 치환 → 인라인 셸 → LLM 전달
public class SkillExecutionPipeline
{
    private readonly SkillInlineCommandProcessor _inlineCmd;
    private readonly PathBasedSkillActivator _pathActivator;
    private readonly ForkContextSkillRunner _forkRunner;
    private readonly HookRunnerService _hookRunner;

    public async Task<SkillResult> 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

// 복합 Bash 명령을 서브커맨드로 분리하여 각각 권한 검사
public class CompoundCommandParser
{
    // &&, ||, ;, | 로 분리 (따옴표, 서브셸 내부는 분리하지 않음)
    public static IReadOnlyList<string> Parse(string command)
    {
        var result = new List<string>();
        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<PermissionRule> _allowRules;
    private readonly IReadOnlyList<PermissionRule> _denyRules;

    /// <summary>
    /// 복합 명령의 각 서브커맨드를 독립적으로 검사합니다.
    /// 하나의 서브커맨드라도 deny되면 전체 명령이 차단됩니다.
    /// </summary>
    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

// 훅 실행 시 셸 환경변수를 자동 주입
public static class HookEnvironmentBuilder
{
    /// <summary>HookContext에서 셸 환경변수 딕셔너리를 생성합니다.</summary>
    public static Dictionary<string, string> Build(HookContext context)
    {
        var env = new Dictionary<string, string>(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<HookResult> 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

// AgentInputArea에서 / 입력 시 자동완성 팝업에 hint 표시
public class SlashAutoCompleteProvider
{
    private readonly SlashCommandRegistry _commands;
    private readonly SkillLoaderService _skills;

    public IReadOnlyList<AutoCompleteItem> GetSuggestions(string input)
    {
        if (!input.StartsWith("/")) return Array.Empty<AutoCompleteItem>();

        var prefix = input.TrimStart('/');
        var results = new List<AutoCompleteItem>();

        // 내장 명령
        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 자동 트리거

// 컨텍스트 크기 모니터링 → 자동 컴팩션
public class AutoCompactMonitor
{
    private readonly TokenEstimatorService _tokenEstimator;
    private readonly int _thresholdPercent; // 기본 80%

    /// <summary>매 에이전트 반복 후 호출 → 컨텍스트가 임계치 초과 시 자동 컴팩션.</summary>
    public async Task<bool> 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() 자가 비활성화

// IAgentTool 인터페이스 확장 (ISP 준수: 옵션 인터페이스)
public interface IConditionalTool
{
    /// <summary>현재 환경에서 이 도구가 사용 가능한지 확인합니다.</summary>
    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<IAgentTool> 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.mddatabase: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.csBuildPathBasedSkillSection() 추가
  • 최근 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 자동 감지
  • GitToolIConditionalTool 구현 (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
  • AgentTypeMemoryRepositoryIAgentMemoryRepository 구현
  • 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: ActiveTabcontext.ActiveTab 참조
  • ExecuteToolsInParallelAsync: 감사 로그 ActiveTabcontext.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 catchcatch (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 주입 — 서비스 로케이터 제거

  • AgentContextSettings (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 AppCurrentApp 정적 프로퍼티 일원화

  • 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_ClickPopup+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개 정적 필드 추가:

  • SegoeMdl2new FontFamily("Segoe MDL2 Assets") (기존)
  • Consolasnew FontFamily("Consolas") (기존)
  • CascadiaCodenew FontFamily("Cascadia Code, Consolas, monospace") (신규)
  • ConsolasCodenew FontFamily("Consolas, Cascadia Code, Segoe UI") (신규)
  • ConsolasCourierNewnew 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

최종 업데이트: 2026-04-03 (Phase 22~39 구현 완료 — CC 동등성 37/37 + 코드 품질 리팩터링 7차)