# AX Copilot 차기 개발 계획 (v1.7.1 ~ v2.0) — 구현 수준 상세 명세

> **개정 기준**: 2026년 4월 — Claude Code 내부 아키텍처 문서 심층 분석 + 구현 수준 상세화
> 각 Phase는 C# 인터페이스·메서드 시그니처·WPF 바인딩·설정 스키마·통합 포인트까지 명시
> 현재 v1.7.1 — 52개 에이전트 도구, 29개 내장 스킬, 20개 코워크 프리셋

---

## Claude Code 아키텍처 갭 진단 (v1.7.1 기준)

| 갭 영역 | CC 보유 기능 | AX 현재 | 우선순위 Phase |
|---------|------------|---------|--------------|
| 훅 이벤트 | 17종 이벤트, 4타입(command/http/prompt/agent) | ~8종, 2타입 | 17-C |
| 훅 출력 | additionalContext·permissionDecision·updatedInput 주입 | 없음 | 17-C |
| 스킬 격리 | context:fork 서브에이전트 컨텍스트 | 없음 | 17-D |
| 스킬 경로 활성화 | paths: glob 패턴 자동 주입 | 없음 | 17-D |
| @include 메모리 | @파일경로 5단계 포함 | 없음 | 17-E |
| 경로 기반 규칙 | rules/*.md paths: 프론트매터 | 없음 | 17-E |
| acceptEdits 모드 | 파일 편집 자동승인, bash 확인 유지 | 없음 | 17-F |
| 패턴 권한 규칙 | Bash(git *) 패턴 허용/차단 | 없음 | 17-F |
| 코디네이터 에이전트 | 계획/라우팅 전담, 구현 위임 | 없음 | 18-A |
| Worktree 격리 | 서브에이전트 독립 git 워킹카피 | 없음 | 18-A |
| 백그라운드 에이전트 | 비동기 실행 + 완료 알림 | 없음 | 18-A |
| Chat UI | 3패널, 인라인 설정, 세션 헤더 바 | 단일 패널, 별도 설정창 | 17-UI |

---

## Phase 17-UI — AX Agent 채팅 화면 전면 개편 (v1.8.0)

> **목표**: ChatGPT Codex 스타일 3패널 레이아웃으로 채팅 화면 완전 재설계.
> 에이전트 설정을 SettingsWindow에서 채팅 화면 인라인 패널로 이전.
> Chat/Cowork/Code 탭 및 프리셋 유지.

### 17-UI-1: 신규 파일 구조

```
src/AxCopilot/Views/AgentWindow/
  AgentWindow.xaml                  ← 기존 AgentChatWindow 대체
  AgentWindow.xaml.cs
  AgentSidebarView.xaml             ← 좌측 사이드바 (240px↔48px)
  AgentSidebarView.xaml.cs
  AgentSessionHeaderBar.xaml        ← 상단 세션 헤더 바
  AgentSessionHeaderBar.xaml.cs
  AgentInlineSettingsPanel.xaml     ← 우측 슬라이드인 설정 패널 (300px)
  AgentInlineSettingsPanel.xaml.cs
  AgentInputArea.xaml               ← 하단 입력 영역
  AgentInputArea.xaml.cs
  AgentChatView.xaml                ← 채팅 메시지 영역
  AgentChatView.xaml.cs
  AgentDiffPanel.xaml               ← 통합 diff 뷰

src/AxCopilot/ViewModels/
  AgentWindowViewModel.cs           ← 신규
  AgentSidebarViewModel.cs          ← 신규
  AgentSessionHeaderViewModel.cs    ← 신규
  AgentInlineSettingsViewModel.cs   ← 신규
```

### 17-UI-2: AgentWindow.xaml 레이아웃 구조

```xml
<!-- 전체 레이아웃: 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

```csharp
public class AgentWindowViewModel : ViewModelBase
{
    // ── 패널 상태 ──
    private bool _isSidebarExpanded = true;
    public bool IsSidebarExpanded
    {
        get => _isSidebarExpanded;
        set { SetProperty(ref _isSidebarExpanded, value); OnSidebarToggled(); }
    }

    private bool _isSettingsPanelOpen;
    public bool IsSettingsPanelOpen
    {
        get => _isSettingsPanelOpen;
        set => SetProperty(ref _isSettingsPanelOpen, value);
    }

    // ── 자식 ViewModel ──
    public AgentSidebarViewModel Sidebar { get; }
    public AgentSessionHeaderViewModel SessionHeader { get; }
    public AgentInlineSettingsViewModel InlineSettings { get; }
    public AgentInputAreaViewModel InputArea { get; }
    public ObservableCollection<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

```csharp
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):**
```xml
<!-- 탭 선택 -->
<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

```csharp
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 설정
- 암호화/보안 설정
```

```csharp
public class AgentInlineSettingsViewModel : ViewModelBase
{
    // ── 탭별 LLM 설정 (ActiveTab에 따라 바인딩 전환) ──
    public string LlmService { get; set; }
    public string ModelName { get; set; }
    public int MaxIterations { get; set; }
    public bool EnableVerification { get; set; }
    public PlanMode PlanMode { get; set; }
    public PermissionMode PermissionMode { get; set; }

    // ── 시스템 프롬프트 미리보기 ──
    public string SystemPromptPreview { get; set; }  // 처음 200자
    public ICommand EditSystemPromptCommand { get; } // 전체 편집 팝업

    // ── 활성 스킬 목록 ──
    public ObservableCollection<SkillChipViewModel> ActiveSkills { get; }
    public ICommand ManageSkillsCommand { get; }

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

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

### 17-UI-7: AgentInputArea

```csharp
public class AgentInputAreaViewModel : ViewModelBase
{
    public string InputText { get; set; }
    public bool IsMultiline { get; set; }           // Shift+Enter로 전환
    public ObservableCollection<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) 핵심:**
```xml
<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.cs`의 `OpenAgentWindow()` 호출부 교체 |
| `AgentChatViewModel.cs` | `AgentWindowViewModel`로 흡수·통합 |
| `SettingsWindow.xaml` | AX Agent 탭 내 에이전트 설정 섹션 제거, "채팅 화면에서 설정 가능" 안내 문구 추가 |
| `AgentLoopService.cs` | 이벤트 구독 포인트 변경 없음. ViewModel에서 이벤트 연결 방식만 변경 |
| `App.xaml.cs` | `_agentWindow` 필드를 `AgentWindow` 타입으로 변경 |

### 17-UI-9: 구현 순서

```
1. AgentWindowViewModel + AgentSessionHeaderViewModel (VM 먼저)
2. AgentWindow.xaml 레이아웃 골격 (3열 Grid, 빈 자식)
3. AgentSessionHeaderBar.xaml (탭, 모드, 상태)
4. AgentSidebarView.xaml (대화 목록, 프리셋)
5. 기존 AgentChatView 내용 이식 (메시지 버블 등)
6. AgentInputArea.xaml (입력창, 첨부, 슬래시 메뉴)
7. AgentInlineSettingsPanel.xaml + SettingsWindow 항목 이전
8. 사이드바/설정 패널 애니메이션
9. GridLengthAnimation 헬퍼 클래스 구현
10. App.xaml.cs OpenAgentWindow() 교체 및 전체 통합 테스트
```

---

## Phase 17-A — Reflexion 강화 (v1.8.0)

> **목표**: 성공·실패 모두 구조화된 자기평가 저장 → 동일 작업 유형 재실행 시 자동 참고

### 17-A-1: 핵심 신규 클래스

```csharp
// 반성 메모리 엔트리
public record ReflexionEntry
{
    public string TaskType { get; init; }       // "code_generation", "file_refactor" 등
    public string Summary { get; init; }        // 작업 요약
    public bool IsSuccess { get; init; }
    public float CompletionScore { get; init; } // 0.0~1.0
    public string[] Weaknesses { get; init; }  // 부족한 점 (실패 시)
    public string[] Strengths { get; init; }   // 잘된 점 (성공 시)
    public string[] Lessons { get; init; }     // 다음에 적용할 교훈
    public DateTime CreatedAt { get; init; }
    public string SessionId { get; init; }
}

// 반성 메모리 저장소 인터페이스
public interface IReflexionRepository
{
    Task SaveAsync(ReflexionEntry entry);
    Task<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 통합 포인트

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

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

```csharp
// AppSettings.cs → LlmSettings 에 추가
public class LlmSettings
{
    // ... 기존 ...
    [JsonPropertyName("enable_reflexion")]
    public bool EnableReflexion { get; set; } = true;

    [JsonPropertyName("reflexion_max_context_entries")]
    public int ReflexionMaxContextEntries { get; set; } = 5;

    [JsonPropertyName("reflexion_evaluate_on_success")]
    public bool ReflexionEvaluateOnSuccess { get; set; } = true;
}
```

---

## Phase 17-B — 구조화된 태스크 상태 + 이벤트 로그 (v1.8.0)

> **목표**: 대화 압축 시에도 유지되는 Working Memory + JSONL 이벤트 로그

### 17-B-1: TaskState (Working Memory)

```csharp
// 세션 전체에 걸쳐 유지되는 구조화된 작업 상태
public class TaskState
{
    public string SessionId { get; init; }
    public string CurrentTask { get; set; }          // 현재 수행 중인 작업 요약
    public List<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 이벤트 로그

```csharp
// 모든 에이전트 이벤트의 구조화된 기록
public record AgentEventRecord
{
    [JsonPropertyName("seq")]
    public long SeqNo { get; init; }

    [JsonPropertyName("id")]
    public string EventId { get; init; }   // GUID

    [JsonPropertyName("parent_id")]
    public string? ParentEventId { get; init; }

    [JsonPropertyName("session_id")]
    public string SessionId { get; init; }

    [JsonPropertyName("type")]
    public AgentEventType Type { get; init; }

    [JsonPropertyName("ts")]
    public DateTime Timestamp { get; init; }

    [JsonPropertyName("payload")]
    public JsonElement Payload { get; init; }  // 이벤트별 페이로드
}

public enum AgentEventType
{
    SessionStart, SessionEnd,
    UserMessage, AssistantMessage,
    ToolRequest, ToolResult,
    HookFired, HookResult,
    SkillActivated, SkillCompleted,
    CompactionTriggered, CompactionCompleted,
    SubagentSpawned, SubagentCompleted,
    Error
}

// JSONL 기반 이벤트 로그 저장소
// 저장: %APPDATA%\AxCopilot\sessions\<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: 설정 스키마 변경

```csharp
// AppSettings.cs 에 추가
public class AppSettings
{
    // ... 기존 ...
    [JsonPropertyName("enable_event_log")]
    public bool EnableEventLog { get; set; } = true;

    [JsonPropertyName("event_log_retention_days")]
    public int EventLogRetentionDays { get; set; } = 30;

    [JsonPropertyName("enable_task_state")]
    public bool EnableTaskState { get; set; } = true;
}
```

---

## Phase 17-C — 훅 시스템 고도화 (v1.8.0)

> **목표**: Claude Code의 17종 이벤트·4타입 훅 시스템을 AX Copilot에 이식

### 17-C-1: 훅 이벤트 열거형 확장

```csharp
// 기존 HookEvent 열거형에 추가
public enum HookEvent
{
    // ── 기존 (유지) ──
    PreToolUse,
    PostToolUse,
    AgentStop,

    // ── 신규 (Phase 17-C) ──
    UserPromptSubmit,       // 사용자 프롬프트 제출 전 (차단·수정 가능)
    PreCompact,             // 컨텍스트 압축 전
    PostCompact,            // 컨텍스트 압축 후
    FileChanged,            // watchPaths에 등록된 파일 변경 시
    CwdChanged,             // 작업 디렉토리 변경 시 (프로젝트 전환 감지)
    SessionStart,           // 세션 시작 (watchPaths 등록, 초기 컨텍스트 주입)
    SessionEnd,             // 세션 종료 (정리 작업)
    ConfigChange,           // .ax/rules/*.md 또는 AX.md 파일 변경
    PermissionRequest,      // 도구 실행 권한 요청 (프로그래밍 방식 승인/거부)
    PreSkillExecute,        // 스킬 실행 전
    PostSkillExecute,       // 스킬 실행 후
    SubagentStart,          // 서브에이전트 시작
    SubagentStop,           // 서브에이전트 종료
    AgentIterationStart,    // 에이전트 루프 반복 시작
    AgentIterationEnd       // 에이전트 루프 반복 종료
}
```

### 17-C-2: 훅 타입 인터페이스 (Strategy 패턴)

```csharp
// 훅 실행기 인터페이스 (Strategy)
public interface IHookExecutor
{
    HookType Type { get; }
    Task<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: 훅 속성 모델 확장

```csharp
// 기존 HookDefinition 에 속성 추가
public class HookDefinition
{
    // ── 기존 ──
    public string? Matcher { get; init; }
    public HookType Type { get; init; }
    public string? Command { get; init; }
    public string? Url { get; init; }

    // ── 신규 속성 (Phase 17-C) ──
    [JsonPropertyName("if")]
    public string? Condition { get; init; }         // 조건부 실행 (권한 모드 등 체크)

    [JsonPropertyName("once")]
    public bool Once { get; init; } = false;        // 실행 후 자기 제거

    [JsonPropertyName("async")]
    public bool IsAsync { get; init; } = false;     // 비동기 실행 (결과 기다리지 않음)

    [JsonPropertyName("async_rewake")]
    public bool AsyncRewake { get; init; } = false; // 비동기 완료 후 에이전트 재깨우기

    [JsonPropertyName("status_message")]
    public string? StatusMessage { get; init; }     // 실행 중 스피너 메시지

    [JsonPropertyName("timeout")]
    public int TimeoutSeconds { get; init; } = 30;

    // prompt 타입 전용
    [JsonPropertyName("prompt")]
    public string? Prompt { get; init; }

    [JsonPropertyName("model")]
    public string? Model { get; init; }

    // agent 타입 전용
    [JsonPropertyName("max_iterations")]
    public int MaxIterations { get; init; } = 3;
}
```

### 17-C-4: HookRunnerService 확장

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

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

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

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

```csharp
// 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에 잔류)

```csharp
// SettingsWindow.xaml — Hooks 탭 (기존 위치 유지)
// 신규: 훅 타입 선택 드롭다운 (Command/Http/Prompt/Agent)
// 신규: prompt 타입 선택 시 → Prompt 텍스트박스 + Model 드롭다운 표시
// 신규: agent 타입 선택 시 → Prompt 텍스트박스 + MaxIterations 숫자 입력 표시
// 신규: 속성 체크박스 행: [once] [async] [asyncRewake]
// 신규: statusMessage 텍스트 입력
// 신규: condition 텍스트 입력 (고급 섹션)
```

---

## Phase 17-D — 스킬 시스템 고도화 (v1.8.0)

> **목표**: fork 격리·경로 자동 활성화·스킬 범위 훅·모델 오버라이드 구현

### 17-D-1: SkillFrontmatter 확장

```csharp
// 기존 SkillFrontmatter에 필드 추가
public class SkillFrontmatter
{
    // ── 기존 ──
    [JsonPropertyName("description")]
    public string Description { get; init; } = string.Empty;

    [JsonPropertyName("when_to_use")]
    public string WhenToUse { get; init; } = string.Empty;

    [JsonPropertyName("arguments")]
    public IReadOnlyList<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

```csharp
// 파일 경로에 따라 적용할 스킬을 자동 선택
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

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

```csharp
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 통합

```csharp
public class AgentLoopService
{
    private readonly PathBasedSkillActivator _pathActivator;
    private readonly ForkContextSkillRunner _forkRunner;

    // 도구 실행 후: 작업 파일 경로 감지 → 경로 기반 스킬 자동 주입
    private async Task OnFileToolExecutedAsync(string filePath)
    {
        var skills = _pathActivator.GetActiveSkillsForFile(filePath);
        if (skills.Count > 0)
        {
            var injection = _pathActivator.BuildSkillContextInjection(skills);
            _contextBuilder.AddDynamicContext("path_skills", injection);
        }
    }

    // 스킬 실행 시: fork vs default 분기
    public async Task<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

```csharp
// 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

```csharp
// .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 명령

```csharp
// 컨텍스트 압축 서비스
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 슬래시 명령 등록

```csharp
// 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

```csharp
// 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 열거형 확장

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

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

```csharp
// 권한 규칙 하나
public record PermissionRule
{
    [JsonPropertyName("tool")]
    public string ToolName { get; init; }        // "process", "file_edit", "mcp__myserver__*"

    [JsonPropertyName("pattern")]
    public string? Pattern { get; init; }        // "git *", "rm -rf *", null(모든 입력)

    [JsonPropertyName("behavior")]
    public PermissionBehavior Behavior { get; init; }  // Allow | Deny | Ask
}

public enum PermissionBehavior { Allow, Deny, Ask }

// 책임 연쇄 핸들러 인터페이스
public abstract class PermissionHandler
{
    protected PermissionHandler? _next;

    public PermissionHandler SetNext(PermissionHandler next)
    {
        _next = next;
        return next;
    }

    public abstract PermissionDecision? Handle(string toolName, string input);
}

// 구현: Deny 규칙 핸들러
public class DenyRuleHandler : PermissionHandler
{
    private readonly IReadOnlyList<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 도구 권한

```csharp
// MCP 도구 규칙: mcp__서버명, mcp__서버명__도구명
// ToolName 패턴: "mcp__myserver", "mcp__myserver__query_db"
// PermissionRule에서 ToolName="mcp__myserver" → 해당 서버 모든 도구 차단

public class McpPermissionFilter
{
    private readonly PermissionDecisionService _permissions;

    // McpClientService.ExecuteToolAsync() 호출 전 적용
    public bool IsToolAllowed(string serverName, string toolName)
    {
        var fullName = $"mcp__{serverName}__{toolName}";
        var serverPattern = $"mcp__{serverName}";

        var byTool = _permissions.Decide(fullName, string.Empty);
        if (byTool == PermissionDecision.Deny) return false;

        var byServer = _permissions.Decide(serverPattern, string.Empty);
        if (byServer == PermissionDecision.Deny) return false;

        return true;
    }
}
```

### 17-F-4: 설정 스키마 변경

```csharp
// AppSettings.cs 에 추가
public class AppSettings
{
    [JsonPropertyName("permissions")]
    public PermissionsConfig Permissions { get; set; } = new();
}

public class PermissionsConfig
{
    [JsonPropertyName("mode")]
    public PermissionMode Mode { get; set; } = PermissionMode.Default;

    [JsonPropertyName("allow")]
    public List<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__외부서버 ] [패턴: (전체) ] [차단 ▾] [삭제]
[+ 규칙 추가]
```

```csharp
// 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 전송 추가

```csharp
// 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 뷰

```csharp
// 기존 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 핵심:**
```xml
<!-- 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: 자동 컨텍스트 수집

```csharp
// 사용자 메시지에서 파일명 감지 → 자동 읽기
public class AutoContextCollector
{
    private readonly IAgentTool _fileReadTool;

    // 패턴: "파일명.확장자", "`백틱경로`", "src/path/to/file" 형태 감지
    private static readonly Regex _filePatterns = new(
        @"(?:^|\s)([A-Za-z가-힣_\-\.\/\\]+\.[a-zA-Z]{1,6})(?:\s|$)|`([^`]+)`",
        RegexOptions.Multiline);

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

```csharp
// 코디네이터 모드: 계획/라우팅 전담
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

```csharp
// 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

```csharp
// IAgentTool 구현: 전문 서브에이전트에 작업 위임
public class DelegateAgentTool : IAgentTool
{
    public string Name => "delegate";
    public string Description => "전문 에이전트에 작업을 위임합니다";

    public JsonObject Parameters => new()
    {
        ["type"] = "object",
        ["properties"] = new JsonObject
        {
            ["agent_type"] = new JsonObject
            {
                ["type"] = "string",
                ["enum"] = new JsonArray("researcher", "code-reviewer", "implementer", "security-auditor", "doc-writer"),
                ["description"] = "위임할 에이전트 유형"
            },
            ["task"] = new JsonObject
            {
                ["type"] = "string",
                ["description"] = "에이전트에게 전달할 작업 설명"
            },
            ["context"] = new JsonObject
            {
                ["type"] = "string",
                ["description"] = "에이전트에게 전달할 추가 컨텍스트"
            },
            ["isolation"] = new JsonObject
            {
                ["type"] = "string",
                ["enum"] = new JsonArray("none", "worktree"),
                ["description"] = "격리 수준: worktree → git worktree 격리"
            }
        },
        ["required"] = new JsonArray("agent_type", "task")
    };

    private readonly BackgroundAgentService _backgroundAgents;
    private readonly WorktreeManager _worktreeManager;
    private readonly AgentTypeMemoryRepository _agentMemory;

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

```csharp
// 비동기 서브에이전트 실행 + 완료 알림
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

```csharp
// 에이전트 타입별 영속 메모리
// 저장: %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

```csharp
// 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의 이벤트 로그를 활용한 세션 재생 및 분기 재실행

```csharp
// 세션 리플레이 서비스
public class AgentReplayService
{
    private readonly AgentEventLog _eventLog;
    private readonly AgentLoopService _agentLoop;

    // 특정 시점까지 이벤트 재생
    public async Task ReplayToAsync(string sessionId, long upToSeqNo, CancellationToken ct)
    {
        await foreach (var evt in _eventLog.ReadFromAsync(0).WithCancellation(ct))
        {
            if (evt.SeqNo > upToSeqNo) break;
            await ApplyEventAsync(evt);
        }
    }

    // 특정 시점에서 분기 (새 세션 생성)
    public async Task<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: 플러그인 갤러리

```csharp
// 플러그인 매니페스트
public record PluginManifest
{
    [JsonPropertyName("id")]
    public string Id { get; init; }

    [JsonPropertyName("name")]
    public string Name { get; init; }

    [JsonPropertyName("version")]
    public string Version { get; init; }

    [JsonPropertyName("description")]
    public string Description { get; init; }

    [JsonPropertyName("type")]
    public PluginType Type { get; init; }  // Skill | Tool | Theme

    [JsonPropertyName("entry")]
    public string EntryFile { get; init; }  // zip 내부 진입점 파일

    [JsonPropertyName("min_app_version")]
    public string MinAppVersion { get; init; }
}

// 플러그인 설치 서비스
public class PluginInstallService
{
    // zip 파일에서 설치
    public async Task<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) | 최상위 |

### 핵심 클래스

```csharp
// 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 통합

```csharp
// 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);
}
```

### 슬래시 명령 추가

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

### 설정 항목

```csharp
// AppSettings.LlmSettings
public bool EnableHierarchicalMemory { get; set; } = true;
public string UserMemoryPath { get; set; } = ""; // 기본: %USERPROFILE%\.axcopilot\AX.md
public int MaxIncludeDepth { get; set; } = 5;
```

---

## Phase 19-B — 권한 패턴 매칭 + 복합 명령 분해 (v2.1)

> **목표**: 현재 경로 기반 Allow/Deny → 명령 패턴 매칭 + `&&`/`||`/`;`/`|` 복합 명령 서브 커맨드별 검증.

### 핵심 클래스

```csharp
// CommandPatternMatcher.cs
public class CommandPatternMatcher
{
    /// <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 확장

```csharp
// 기존 PermissionRuleEntry에 패턴 지원 추가
public class PermissionRuleEntry
{
    [JsonPropertyName("tool")]
    public string Tool { get; set; } = "";          // 도구명 또는 "bash:*"

    [JsonPropertyName("pattern")]
    public string Pattern { get; set; } = "*";      // glob 패턴 (bash 명령에만 적용)

    [JsonPropertyName("decision")]
    public string Decision { get; set; } = "allow"; // "allow" | "deny"

    [JsonPropertyName("reason")]
    public string Reason { get; set; } = "";        // 사용자 표시 이유
}
```

### 도구 가시성 필터링

```csharp
// AgentLoopService — LLM 호출 전 비활성 도구를 tools 목록에서 제거
// 현재: 도구 이름은 넘기되 실행 시 차단
// 개선: 아예 스키마 자체를 LLM에 전달하지 않음 → 환각 방지
private IReadOnlyList<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가지 훅 타입 완성.

### 추가 훅 이벤트

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

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

### 훅 타입 완성 (4종)

```csharp
// ExtendedHookRunner.cs 확장
public enum HookCommandType { Shell, Http, Llm, Agent }

public class HookEntry
{
    public AgentHookEvent Event { get; set; }
    public string? Matcher { get; set; }        // tool_name 또는 notification_type 패턴
    public HookCommandType Type { get; set; } = HookCommandType.Shell;

    // Shell 훅
    public string? Command { get; set; }
    public int Timeout { get; set; } = 30;
    public bool Async { get; set; } = false;
    public bool AsyncRewake { get; set; } = false;  // 완료 시 에이전트 재개

    // HTTP 훅
    public string? Url { get; set; }
    public Dictionary<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 입력 수정 기능

```csharp
// 훅이 도구 입력을 수정할 수 있도록 허용
public record HookResult
{
    public bool Continue { get; init; } = true;
    public string? Reason { get; init; }
    public string? SystemMessage { get; init; }
    public JsonElement? UpdatedInput { get; init; }  // PreToolUse에서 도구 파라미터 수정
    public bool SuppressOutput { get; init; } = false;
}
```

---

## Phase 19-D — 스킬 시스템 완성 (v2.1)

> **목표**: 스킬 파일의 `!``cmd`` ` 인라인 실행, 유저급 스킬, 네임스페이스, 스킬별 모델 오버라이드 완성.

### 스킬 인라인 명령 실행

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

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

    /// <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;
    }
}
```

### 스킬 프론트매터 확장

```yaml
---
description: 한 줄 설명 (/skills 목록에 표시)
argument-hint: "[파일명] [옵션]"   # 자동완성 힌트
allowed-tools: [file_read, grep]   # 허용 도구 제한
model: claude-haiku-4-5-20251001   # 스킬별 모델 오버라이드
user-invocable: false              # /skills 목록 숨김 (Claude만 사용)
context: fork                      # 격리 서브에이전트로 실행
paths: "**/*.cs"                   # 이 파일 편집 시 자동 활성화
hooks:                             # 스킬 전용 훅
  PostToolUse:
    - command: "dotnet build"
---
```

### 유저급 스킬 경로

```
%USERPROFILE%\.axcopilot\skills\<skill-name>\SKILL.md   # 유저 전역
<cwd>\.axcopilot\skills\<skill-name>\SKILL.md            # 프로젝트
<cwd>\.axcopilot\skills\<ns>\<skill-name>\SKILL.md       # 네임스페이스 (/<ns>:<skill>)
```

### 스킬 네임스페이스

```csharp
// /database:migrate → .axcopilot/skills/database/migrate/SKILL.md
// /test:unit → .axcopilot/skills/test/unit/SKILL.md
public class SkillNamespaceResolver
{
    public string? ResolveFilePath(string slashCommand, string workFolder) { ... }
    // "database:migrate" → "<workFolder>/.axcopilot/skills/database/migrate/SKILL.md"
}
```

---

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

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

### 세션 관리 API

```csharp
// 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        # 로컬 범위
```

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

### 출력 예산 관리

```csharp
// 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...";
    }
}
```

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

```csharp
// 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 자동 생성 + 스킬/훅 초기 설정 제안.

### 핵심 클래스

```csharp
// 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` |

```csharp
// SdkControlRequest 타입
public enum SdkControlRequestType
{
    Initialize,          // 세션 설정, 커스텀 에이전트 정의, 훅 등록
    SetPermissionMode,   // 권한 모드 변경
    SetModel,            // 모델 전환
    Interrupt,           // 현재 턴 취소
    CanUseTool,          // 권한 처리기 응답
    GetContextUsage,     // 컨텍스트 사용량 조회
    RewindFiles,         // 메시지 이후 파일 변경 되돌리기
    HookCallback,        // SDK측 훅 이벤트 전달
}

// SdkMessage (스트리밍 출력)
public enum SdkMessageType
{
    AssistantToken,      // 스트리밍 텍스트
    ToolProgress,        // 도구 실행 진행
    ToolResult,          // 도구 결과
    SessionResult,       // 최종 결과
    HookEvent,           // 훅 이벤트 (SDK 처리 요청)
    Notification,        // 알림
}
```

---

## Phase 21 — AX Agent UI 전면 개편 (v2.3)

> **목표**: CLAUDE.md Section 13에 명시된 Claude.ai + Codex 스타일 레이아웃 전면 구현.

| # | 기능 | 핵심 컴포넌트 |
|---|------|------------|
| 21-1 | 3-Pane 레이아웃 골격 | `ChatWindow.xaml` 리팩터링 |
| 21-2 | 좌측 사이드바 | `AgentSidebarView` (탭 세그먼트 + 프리셋 + 이력) |
| 21-3 | 세션 헤더 바 | `AgentSessionHeaderBar` (모델/Plan/권한/도구 칩) |
| 21-4 | 우측 설정 패널 | `AgentSettingsPanel` (SettingsWindow AX Agent 탭 완전 대체) |
| 21-5 | 고도화 입력 영역 | `AgentInputArea` (@멘션, /스킬, 하단 칩 열) |
| 21-6 | 메시지 버블 개선 | Claude.ai 스타일 (AI=배경 없음, 도구블록=접히는 Border) |
| 21-7 | SettingsWindow 정리 | AX Agent 탭 제거, 전역 설정만 유지 |

---

## Phase 22 — 슬래시 명령 체계 완성 (v2.1) ✅ 완료

> **목표**: Claude Code의 16종 슬래시 명령을 AX Agent에 구현. `/init`(19-G)을 제외한 15종 명령 추가.
> 기존 스킬 기반 `/` 호출과 공존하되, 내장 명령은 스킬보다 우선 처리.
>
> **구현 완료** (2026-04-03): ISlashCommand 인터페이스, SlashCommandRegistry, InputRouter, 13종 명령 (/compact, /clear, /memory, /model, /plan, /commit, /review, /mcp, /permissions, /hooks, /config, /skills, /help) + SlashAutoCompleteProvider.

### 22-1: ISlashCommand 인터페이스 + 레지스트리

```csharp
// 슬래시 명령 인터페이스 (Command 패턴)
public interface ISlashCommand
{
    string Name { get; }                    // "compact", "memory" 등
    string[] Aliases { get; }               // "/settings" → "/config" 별칭
    string Description { get; }
    string? ArgumentHint { get; }           // 자동완성 힌트
    bool RequiresActiveSession { get; }     // 세션 없이도 사용 가능 여부

    Task ExecuteAsync(string arguments, IAgentChatContext context, CancellationToken ct);
}

// 채팅 컨텍스트 인터페이스 (DIP: 명령이 UI에 직접 의존하지 않음)
public interface IAgentChatContext
{
    AgentLoopService AgentLoop { get; }
    SettingsService Settings { get; }
    SkillLoaderService SkillLoader { get; }
    HookRunnerService HookRunner { get; }
    McpManagerService McpManager { get; }
    AgentSessionRepository SessionRepo { get; }
    LlmService Llm { get; }
    string ActiveTab { get; }               // "Chat" | "Cowork" | "Code"
    string WorkFolder { get; }

    // UI 상호작용 (ViewModel 위임)
    Task SendSystemMessageAsync(string message);
    Task<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 — 컨텍스트 압축

```csharp
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 — 대화 초기화

```csharp
public class ClearCommand : ISlashCommand
{
    public string Name => "clear";
    public string[] Aliases => new[] { "reset", "new" };
    public string Description => "대화 기록을 지우고 새 세션을 시작합니다";
    public string? ArgumentHint => null;
    public bool RequiresActiveSession => false;

    public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct)
    {
        // 1. 현재 세션 저장
        await ctx.SessionRepo.SaveAsync(ctx.AgentLoop.CurrentSession);

        // 2. UI 메시지 목록 초기화
        ctx.ClearMessages();

        // 3. 새 세션 시작
        await ctx.AgentLoop.StartNewSessionAsync(ctx.ActiveTab, ct);

        // 4. SessionStart 훅 발화 (source: "clear")
        await ctx.HookRunner.RunAsync(HookEvent.SessionStart, new HookContext
        {
            Event = HookEvent.SessionStart,
            SessionId = ctx.AgentLoop.CurrentSessionId,
            // source = "clear"
        }, ct);

        await ctx.SendSystemMessageAsync("새 대화가 시작되었습니다.");
    }
}
```

### 22-4: /memory — 메모리 파일 편집

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

```csharp
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 — 플랜 모드 토글

```csharp
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 커밋 메시지

```csharp
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 서버 관리

```csharp
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

```csharp
// /permissions — 권한 규칙 관리
public class PermissionsCommand : ISlashCommand
{
    public string Name => "permissions";
    public string[] Aliases => new[] { "allowed-tools" };
    public string Description => "도구 허용/차단 규칙을 관리합니다";

    public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct)
    {
        // 현재 권한 모드 + allow/deny 규칙 목록 표시
        // "allow Bash(git *)" / "deny Bash(rm -rf *)" 형식 인수 처리
        var settings = ctx.Settings.Settings.Llm;
        var sb = new StringBuilder("## 권한 설정\n");
        sb.AppendLine($"현재 모드: **{settings.PermissionMode}**\n");

        sb.AppendLine("### 허용 규칙");
        foreach (var rule in settings.AllowRules)
            sb.AppendLine($"- ✅ `{rule}`");

        sb.AppendLine("\n### 차단 규칙");
        foreach (var rule in settings.DenyRules)
            sb.AppendLine($"- ❌ `{rule}`");

        await ctx.SendSystemMessageAsync(sb.ToString());
    }
}

// /hooks — 훅 설정 표시
public class HooksCommand : ISlashCommand
{
    public string Name => "hooks";
    public string[] Aliases => Array.Empty<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: 입력 라우팅 통합

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

```csharp
// 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: 스킬 네임스페이싱 (서브디렉토리 콜론 구분)

```csharp
// 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 구문)

```csharp
// 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 통합

```csharp
// 스킬 실행 파이프라인: 인수 치환 → 인라인 셸 → 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

```csharp
// 복합 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

```csharp
// 훅 실행 시 셸 환경변수를 자동 주입
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

```csharp
// 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 자동 트리거

```csharp
// 컨텍스트 크기 모니터링 → 자동 컴팩션
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() 자가 비활성화

```csharp
// 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.md` → `database:migrate` 자동 변환
- 중간 디렉토리(SKILL.md 없음)는 건너뛰고 더 깊이 탐색

### 24-B: 고급 프론트매터 필드
- `context`: "fork"이면 격리 서브에이전트에서 실행 (플래그 설정, 실행은 AgentLoopService에서 처리)
- `model`: 스킬별 모델 오버라이드
- `user-invocable`: false면 `/` 자동완성에서 숨김 (AI만 사용)
- `paths`: 파일 경로 글로브 — 매칭 파일 터치 시 자동 제안
- `hooks`: 스킬 스코프 훅 정의 (JSON)
- `arguments`: 명명된 인수 리스트 (`[$name, $dir]`)
- `when_to_use`: AI 선제적 사용 힌트
- `version`: 스킬 버전

### 24-C: SkillDefinition 확장
- 10개 새 프로퍼티 추가 (`Context`, `ModelOverride`, `UserInvocable`, `Paths`, `ScopedHooks`, `Arguments`, `WhenToUse`, `Version`, `IsForkContext`)
- `ParseSkillFile()` 2-param 오버로드 (네임스페이스 이름 주입)
- `MatchSlashCommand()` — `UserInvocable` 필터 적용
- `PrepareSkillBodyAsync()` — `Arguments` 필드 연동

---

## Phase 25 — 훅 고급 기능 + 도구 결과 크기 제한 (v2.1) ✅ 완료

> **목표**: 훅 조건부 실행, asyncRewake, 누락 이벤트 추가. 도구 결과 오버플로우 방지.
>
> **구현 완료** (2026-04-03):

### 25-A: Hook `if` 조건부 실행
- `EvaluateCondition()` 완전 구현 — `"field == value"`, `"field != value"`, `"field contains value"` 지원
- `&&` 연결로 복합 조건 지원
- 지원 필드: `tool_name`, `event`, `session_id`, `work_folder`, `user_message`, `changed_file`

### 25-B: Hook asyncRewake + 이벤트 확장
- `ExtendedHookEntry.AsyncRewake` 필드 추가
- asyncRewake 훅: 비동기 실행 후 exit code 2 시 `AsyncRewakeTriggered` 이벤트 발생
- `AsyncRewakeEventArgs` — HookName, AdditionalContext, SessionId
- `HookEventKind` 확장: `PostToolUseFailure`, `Stop` 추가 (총 17개, CC 동등)

### 25-C: 도구 결과 크기 제한 (ToolResultSizer)
- `ToolResultSizer.Apply()` — 50,000자 초과 시 임시 파일 저장 + head/tail 프리뷰 반환
- `ToolResultSizeInfo` — Output, WasTruncated, SpilloverFilePath, OriginalLength
- `CleanupOldResults()` — 24시간 지난 임시 결과 자동 정리
- CC의 `maxResultSizeChars` + temp file spillover 패턴 완전 재현

---

## Phase 26 — /init 프로젝트 초기화 (v2.1) ✅ 완료

> **목표**: CC의 `/init` 명령 동등 — 코드베이스 분석 → AX.md 자동 생성.
>
> **구현 완료** (2026-04-03):

### InitCommand (/init)
- 프로젝트 구조 트리 (3레벨, node_modules/.git 등 제외)
- Git 정보 수집 (브랜치, 리모트, 최근 커밋)
- 기술 스택 자동 감지 (Node.js/TypeScript/.NET/Rust/Go/Python/Java/Docker/GitHub Actions)
- 주요 설정 파일 내용 수집 (package.json, *.csproj, go.mod 등)
- LLM에게 프로젝트 분석 → AX.md 5섹션 자동 생성
  - 프로젝트 개요, 코드 컨벤션, 빌드/테스트, 작업 가이드라인, 주의사항
- 기존 AX.md 존재 시 덮어쓰기 방지

---

---

## Phase 27 — 미통합 인프라 연결 (v2.1) ✅ 완료

> **목표**: 이미 정의되었으나 미사용 상태인 인프라(ToolResultSizer, PathBasedSkillActivator, AxMdIncludeResolver)를 실제 코드에 통합.
>
> **구현 완료** (2026-04-03):

### 27-A: ToolResultSizer → AgentLoopService 통합
- `AgentLoopService.cs:870` — 기존 `TruncateOutput(result.Output, 4000)` → `ToolResultSizer.Apply(result.Output, call.ToolName)` 교체
- 50,000자 초과 결과 → 임시 파일 + head/tail 프리뷰 → LLM에 전달

### 27-B: PathBasedSkillActivator → 시스템 프롬프트 통합
- `ChatWindow.xaml.cs` — `BuildPathBasedSkillSection()` 추가
- 최근 5개 메시지에서 파일 경로 패턴 추출 → `PathBasedSkillActivator.GetActiveSkillsForFile()` → 시스템 프롬프트 자동 주입
- `SkillDefinitionExtensions.GetPathPatterns()` — Phase 24의 `Paths` 프로퍼티 우선 사용, ExtensionStore 폴백

### 27-C: AxMdIncludeResolver → AX.md 로딩 통합
- `LoadProjectContext()` — AX.md 내용에 `@` 포함 시 `AxMdIncludeResolver.ResolveAsync()` 실행
- `@./relative/path`, `@~/home`, `@/absolute` 5레벨 깊이 재귀 해석
- 순환 참조 감지, 파일 미존재 시 HTML 주석으로 대체

---

## Phase 28 — 세션 매니저 (v2.1) ✅ 완료

> **목표**: CC의 세션 관리(save/resume/fork/tag)와 동등한 세션 영속성 시스템.
>
> **구현 완료** (2026-04-03):

### SessionManager
- `SaveSessionAsync()` / `LoadSessionAsync()` — JSON 기반 세션 저장/복원
- `ForkSessionAsync()` — 기존 세션에서 분기 (메시지 복사 + "forked" 태그 + ParentSessionId)
- `TagSessionAsync()` / `RenameSessionAsync()` — 태그/이름 관리
- `ListSessions()` — 최신순 목록 (최대 50개)
- `FindByTag()` — 태그 기반 검색
- `GetMostRecent()` — CC의 `--continue` 동등 (탭별 최근 세션)
- `CleanupOldSessions()` — 30일 보존 기간 경과 시 자동 삭제
- `AgentSession` — Id, Name, Tab, WorkFolder, Model, Messages, Tags, ParentSessionId, CreatedAt/UpdatedAt
- `AgentSessionSummary` — 목록 표시용 경량 DTO

---

## Phase 29 — 최종 통합 & 검증 (v2.1) ✅ 완료

> **목표**: Phase 22~28에서 생성된 클래스들을 실제 코드 경로에 연결하고, CC 기능 매트릭스를 검증.
>
> **구현 완료** (2026-04-03):

### 29-A: AutoCompactMonitor → AgentLoopService 통합
- `AgentLoopService.RunAsync` 메인 루프에서 `LastTokenUsage` 기반 컨텍스트 사용량 모니터링
- `AutoCompactThreshold` (기본 80%) 초과 시 `ContextCondenser.CondenseIfNeededAsync()` 자동 실행
- `AppSettings.LlmSettings.AutoCompactThreshold` 설정 추가

### 29-B: IConditionalTool + ToolEnvironmentContext 통합
- `AgentLoopService` line 339: `ToolEnvironmentContext` 빌드 후 `GetActiveTools(disabled, env)` 오버로드 사용
- `.git` 폴더 존재 여부로 `HasGitRepo` 자동 감지
- `GitTool` → `IConditionalTool` 구현 (HasGitRepo=false 시 도구 목록에서 자동 제외)

### 29-C: SessionManager → AgentLoopService 통합
- `RunAsync` finally 블록에서 세션 자동 저장 (`SessionManager.SaveSessionAsync`)
- 사용자 질의 첫 40자를 세션 이름으로 자동 설정
- 현재 탭, 모델, 작업 폴더, 메시지를 `AgentSession`으로 영속화

### 29-D: CC 기능 매트릭스 상태 갱신
- 33개 기능 항목 중 **29개 ✅ 구현 완료**, 4개 📋 계획 (SDK, 멀티에이전트, 에이전트 메모리, 3-Pane UI)
- Phase 22~29 구현 결과 반영

---

## Phase 30 — 아키텍처 정제 & 계층 메모리 (v2.1) ✅ 완료

> **목표**: DIP 원칙 적용, CC 4-layer 계층 메모리 통합, 기존 구현 누락 항목 매트릭스 반영.
>
> **구현 완료** (2026-04-03):

### 30-A: CC 기능 매트릭스 재갱신
- 기존 멀티에이전트(SubAgentTool, DelegateAgentTool, WorktreeManager) 및 에이전트 메모리(AgentTypeMemoryRepository, MemoryTool, /memory) 확인 → ✅ 반영
- 35개 항목 중 **33개 ✅ 구현 완료**, 2개 📋 계획 (SDK, 3-Pane UI)

### 30-B: IAgentMemoryRepository 인터페이스 추출
- `IAgentMemoryRepository` 인터페이스 신규 — LoadMemoryAsync, SaveMemoryAsync, AppendLearnAsync, GetAgentTypes, DeleteMemory
- `AgentTypeMemoryRepository` → `IAgentMemoryRepository` 구현
- `DelegateAgentTool` → 구체 클래스 대신 `IAgentMemoryRepository` 인터페이스에 의존 (DIP)

### 30-C: HierarchicalMemoryService — 4-layer 메모리 통합 조회
- 4단계 우선순위: Managed(%PROGRAMDATA%) → User(%APPDATA%) → Project(WorkFolder/AX.md + .ax/rules/) → Local(AX.local.md)
- `CollectAll()` — 모든 레벨의 MemoryFile 수집
- `BuildMergedContext()` — 단일 문자열 병합 (maxChars 제한, 시스템 프롬프트 삽입용)
- `GetByLevel()` — 특정 레벨만 필터 조회
- `ChatWindow.LoadProjectContext()` → HierarchicalMemoryService 우선 사용, 실패 시 레거시 폴백

### 30-D: DelegateAgentTool에 IConditionalTool 적용
- AI 비활성화(`AiEnabled=false`) 시 delegate 도구 자동 비활성화

---

## Phase 31 — SDK 제어 프로토콜 (v2.2) ✅ 완료

> **목표**: CC의 JSON 스트리밍 SDK와 동등한 프로그래밍 방식 에이전트 제어 프로토콜.
> 외부 IDE, 스크립트, CI/CD에서 AX Agent를 stdin/stdout JSON으로 제어.
>
> **구현 완료** (2026-04-03):

### 31-A: SDK 메시지 타입 정의 (`Services/Agent/Sdk/SdkProtocol.cs`)
- `SdkControlRequestType` — 9종: Initialize, UserMessage, SetPermissionMode, SetModel, Interrupt, GetContextUsage, RewindFiles, CanUseToolResponse, Terminate
- `SdkControlRequest` — Host → CLI 요청 (Type + Id + Payload)
- `SdkMessageType` — 11종: InitializeResult, AssistantToken, AssistantMessage, ToolUseStart, ToolProgress, ToolResult, SessionResult, HookEvent, CanUseTool, Notification, Error
- `SdkMessage` — CLI → Host 응답/이벤트
- 데이터 클래스: SdkInitializePayload, SdkUserMessagePayload, SdkTokenData, SdkToolUseStartData, SdkToolResultData, SdkSessionResultData, SdkCanUseToolData, SdkErrorData

### 31-B: AgentSdkServer (`Services/Agent/Sdk/AgentSdkServer.cs`)
- stdin/stdout 기반 양방향 JSON 스트리밍 서버
- Initialize → 설정 적용 + 커스텀 에이전트 등록 + 훅 콜백 등록
- UserMessage → AgentLoopService.RunAsync 실행 + 스트리밍 이벤트 전달
- SetModel/SetPermissionMode → 런타임 설정 변경
- Interrupt → 현재 턴 취소
- GetContextUsage → 토큰 사용량 조회
- AgentEvent → SdkMessage 자동 변환 (이벤트 구독)
- Thread-safe 출력 (SemaphoreSlim)

### 31-C: CustomAgentDefinition + AgentTypeRegistry (`Services/Agent/Sdk/AgentTypeRegistry.cs`)
- `CustomAgentDefinition` — name, description, prompt, model, allowedTools, disallowedTools, maxTurns, permissionMode
- `AgentTypeRegistry` — CRUD + 목록 조회, Initialize 시 등록

### 31-D: SdkHookCallbackHandler (`Services/Agent/Sdk/SdkHookCallbackHandler.cs`)
- 도구 사용 허가 요청 → Host로 CanUseTool 전송 → 30초 타임아웃 대기 → Allow/Deny
- 훅 이벤트 알림 → 등록된 이벤트만 Host에 전달
- TaskCompletionSource 기반 비동기 응답 대기

---

## Phase 32 — 3-Pane Claude.ai UI 기반 구조 (v2.3) ✅ 완료

> **목표**: CC의 Claude.ai + Codex 레이아웃과 동등한 3-Pane UI 구조를 수립.
> 새 UserControl 컴포넌트 4종 + ChatWindow 통합.
>
> **구현 완료** (2026-04-03):

### 32-A: AgentSettingsPanel (`Views/Controls/AgentSettingsPanel.xaml/.cs`)
- 우측 슬라이드인 설정 패널 (TranslateTransform X 애니메이션 200ms/150ms)
- **모델 & 서비스**: LLM 서비스 선택, 모델 선택, API 엔드포인트 입력
- **에이전트 동작**: 최대 반복 횟수(Slider), 오류 재시도(Slider), 병렬 도구 실행(Toggle)
- **탭 전용 설정**: Cowork(검증 강제) / Code(LSP, 코드 검증) 자동 분기
- **도구 관리**: 등록된 모든 도구의 개별 Toggle 동적 생성
- **고급**: 자동 컴팩션 임계치(Slider), 프로젝트 규칙, 개발자 모드
- 설정 변경 즉시 저장 (`SettingsService.Save()`)

### 32-B: AgentSidebarView (`Views/Controls/AgentSidebarView.xaml/.cs`)
- Claude.ai 스타일 좌측 사이드바 (260px, 접기 시 48px 애니메이션)
- 탭 세그먼트 (Chat|Cowork|Code) — AccentColor 배경 강조
- 새 대화 버튼, 검색 필드
- 날짜 그룹별 대화 이력 (오늘/어제/이전 7일/30일)
- 호버 효과, 대화 선택/삭제 이벤트

### 32-C: AgentSessionHeaderBar (`Views/Controls/AgentSessionHeaderBar.xaml/.cs`)
- Codex 스타일 세션 헤더 바 (42px)
- 모델 칩 (클릭 → 모델 변경), Plan 칩 (3-state 순환), 권한 칩 (4-state 순환)
- 설정 패널 토글 버튼
- 모든 칩: Border + MouseLeftButtonUp + 호버 효과 (CLAUDE.md 원칙 준수)

### 32-D: AgentInputArea (`Views/Controls/AgentInputArea.xaml/.cs`)
- Claude.ai 스타일 입력 영역
- 상단 툴바: @ 멘션 / / 스킬 / 첨부 버튼
- 멀티라인 TextBox (40~200px 자동 확장, Ctrl+Enter 전송)
- 하단 칩 열: 모델명, 권한, Plan 상태 표시
- 전송/중단 버튼 전환 (스트리밍 상태 기반)
- 첨부 파일 칩 (추가/제거)

### 32-E: ChatWindow.xaml 3-Pane 통합
- Grid Column 5 추가 (`SettingsPanelColumn`, Auto)
- `AgentSettingsPanel` 배치 — Shift+설정 버튼 클릭으로 토글
- `xmlns:ctrl` 네임스페이스 등록
- 기존 UI 완전 호환 유지 (기존 사이드바/메인 영역 변경 없음)

---

## Phase 33 — 코드 품질 리팩터링 (v2.3) ✅ 완료

> **목표**: Claude Code 동등 수준의 코드 품질 달성. 기능 파라티가 아닌 구조적 품질 파라티.
> AgentLoopService (925줄 RunAsync, 매직 넘버, 레이스 컨디션) + ChatWindow.xaml.cs (176건 브러시 중복, 46건 폰트 중복) 정리.

### 33-A: AgentLoopService — 매직 넘버 상수화
- `Defaults` 내부 정적 클래스 신설 (16개 상수)
- MaxIterations, MaxRetryOnError, MaxTestFixIterations, MaxPlanRegenerationRetries 등
- ToolCallThresholdForExpansion, ContextCompressionTargetRatio, AutoCompactThresholdPercent
- QueryNameMaxLength, QueryTitleMaxLength, ThinkingTextMaxLength, VerificationSummaryMaxLength 등

### 33-B: AgentLoopService — RunAsync 분해 (ProcessSingleToolCallAsync 추출)
- `ProcessSingleToolCallAsync()` 메서드로 단일 도구 실행 로직 약 250줄 추출
- `ToolExecutionState` 클래스: 루프 상태 공유 (카운터, 통계, 플랜 메타데이터)
- `ToolCallAction` enum: Continue/Break/Return 루프 제어
- `RunToolHooksAsync()` 헬퍼: Pre/Post 훅 실행 통합 (중복 코드 제거)
- RunAsync: 925줄 → 약 680줄 (약 27% 감소)

### 33-C: AutoCompactMonitor + ContextCondenser 통합
- 기존: 별도 실행되어 이중 압축 가능성
- 통합: `!condensed` 가드로 단일 흐름 보장
- 1단계: ContextCondenser 기본 압축 → 2단계: AutoCompactMonitor 임계치 기반 적극적 압축

### 33-D: SessionManager DI 기반 개선
- 기존: `new SessionManager()` + `_ = SaveSessionAsync()` fire-and-forget
- 개선: 생성자 주입 (`SessionManager? sessionManager = null`) + `await` + 에러 로깅

### 33-E: ChatWindow — ThemeResourceHelper 통합 적용
- `ThemeResourceHelper` 정적 헬퍼 (Phase 32에서 생성) 실제 적용
- `TryFindResource("XXX") as Brush ?? Brushes.YYY` 패턴 ~162건 → `ThemeResourceHelper.Primary(this)` 등으로 교체
- `new FontFamily("Segoe MDL2 Assets")` 46건 → `ThemeResourceHelper.SegoeMdl2` 캐시 참조로 교체

### 33-F: ActiveTab 레이스 컨디션 + 에러 핸들링 정리
- `activeTabSnapshot` 캡처 — RunAsync 진입 시 스냅샷, 루프 전체에서 일관 사용
- `BuildContext(tabOverride)` — 스냅샷 기반 컨텍스트 생성
- RunPostToolVerificationAsync: `ActiveTab` → `context.ActiveTab` 참조
- ExecuteToolsInParallelAsync: 감사 로그 `ActiveTab` → `context.ActiveTab`
- bare `catch {}` 5건 → `catch (Exception ex) { LogService.Warn(...) }` 패턴으로 교체
- bare `catch { break; }` → 오류 메시지 로깅 추가

---

## Phase 34 — ChatWindow God Class 분해 (v2.3) ✅ 완료

> **목표**: 10,372줄 ChatWindow.xaml.cs의 구조적 개선. 중복 제거, 대형 메서드 분해, 헬퍼 추출.

### 34-A: SendMessageAsync Cowork/Code 중복 제거
- 기존: Cowork(35줄)과 Code(35줄) 블록이 거의 동일 — DRY 위반
- `RunAgentLoopAsync(tab, sendMessages, ct)` 단일 메서드로 통합 (~70줄 → ~40줄)
- 탭별 시스템 프롬프트 분기, 완료 알림 탭별 라벨 통합

### 34-B: ThemeResourceHelper 완전 적용
- 잔여 14건 `TryFindResource` 교체 (HintBackground, HintText, SeparatorColor)
- ThemeResourceHelper에 `Hint()`, `HintFg()`, `Separator()` 메서드 추가
- **TryFindResource 잔여: 0건** (전량 ThemeResourceHelper로 이전 완료)
- `new FontFamily("Consolas")` 8건 → `ThemeResourceHelper.Consolas` 캐시 참조

### 34-C: PopupMenuHelper 정적 헬퍼 생성 (`Views/PopupMenuHelper.cs`)
- ChatWindow에서 6회 이상 반복되는 `Popup + Border + StackPanel + DropShadow` 패턴 통합
- `Create(target, owner, ...)` — 테마 적용된 팝업 메뉴 구조 생성
- `MenuItem(text, fg, hoverBg, onClick, isChecked, icon)` — 메뉴 항목 생성
- `Separator()`, `SectionHeader()` — 구분선/섹션 제목
- 기존 팝업 메서드들의 점진적 마이그레이션 기반 확보

### 34-D: AgentLoopService 잔여 정리
- 마지막 bare `catch {}` → `catch (Exception)` + 주석 명시 (document_plan JSON 파싱)

---

## Phase 35 — 코드 품질 심층 정리 (v2.3) ✅ 완료

> **목표**: 전체 코드베이스의 bare catch, 하드코딩 색상, 서비스 로케이터, 체인 액세스 패턴을 체계적으로 정리.

### 35-A: Bare catch 전량 정리 (109개 파일)
- `catch { }` 및 `catch { return X; }` 등 **모든 bare catch** → `catch (Exception)` + 상황별 주석
- 전체 코드베이스 109개 .cs 파일에 PowerShell 일괄 치환 적용
- 빈 catch, return 포함 catch, 값 할당 catch 등 모든 변형 포함

### 35-B: ColorConverter.ConvertFromString 헬퍼 추출
- `ThemeResourceHelper.HexBrush(hex)` / `HexColor(hex)` 정적 메서드 추가
- ChatWindow.xaml.cs 47건 `new SolidColorBrush((Color)ColorConverter.ConvertFromString(...))` → `ThemeResourceHelper.HexBrush(...)` 일괄 치환
- SettingsWindow, ShortcutHelpWindow, HelpDetailWindow, StatisticsViewModel 등 7개 추가 파일 34건 치환
- 총 **81건** 장황한 색상 변환 패턴 제거

### 35-C: ChatWindow `_settings.Settings.Llm` 체인 액세스 캐싱
- `private LlmSettings Llm => _settings.Settings.Llm;` 편의 프로퍼티 추가
- 92개 `_settings.Settings.Llm.` 체인 → `Llm.` 단축 치환
- 가독성 향상 + 향후 리팩터링 시 단일 변경 지점 확보

### 35-D: AgentContext.Settings 주입 — 서비스 로케이터 제거
- `AgentContext`에 `Settings` (AppSettings?) + `Llm` (LlmSettings?) 프로퍼티 추가
- `AgentLoopService.BuildContext()`에서 Settings 주입
- 에이전트 도구 11개 파일의 `Application.Current as App` 서비스 로케이터 패턴 제거:
  - BuildRunTool, CodeSearchTool, CodeReviewTool, MemoryTool, DocumentPlannerTool,
    HttpTool, LspTool, SkillManagerTool, SubAgentTool, SnippetRunnerTool, TestLoopTool
- `context.Llm?.X` 패턴으로 교체 — DIP(의존성 역전 원칙) 준수

---

## Phase 36 — 서비스 로케이터 제거 + PopupMenuHelper 적용 (v2.3) ✅ 완료

> **목표**: `Application.Current as App` 서비스 로케이터 패턴을 전체 코드베이스에서 제거하고, PopupMenuHelper를 실제 팝업 생성 코드에 적용.

### 36-A: `Application.Current as App` → `CurrentApp` 정적 프로퍼티 일원화
- SettingsWindow (17건), ChatWindow (4건), 기타 12개 파일에 `private static App? CurrentApp` 프로퍼티 추가
- 모든 인라인 `Application.Current as App` 호출을 `CurrentApp`으로 교체
- ChatWindow의 설정 접근은 기존 `Llm` 프로퍼티 + `_settings.Save()` 패턴으로 직접 대체
- **39건 → 0건** (프로퍼티 정의 제외) 서비스 로케이터 인라인 호출 제거

### 36-B: PopupMenuHelper 실제 적용 (ChatWindow 3개 팝업)
- `BtnCategoryDrop_Click` — `Popup+Border+StackPanel+DropShadow` 30줄 → `PopupMenuHelper.Create()` 2줄
- `ShowConversationMenu` — 팝업 구조 + `CreateMenuItem` 로컬 함수 80줄 → `PopupMenuHelper.Create()` + `PopupMenuHelper.MenuItem()` 래핑 10줄
- `ShowFileTreeContextMenu` — 팝업 구조 + `AddItem`/`AddSep` 로컬 함수 60줄 → `PopupMenuHelper` 대체 5줄
- 모델 선택 팝업 (`BtnModelSelector`) — 팝업 구조 35줄 → `PopupMenuHelper.Create()` 1줄 (커스텀 애니메이션 항목은 유지)
- ChatWindow: **10,353줄 → 10,184줄** (169줄 감소)

---

## Phase 37 — ChatWindow God Class 파셜 클래스 분할 (v2.3) ✅ 완료

> **목표**: 10,184줄 ChatWindow.xaml.cs를 7개 파셜 클래스 파일로 분할하여 메인 파일을 4,767줄로 축소 (53.2% 감소).

### 분할 결과

| # | 파일명 | 줄 수 | 추출 내용 |
|---|--------|-------|----------|
| 1 | `ChatWindow.MessageRendering.cs` | 522 | 메시지 렌더링, 체크 아이콘, 액션 버튼 |
| 2 | `ChatWindow.SlashCommands.cs` | 579 | 슬래시 명령 팝업, 드래그앤드롭 AI 액션 |
| 3 | `ChatWindow.AgentSupport.cs` | 475 | 에이전트 루프, 시스템 프롬프트, 워크플로우 |
| 4 | `ChatWindow.TaskDecomposition.cs` | 1,170 | Plan UI, Diff 뷰, 진행률, 이벤트 배너 |
| 5 | `ChatWindow.Presets.cs` | 1,280 | 주제 버튼, 프리셋 관리, 하단바, 설정 토글 |
| 6 | `ChatWindow.ModelSelector.cs` | 395 | 모델 선택, 프롬프트 템플릿, 대화 관리 |
| 7 | `ChatWindow.PreviewAndFiles.cs` | 1,105 | 미리보기 패널, 진행률 바, 파일 탐색기 |
| — | **ChatWindow.xaml.cs (메인)** | **4,767** | 생성자, 탭 전환, 전송, 대화 목록, 권한 등 |

### 핵심 성과
- **메인 파일**: 10,184줄 → 4,767줄 (**53.2% 감소**)
- **총 코드**: 10,293줄 (리다이렉트 주석 + 파셜 파일 헤더 오버헤드 ~1%)
- **빌드**: 경고 0, 오류 0
- **각 파셜 파일**: 독립적 섹션으로 응집도 높은 메서드 그룹
- **필드 이동**: 각 섹션에 속하는 필드를 함께 이동하여 관련 코드 근접 배치

---

## Phase 38 — SettingsWindow 파셜 클래스 분할 (v2.3) ✅ 완료

> **목표**: 3,216줄 SettingsWindow.xaml.cs를 3개 파셜 클래스 파일로 분할.

| 파일 | 줄 수 | 내용 |
|------|-------|------|
| `SettingsWindow.xaml.cs` (메인) | 373 | 생성자, 필드, 저장/닫기, 스니펫 이벤트 |
| `SettingsWindow.UI.cs` | 802 | 섹션 헬퍼, 탭 전환, 독바, 스토리지, 핫키, 버전 |
| `SettingsWindow.Tools.cs` | 875 | 도구/커넥터 카드 UI, AX Agent 탭, 도구 관리 |
| `SettingsWindow.AgentConfig.cs` | 1,202 | 모델 등록, 스킬, 템플릿, AI토글, 네트워크모드, 훅, MCP |

- **메인 파일**: 3,216줄 → 373줄 (**88.4% 감소**)
- **빌드**: 경고 0, 오류 0

---

## Phase 39 — FontFamily 캐싱 + LauncherWindow 파셜 분할 (v2.3) ✅ 완료

> **목표**: 89개 `new FontFamily(...)` 반복 생성 제거 + LauncherWindow 파셜 분할.

### FontFamily 캐싱 (25개 파일)

ThemeResourceHelper에 5개 정적 필드 추가:
- `SegoeMdl2` — `new FontFamily("Segoe MDL2 Assets")` (기존)
- `Consolas` — `new FontFamily("Consolas")` (기존)
- `CascadiaCode` — `new FontFamily("Cascadia Code, Consolas, monospace")` (신규)
- `ConsolasCode` — `new FontFamily("Consolas, Cascadia Code, Segoe UI")` (신규)
- `ConsolasCourierNew` — `new FontFamily("Consolas, Courier New")` (신규)

총 89개 `new FontFamily(...)` 호출 → 정적 캐시 필드 참조로 교체 (25개 파일)

### LauncherWindow 파셜 분할

| 파일 | 줄 수 | 내용 |
|------|-------|------|
| `LauncherWindow.xaml.cs` (메인) | 578 | Win32 P/Invoke, 생성자, Show(), 아이콘 20종 애니메이션 |
| `LauncherWindow.Theme.cs` | 116 | ApplyTheme, 테마 빌드, BuildCustomDictionary, IsSystemDarkMode |
| `LauncherWindow.Animations.cs` | 153 | 무지개 글로우, 애니메이션 헬퍼, CenterOnScreen, AnimateIn |
| `LauncherWindow.Keyboard.cs` | 593 | IME 검색, PreviewKeyDown, KeyDown 20여 단축키, ShowToast |
| `LauncherWindow.Shell.cs` | 177 | Shell32 P/Invoke, SendToRecycleBin, ShowLargeType, 클릭 핸들러 |

- **메인 파일**: 1,563줄 → 578줄 (**63.0% 감소**)
- **빌드**: 경고 0, 오류 0

---

## Phase 40 — ChatWindow 2차 파셜 클래스 분할 (v2.3) ✅ 완료

> **목표**: 4,767줄 ChatWindow.xaml.cs (1차 분할 후 잔여)를 7개 파셜 파일로 추가 분할.

| 파일 | 줄 수 | 내용 |
|------|-------|------|
| `ChatWindow.xaml.cs` (메인) | 262 | 필드, 생성자, OnClosing, ForceClose, ConversationMeta |
| `ChatWindow.Controls.cs` | 595 | 사용자 정보, 스크롤, 제목 편집, 카테고리 드롭다운, 탭 전환 |
| `ChatWindow.WorkFolder.cs` | 359 | 작업 폴더 메뉴, 폴더 설정, 컨텍스트 메뉴 |
| `ChatWindow.PermissionMenu.cs` | 498 | 권한 팝업, 데이터 활용 메뉴, 파일 첨부, 사이드바 토글 |
| `ChatWindow.ConversationList.cs` | 747 | 대화 목록, 그룹 헤더, 제목 편집, 검색, 날짜 포맷 |
| `ChatWindow.Sending.cs` | 720 | 편집 모드, 타이머, SendMessageAsync, BtnSend_Click |
| `ChatWindow.HelpCommands.cs` | 157 | /help 도움말 창, AddHelpSection |
| `ChatWindow.ResponseHandling.cs` | 1,494 | 응답재생성, 스트리밍, 내보내기, 팁, 토스트, 상태바, 키보드 |

- **메인 파일**: 4,767줄 → 262줄 (**94.5% 감소**)
- **전체 ChatWindow 파셜 파일 수**: 15개 (1차 7개 + 2차 7개 + 메인 1개)
- **빌드**: 경고 0, 오류 0

---

## Phase 41 — SettingsViewModel·AgentLoopService 파셜 분할 (v2.3) ✅ 완료

> **목표**: SettingsViewModel (1,855줄)·AgentLoopService (1,823줄) 파셜 클래스 분할.

### SettingsViewModel 분할

| 파일 | 줄 수 | 내용 |
|------|-------|------|
| `SettingsViewModel.cs` (메인) | 320 | 클래스 선언, 필드, 이벤트, 생성자 |
| `SettingsViewModel.Properties.cs` | 837 | 바인딩 프로퍼티 전체 (LLM, 런처, 기능토글, 테마 등) |
| `SettingsViewModel.Methods.cs` | 469 | Save, Browse, AddShortcut, AddSnippet 등 메서드 |
| `SettingsViewModelModels.cs` | 265 | ThemeCardModel, ColorRowModel, SnippetRowModel 등 6개 모델 클래스 |

- **메인 파일**: 1,855줄 → 320줄 (**82.7% 감소**)

### AgentLoopService 분할

| 파일 | 줄 수 | 내용 |
|------|-------|------|
| `AgentLoopService.cs` (메인) | 1,334 | 상수, 필드, 생성자, RunAsync 루프, 헬퍼 |
| `AgentLoopService.Execution.cs` | 498 | TruncateOutput, ReadOnlyTools, ClassifyToolCalls, ToolExecutionState, ParallelState, ExecuteToolsInParallelAsync |

- **메인 파일**: 1,823줄 → 1,334줄 (**26.8% 감소**)
- **빌드**: 경고 0, 오류 0

---

## Phase 42 — ChatWindow.ResponseHandling·LlmService 파셜 분할 (v2.3) ✅ 완료

> **목표**: ChatWindow.ResponseHandling (1,494줄)·LlmService (1,010줄) 추가 분할.

### ChatWindow.ResponseHandling 분할

| 파일 | 줄 수 | 내용 |
|------|-------|------|
| `ChatWindow.ResponseHandling.cs` | 741 | 응답 재생성, 스트리밍 완료 마크다운, 중지, 대화 분기, 팔레트 |
| `ChatWindow.MessageActions.cs` | 277 | 버튼 이벤트, 메시지 내 검색(Ctrl+F), 에러 복구 |
| `ChatWindow.StatusAndUI.cs` | 498 | 우클릭 메뉴, 팁, AX.md, 무지개 글로우, 토스트, 하단바, 헬퍼 |

- **원본 대비**: 1,494줄 → 741줄 (**50.3% 감소**)

### LlmService 분할

| 파일 | 줄 수 | 내용 |
|------|-------|------|
| `LlmService.cs` (메인) | 263 | 필드, 생성자, 라우팅, 시스템 프롬프트, 비스트리밍 |
| `LlmService.Streaming.cs` | 516 | StreamAsync, TestConnectionAsync, 백엔드별 구현 |
| `LlmService.Helpers.cs` | 252 | 메시지 빌드, HTTP 재시도, 토큰 파싱, Dispose |

- **메인 파일**: 1,010줄 → 263줄 (**74.0% 감소**)
- **빌드**: 경고 0, 오류 0

---

## Phase 43 — 4개 대형 파일 파셜 분할 (v2.3) ✅ 완료

> **목표**: SettingsWindow.AgentConfig·ChatWindow.Presets·PreviewAndFiles·WorkflowAnalyzerWindow 동시 분할.

| 원본 파일 | 원본 | 메인 | 신규 파일 | 신규 줄 수 |
|----------|------|------|----------|----------|
| SettingsWindow.AgentConfig.cs | 1,202 | 608 | AgentHooks.cs | 605 |
| ChatWindow.Presets.cs | 1,280 | 315 | CustomPresets.cs | 978 |
| ChatWindow.PreviewAndFiles.cs | 1,105 | 709 | FileBrowser.cs | 408 |
| WorkflowAnalyzerWindow.xaml.cs | 929 | 274 | Charts.cs | 667 |

- **총 신규 파일**: 4개
- **빌드**: 경고 0, 오류 0

---

## Phase 44 — LauncherViewModel·SettingsWindow.Tools·MarkdownRenderer 파셜 분할 (v2.3) ✅ 완료

> **목표**: LauncherViewModel(805줄)·SettingsWindow.Tools(875줄)·MarkdownRenderer(825줄) 분할.

| 원본 파일 | 원본 | 메인 | 신규 파일 | 신규 줄 수 |
|----------|------|------|----------|----------|
| LauncherViewModel.cs | 805 | 402 | FileAction.cs | 154 |
| LauncherViewModel.cs | — | — | Commands.cs | 273 |
| SettingsWindow.Tools.cs | 875 | 605 | ToolCards.cs | 283 |
| MarkdownRenderer.cs | 825 | 621 | Highlighting.cs | 215 |

- **LauncherViewModel 메인**: 805줄 → 402줄 (50.1% 감소)
- **총 신규 파일**: 4개
- **빌드**: 경고 0, 오류 0

---

## Phase 45 — AppSettings.cs 클래스 파일 분리 (v2.3) ✅ 완료

> **목표**: 31개 클래스가 혼재된 AppSettings.cs (1,320줄)를 3개 파일로 분리.

| 파일 | 줄 수 | 내용 |
|------|-------|------|
| `AppSettings.cs` | 564 | AppSettings·LauncherSettings·CustomThemeColors 등 17개 클래스 |
| `AppSettings.LlmSettings.cs` | 481 | LlmSettings(408줄)·CodeSettings |
| `AppSettings.AgentConfig.cs` | 284 | 권한·훅·이벤트·모델·프리셋 등 12개 설정 클래스 |

- **메인 파일**: 1,320줄 → 564줄 (**57.3% 감소**)
- **빌드**: 경고 0, 오류 0

---

## Phase 46 — AgentLoopService·ChatWindow·SettingsViewModel·TemplateService·PlanViewerWindow 분리 (v2.3) ✅ 완료

> **목표**: 6개 대형 파일을 16개 파셜 파일로 분리.

| 원본 파일 | 원본 줄 | 신규 파일 | 내용 |
|-----------|---------|-----------|------|
| `AgentLoopService.cs` | 1,334 → 846 | `AgentLoopService.HtmlReport.cs` (151) | HTML 자동저장, 텍스트→HTML 변환 |
| | | `AgentLoopService.Verification.cs` (349) | 도구 실행 후 검증, 문서 생성 감지 |
| `ChatWindow.TaskDecomposition.cs` | 1,170 → 307 | `ChatWindow.PlanViewer.cs` (474) | Plan 카드, 결정 버튼, 플랜 뷰어 제어 |
| | | `ChatWindow.EventBanner.cs` (411) | 에이전트 이벤트 배너, 파일 퀵액션 |
| `SettingsViewModel.Properties.cs` | 837 → 427 | `SettingsViewModel.LlmProperties.cs` (417) | LLM/에이전트 설정 바인딩 프로퍼티 |
| `TemplateService.cs` | 734 → 179 | `TemplateService.Css.cs` (559) | 11개 CSS 스타일 상수 |
| `PlanViewerWindow.cs` | 931 → 324 | `PlanViewerWindow.StepRenderer.cs` (616) | 단계 목록 렌더링, 드래그, 편집 |

- **빌드**: 경고 0, 오류 0

---

## Phase 47 — ChatWindow 추가 분리 (v2.3) ✅ 완료

> **목표**: ChatWindow 관련 대형 파일 추가 분리.

| 원본 파일 | 원본 줄 | 신규 파일 | 내용 |
|-----------|---------|-----------|------|
| `ChatWindow.ResponseHandling.cs` | 741 → 269 | `ChatWindow.StreamingUI.cs` (303) | 스트리밍 컨테이너, 파싱, 토큰 |
| | | `ChatWindow.ConversationExport.cs` (188) | 대화 분기, 커맨드 팔레트, 내보내기 |
| `ChatWindow.PreviewAndFiles.cs` | 709 → ~340 | `ChatWindow.PreviewPopup.cs` (~230) | 프리뷰 탭 컨텍스트 메뉴, 팝업 |
| `HelpDetailWindow.xaml.cs` | 673 → 254 | `HelpDetailWindow.Shortcuts.cs` (168) | 단축키 항목 목록 빌드 |
| | | `HelpDetailWindow.Navigation.cs` (266) | 테마, 상단 메뉴, 카테고리 바, 페이지 이동 |
| `SkillService.cs` | 661 → 386 | `SkillService.Import.cs` (203) | 스킬 내보내기/가져오기/도구명 매핑 |
| `SkillDefinition.cs` | (신규) | `SkillDefinition.cs` (81) | SkillDefinition 클래스 독립 파일 |

- **빌드**: 경고 0, 오류 0

---

## Phase 48 — ChatWindow·WorkflowAnalyzer·SkillGallery 분리 (v2.3) ✅ 완료

> **목표**: 탭 전환, 드롭 액션, 워크플로우 타임라인, 스킬 상세 분리.

| 원본 파일 | 원본 줄 | 신규 파일 | 내용 |
|-----------|---------|-----------|------|
| `ChatWindow.Controls.cs` | 595 → 372 | `ChatWindow.TabSwitching.cs` (232) | 탭 전환, Plan 모드 UI, 대화 복원 |
| `ChatWindow.SlashCommands.cs` | 579 → 406 | `ChatWindow.DropActions.cs` (160) | 드래그&드롭 액션 팝업, 코드/데이터 확장자 |
| `WorkflowAnalyzerWindow.Charts.cs` | 667 → 397 | `WorkflowAnalyzerWindow.Timeline.cs` (281) | 타임라인 노드, 배지, 상세, 요약 카드 |
| `SkillGalleryWindow.xaml.cs` | 631 → ~430 | `SkillGalleryWindow.SkillDetail.cs` (197) | 스킬 상세 팝업 |

- **빌드**: 경고 0, 오류 0

---

## Phase 49 — ChatWindow.Sending·MarkdownRenderer·MessageRendering 분리 (v2.3) ✅ 완료

> **목표**: 전송 로직, 마크다운 렌더러, 메시지 렌더링 분리.

| 원본 파일 | 원본 줄 | 신규 파일 | 내용 |
|-----------|---------|-----------|------|
| `ChatWindow.Sending.cs` | 720 → ~300 | `ChatWindow.MessageEdit.cs` (~180) | 편집 모드 진입, SubmitEditAsync |
| | | `ChatWindow.StreamingTimers.cs` (~58) | 커서/경과시간/타이핑 타이머 |
| `MarkdownRenderer.cs` | 621 → 405 | `MarkdownRenderer.CodeBlock.cs` (218) | 코드 블록 UI, 파일저장, 전체화면 |
| `ChatWindow.MessageRendering.cs` | 522 → 220 | `ChatWindow.Animations.cs` (172) | 애니메이션 헬퍼, 체크 아이콘 |
| | | `ChatWindow.FeedbackButtons.cs` (121) | 액션 버튼, 좋아요/싫어요 피드백 |

- **빌드**: 경고 0, 오류 0

---

## Phase 50 — PlanViewerWindow·SettingsWindow 추가 분리 (v2.3) ✅ 완료

> **목표**: 버튼 빌더, AI 토글, MCP 고급 설정 분리.

| 원본 파일 | 원본 줄 | 신규 파일 | 내용 |
|-----------|---------|-----------|------|
| `PlanViewerWindow.StepRenderer.cs` | 616 → 425 | `PlanViewerWindow.EditButtons.cs` (197) | 승인/실행/닫기 버튼, 편집 입력 |
| `SettingsWindow.AgentConfig.cs` | 608 → 303 | `SettingsWindow.AiToggle.cs` (316) | AI 활성화·네트워크 모드 토글 |
| `SettingsWindow.AgentHooks.cs` | 605 → 334 | `SettingsWindow.McpAdvanced.cs` (271) | MCP 서버 관리, 폴백 모델, 고급 설정 |

- **빌드**: 경고 0, 오류 0

---

## Phase 51 — 9개 대형 파일 → 19개 파일 분리 (v2.3) ✅ 완료

> **목표**: 핸들러·서비스·모델 대규모 분리.

| 원본 파일 | 원본 줄 | 신규 파일 | 내용 |
|-----------|---------|-----------|------|
| `SettingsWindow.Tools.cs` | 605 → 238 | `SettingsWindow.SkillListPanel.cs` (295) | 스킬 목록 섹션, 스킬 그룹 카드 |
| `LauncherWindow.Keyboard.cs` | 593 → 454 | `LauncherWindow.ShortcutHelp.cs` (139) | 단축키 도움말, 토스트, 특수 액션 |
| `CalculatorHandler.cs` | 566 → ~240 | `UnitConverter.cs` (152) | 단위 변환 클래스 독립 파일 |
| | | `MathEvaluator.cs` (183) | 수식 파서 클래스 독립 파일 |
| `EmojiHandler.cs` | 553 → 70 | `EmojiHandler.Data.cs` (490) | 이모지 데이터베이스 배열 |
| `DocumentPlannerTool.cs` | 598 → 324 | `DocumentPlannerTool.Generators.cs` (274) | HTML/DOCX/Markdown 생성, 섹션 계획 |
| `DocumentReaderTool.cs` | 571 → 338 | `DocumentReaderTool.Formats.cs` (233) | BibTeX/RIS/DOCX/XLSX/Text/Helpers |
| `CodeIndexService.cs` | 588 → 285 | `CodeIndexService.Search.cs` (199) | 검색, TF-IDF, 토큰화, Dispose |
| `ClipboardHistoryService.cs` | 575 → 458 | `ClipboardHistoryService.ImageCache.cs` (120) | 이미지 캐시 유틸리티 |
| `IndexService.cs` | 568 → 412 | `IndexService.Helpers.cs` (163) | 경로 탐색 헬퍼, 검색 캐시 |

- **빌드**: 경고 0, 오류 0

---

## Phase 52 — 7개 파일 추가 분리 (v2.3) ✅ 완료

> **목표**: 스트리밍, 스킬, 핸들러, 모델, 뷰 분리 마무리.

| 원본 파일 | 원본 줄 | 신규 파일 | 내용 |
|-----------|---------|-----------|------|
| `LlmService.Streaming.cs` | 516 → 256 | `LlmService.GeminiClaude.cs` (260) | Gemini·Claude 스트리밍 메서드 |
| `DocxSkill.cs` | 543 → 158 | `DocxSkill.Builders.cs` (290) | 11개 Word 문서 빌더 헬퍼 |
| `ChartSkill.cs` | 537 → 174 | `ChartSkill.Renderers.cs` (280) | 7개 차트 렌더러, 헬퍼, Dataset |
| `ScreenCaptureHandler.cs` | 637 → 241 | `ScreenCaptureHandler.Helpers.cs` (310) | 스크롤·영역 캡처, 이미지 처리 헬퍼 |
| `SystemInfoHandler.cs` | 509 → 352 | `SystemInfoHandler.Helpers.cs` (161) | 8개 시스템 정보 헬퍼, StarInfoHandler |
| `AppSettings.cs` | 564 → 309 | `AppSettings.Models.cs` (233) | 14개 설정 모델 클래스 |
| `SkillEditorWindow.xaml.cs` | 528 → 303 | `SkillEditorWindow.PreviewSave.cs` (226) | 미리보기·저장·편집 모드 로드 |
| `PreviewWindow.xaml.cs` | 505 → 317 | `PreviewWindow.Content.cs` (170) | 콘텐츠 로드, 타이틀바 핸들러 |

### 코드 품질 최종 결과 (Phase 46~52)

| 구분 | Phase 45 이전 | Phase 52 완료 후 |
|------|-------------|----------------|
| 500줄 이상 파일 | 40+ | **3개** (AgentLoopService 846줄·LauncherWindow.xaml 578줄·TemplateService.Css 559줄) |
| 허용 이유 | — | RunAsync 679줄 단일 메서드 / 345줄 애니메이션 메서드 / CSS 데이터 상수 |
| 신규 파일 수 | — | Phase 46~52 동안 **45개** 신규 partial 파일 생성 |

- **빌드**: 경고 0, 오류 0

---

---

## Phase 17-UI-A — AgentSettingsPanel 완전 통합 (v1.8.0) ✅ 완료

> **목표**: 채팅 창 우측 AgentSettingsPanel을 주 설정 엔트리로 승격.
> 기어 버튼 클릭 → 인라인 패널 직접 열기 (Shift+Click 불필요).
> ToolRegistry 의존 제거, 탭 전환 연동 완성.

### 변경 파일

| 파일 | 변경 내용 |
|------|----------|
| `ChatWindow.MoodMenu.cs` | `BtnSettings_Click`: Shift 조건 제거 → 직접 `ToggleSettingsPanel()` 호출; Ctrl+클릭만 SettingsWindow 열기 |
| `ChatWindow.MoodMenu.cs` | `ToggleSettingsPanel()`: `_toolRegistry` 패널에 전달; `SettingsChanged` 이벤트 연결 → `UpdateModelLabel()` + `UpdateAnalyzerButtonVisibility()` 반영 |
| `ChatWindow.MoodMenu.cs` | `OnSettingsPanelChanged()` 신규: 패널 설정 변경 시 ChatWindow UI 즉시 갱신 |
| `ChatWindow.TabSwitching.cs` | `UpdateTabUI()`: 설정 패널 열림 상태이면 `UpdateActiveTab()` 자동 호출 |
| `ChatWindow.xaml` | 설정 버튼 ToolTip: "설정 패널 (Ctrl+클릭: 전역 설정)" |
| `AgentSettingsPanel.xaml.cs` | `LoadFromSettings()`: `ToolRegistry? toolRegistry` 매개변수 추가 |
| `AgentSettingsPanel.xaml.cs` | `BuildToolToggles()`: 외부 레지스트리 우선, 없으면 `ToolRegistry.CreateDefault()` 폴백 |

### 개선 효과

- ⚙ 버튼 한 번 클릭으로 에이전트 설정 패널 바로 접근
- 탭 전환(Chat/Cowork/Code) 시 패널 탭별 설정(검증 강제, LSP 등) 자동 반영
- 설정 변경 즉시 ChatWindow 모델 라벨·분석기 버튼 상태 갱신
- 도구 토글 패널: 실제 ChatWindow ToolRegistry 참조로 정확한 목록 표시
- **빌드**: 경고 0, 오류 0

---

## Phase 17-UI-B — 헤더 바 모델·권한 칩 추가 (v1.8.0) ✅ 완료

> **목표**: 서브 헤더 바(Row 1)에 현재 모델과 권한 모드를 항상 노출.
> 클릭 시 각각 모델 선택기·권한 팝업을 인라인으로 열어 UX 일관성 강화.

### 변경 파일

| 파일 | 변경 내용 |
|------|----------|
| `ChatWindow.xaml` | 서브 바 우측에 `ModelHeaderChip` Button 추가: 브레인 아이콘 + `ModelHeaderLabel` TextBlock |
| `ChatWindow.xaml` | 서브 바 우측에 `PermissionHeaderChip` Button 추가: 잠금 아이콘(#4FC3F7) + `PermissionHeaderLabel` TextBlock |
| `ChatWindow.ModelSelector.cs` | `UpdateModelLabel()`: `ModelHeaderLabel` 동기 갱신 코드 추가 |
| `ChatWindow.PermissionMenu.cs` | `UpdatePermissionUI()`: `PermissionHeaderLabel` 동기 갱신 코드 추가 |
| `ChatWindow.PermissionMenu.cs` | `PermissionHeaderChip_Click()` 신규: 팝업 PlacementTarget을 헤더 칩으로 교체 후 기존 권한 팝업 호출 |
| `ChatWindow.xaml.cs` | `Loaded` 핸들러: `UpdatePermissionUI()` 초기 호출 추가 |

### 개선 효과

- 서브 헤더 바에서 현재 모델(예: "Claude · Sonnet 4.6")과 권한 모드(예: "Ask") 상시 확인 가능
- `ModelHeaderChip` 클릭 → 기존 모델 선택 팝업 즉시 열림
- `PermissionHeaderChip` 클릭 → 권한 팝업이 헤더 칩 기준 하단으로 올바르게 열림
- 탭 전환·설정 변경 시 양쪽 칩(폴더 바 + 헤더 바) 동시 갱신
- **빌드**: 경고 0, 오류 0

---

## Phase 17-UI-C — AgentSessionHeaderBar UserControl 통합 (v1.8.0) ✅ 완료

> **목표**: 기존 Row 1 서브 바의 모델·Plan·권한 칩을 `AgentSessionHeaderBar` UserControl로 교체.
> 투명 배경으로 기존 레이아웃에 임베드. 이벤트 기반 위임으로 ChatWindow 기존 로직 재사용.

### 변경 파일

| 파일 | 변경 내용 |
|------|----------|
| `AgentSessionHeaderBar.xaml` | 외부 Border를 `Background="Transparent" BorderThickness="0"`으로 수정. 높이 42→38px. `x:Name="PlanIcon"`, `x:Name="PermIcon"` 추가. |
| `AgentSessionHeaderBar.xaml.cs` | `SetPlanMode()`: `PlanIcon` x:Name 직접 참조. `ChipPermission_Click`: 이벤트 발행만 (팝업은 ChatWindow 처리). `SetPermissionMode()`: PermIcon 색상 업데이트. |
| `ChatWindow.xaml` | Row 1 우측에서 `ModelHeaderChip`, `BtnPlanMode`, `PermissionHeaderChip` 제거 → `<ctrl:AgentSessionHeaderBar x:Name="SessionHeaderBar"/>` 추가. |
| `ChatWindow.SessionHeaderBar.cs` | 신규: `InitSessionHeaderBar()` — ModelChipClicked·PlanModeChanged·PermissionModeChanged·SettingsRequested 이벤트 구독. |
| `ChatWindow.ModelSelector.cs` | `UpdateModelLabel()`: `SessionHeaderBar?.SetModel()` 동기화. |
| `ChatWindow.PermissionMenu.cs` | `UpdatePermissionUI()`: `SessionHeaderBar?.SetPermissionMode()` 동기화. |
| `ChatWindow.TabSwitching.cs` | `UpdateTabUI()`: `SessionHeaderBar?.SetTabLabel()` 동기화. `UpdatePlanModeUI()`: SessionHeaderBar 전용으로 단순화. |
| `ChatWindow.xaml.cs` | Loaded: `InitSessionHeaderBar()` 호출 추가. |

- **빌드**: 경고 0, 오류 0

---

## Phase 17-UI-D — AgentSidebarView UserControl 통합 (v1.8.0) ✅ 완료

> **목표**: 기존 `SidebarPanel` Border(인라인 XAML 110줄)를 `AgentSidebarView` UserControl로 교체.
> 34개 기존 ChatWindow 파셜 파일이 사이드바 내부 요소를 무수정으로 참조할 수 있도록 프록시 패턴 적용.

### 변경 파일

| 파일 | 변경 내용 |
|------|----------|
| `AgentSidebarView.xaml` | 7행 구조로 재작성: 헤더(로고+새대화) / 탭세그먼트 / 검색 / 카테고리드롭다운 / 대화목록 / 삭제 / 사용자계정. |
| `AgentSidebarView.xaml.cs` | 완전 재작성: `internal` 프록시 프로퍼티 8개 + `SidebarTabChanged`/`NewChatRequested`/`DeleteAllRequested`/`CategoryDropClicked`/`SearchTextChanged` 이벤트. |
| `ChatWindow.xaml` | `<Border x:Name="SidebarPanel">` 인라인 110줄 → `<ctrl:AgentSidebarView x:Name="Sidebar"/>` 1줄 대체. |
| `ChatWindow.SidebarCompat.cs` | 신규: 8개 계산 프로퍼티(`ConversationPanel`, `SearchBox`, `CategoryIcon`, `CategoryLabel`, `BtnCategoryDrop`, `UserInitialSidebar`, `UserNameText`, `UserPcText`) + `InitSidebarEvents()`. |
| `ChatWindow.PermissionMenu.cs` | `BtnToggleSidebar_Click()`: `SidebarPanel.Visibility` → `Sidebar.Visibility`. `PermissionHeaderChip_Click()`: `PermissionHeaderChip` → `SessionHeaderBar`. |
| `ChatWindow.TabSwitching.cs` | 탭 핸들러 3개: `Sidebar?.SetActiveTab()` 동기화 추가. `UpdatePlanModeUI()`: 죽은 코드 제거. |
| `ChatWindow.xaml.cs` | Loaded: `InitSidebarEvents()` 호출 추가. |

- **빌드**: 경고 0, 오류 0

---

## Phase 17-UI-E — AgentInputArea 툴바 통합 (v1.8.0) ✅ 완료

> **목표**: `AgentInputArea` UserControl에 `IsToolbarOnly` 모드 추가.
> 기존 InputBox 위에 @ / 스킬 / 첨부 툴바 행을 추가하여 Claude.ai 스타일 입력 경험 제공.

### 변경 파일

| 파일 | 변경 내용 |
|------|----------|
| `AgentInputArea.xaml` | `x:Name="OuterBorder"`, `x:Name="InputTextBoxRow"`, `x:Name="ChipsSendRow"` 추가. |
| `AgentInputArea.xaml.cs` | `IsToolbarOnly` 의존성 프로퍼티 추가. `ApplyToolbarOnlyMode()`: TextBox/칩 행 Collapsed, 외부 Border 투명화. |
| `ChatWindow.xaml` | InputBorder Grid에 Row 0 추가(`ctrl:AgentInputArea x:Name="InputToolbar" IsToolbarOnly="True"`). 기존 Row 0→1, 1→2, 2→3 시프트. |
| `ChatWindow.InputToolbar.cs` | 신규: `InitInputToolbar()` — MentionRequested(@삽입), SkillRequested(/삽입), AttachRequested(BtnAttach_Click 위임). |
| `ChatWindow.xaml.cs` | Loaded: `InitInputToolbar()` 호출 추가. |

- **빌드**: 경고 0, 오류 0

---

## Phase 17-A — Reflexion 강화 (v1.8.0) ✅ 완료

> **목표**: 성공·실패 모두 구조화된 자기평가 저장 → 동일 작업 유형 재실행 시 자동 참고

### 변경 파일

| 파일 | 변경 내용 |
|------|----------|
| `ReflexionService.cs` | `ReflexionEvaluatorService` 신규 추가: LLM 기반 자기평가(점수·강점·약점·교훈 추출), 규칙 기반 폴백 엔트리. `ReflexionRepository.BuildContextPromptAsync()` maxEntries 파라미터 추가. |
| `AgentLoopService.Reflexion.cs` (신규, 82줄) | `InjectReflexionContextAsync()` — 세션 시작 시 과거 교훈을 시스템 메시지에 주입. `FireAndForgetReflexionEval()` — 세션 완료 후 비동기 자기평가 저장. |
| `AgentLoopService.cs` | `RunAsync()` 루프 시작 전: `InjectReflexionContextAsync()` 호출. `finally` 블록: `FireAndForgetReflexionEval()` 호출. |
| `AgentSettingsPanel.xaml` | "자기성찰 메모리" 섹션 추가: 활성화 토글, 성공 시만 평가 토글, 최대 참고 교훈 수 슬라이더. |
| `AgentSettingsPanel.xaml.cs` | `LoadFromSettings()`: Reflexion 설정 초기화. `ChkReflexion_Changed()`, `ChkReflexionSuccessOnly_Changed()`, `SliderReflexionMaxEntries_ValueChanged()` 핸들러 추가. |

### 구현 세부사항

- **ReflexionEvaluatorService**: `$$"""..."""` raw string으로 평가 프롬프트 생성, JSON 블록 추출 후 역직렬화
- **작업 유형 분류**: `TaskTypeClassifier.Classify()` — 8개 카테고리 (code_generation, file_refactor, document, analysis, search, test, git, debug, general)
- **저장 위치**: `%APPDATA%\AxCopilot\reflexion\<taskType>.jsonl` (JSONL 형식, 최신 N개 역순 조회)
- **비동기 처리**: fire-and-forget — 루프 취소(`CancellationToken`) 영향 없음
- **빌드**: 경고 0, 오류 0

---

## Phase 17-B — 구조화된 태스크 상태 + 이벤트 로그 통합 (v1.8.0) ✅ 완료

> **목표**: 대화 압축 시에도 유지되는 Working Memory + JSONL 이벤트 로그 완전 통합

### 변경 파일

| 파일 | 변경 내용 |
|------|----------|
| `AgentLoopService.TaskState.cs` (신규, 96줄) | `InitTaskStateAsync()` — 세션 시작 시 TaskState 초기화·현재 작업 기록. `TrackToolFile()` — 도구 성공 시 파일 경로 참조 목록 추가(fire-and-forget). `InjectTaskStateContext()` — 압축 전 Working Memory를 시스템 메시지에 in-place 주입. `UpdateTaskStateSummaryAsync()` — 압축 완료 후 컨텍스트 요약 갱신(fire-and-forget). |
| `AgentLoopService.cs` | `RunAsync()`: UserMessage 이벤트 기록 + `InitTaskStateAsync()` 호출. 압축 블록: `InjectTaskStateContext()` 호출 + CompactionCompleted/CompactionTriggered 이벤트 로그. LLM 응답 후: AssistantMessage 이벤트 기록. |
| `AgentLoopService.Execution.cs` | 도구 성공 시 `TrackToolFile(result.FilePath)` 호출 — 파일 경로 Working Memory 추적. |

### 구현 세부사항

- **TaskState 지연 초기화**: `_taskState ??= new TaskStateService()` — 사용 시 첫 생성, 세션 간 `InitializeAsync(sessionId)`로 상태 재로드
- **In-place 주입**: `InjectTaskStateContext()`가 `## 현재 작업 상태 (Working Memory)` 마커로 기존 섹션 탐지 후 교체 → 중복 방지
- **이벤트 커버리지**: SessionStart/End(기존), UserMessage, AssistantMessage, ToolRequest/Result(기존), CompactionTriggered, CompactionCompleted 모두 JSONL 기록
- **설정 연동**: `LlmSettings.EnableTaskState` + `LlmSettings.EventLog.Enabled` 체크 후 동작
- **저장 위치**: `%APPDATA%\AxCopilot\sessions\{sessionId}\task_state.json` + `events.jsonl`
- **빌드**: 경고 0, 오류 0

---

---

## Phase 17-C — 훅 시스템 고도화 (v1.8.0) ✅ 완료

> **목표**: ExtendedHookRunner를 에이전트 라이프사이클 전 구간에 연결 + Prompt 모드 구현

### 변경 파일

| 파일 | 변경 내용 |
|------|----------|
| `AppSettings.AgentConfig.cs` | `ExtendedHooksConfig`에 4개 이벤트 추가: `preToolUse`, `postToolUse`, `postToolUseFailure`, `agentStop` |
| `AgentLoopService.ExtendedHooks.cs` (신규, 150줄) | `RunExtendedEventAsync()` — 이벤트 훅 실행·결과 적용. `GetExtendedHooks()` — 설정에서 런타임 엔트리 조회. `ConvertToRuntimeEntries()` — ExtendedHookEntryConfig → ExtendedHookEntry 변환. `ApplyExtendedHookResult()` — additionalContext 시스템 메시지 in-place 주입. |
| `ExtendedHookRunner.cs` | `RunEventAsync()` + `ExecuteSingleAsync()`에 `LlmService? llm` 파라미터 추가. `RunPromptHookAsync()` 신규 구현: `{{tool_name}}` 등 변수 치환, LLM 호출, 차단 신호 감지. `using AxCopilot.Models` 추가. |
| `AgentLoopService.cs` | TaskState init 후: `SessionStart` 훅(fire-and-forget) + `UserPromptSubmit` 훅(차단 시 즉시 반환). 적극적 압축 직전: `PreCompact` 훅. 압축 완료 후: `PostCompact` 훅(fire-and-forget). `finally` 블록: `SessionEnd` + `AgentStop` 훅(fire-and-forget). |
| `AgentLoopService.Execution.cs` | `RunToolHooksAsync()` 개선: 레거시 AgentHookRunner 유지 + ExtendedHookRunner `PreToolUse`/`PostToolUse`/`PostToolUseFailure` 이벤트 추가 실행. |

### 구현 세부사항

- **이벤트 커버리지**: SessionStart, SessionEnd, AgentStop, UserPromptSubmit, PreToolUse, PostToolUse, PostToolUseFailure, PreCompact, PostCompact, FileChanged, PermissionRequest — 11종 이벤트 완전 연결
- **실행 모드**: Command(bat/ps1), Http(POST webhook), Prompt(LLM 평가) 구현. Agent 모드는 Phase 18-A 예정.
- **Prompt 모드**: `{{tool_name}}`, `{{tool_input}}`, `{{tool_output}}`, `{{user_message}}`, `{{event}}`, `{{session_id}}` 변수 치환. 응답에 block/deny/차단 포함 시 Block=true.
- **차단 처리**: UserPromptSubmit 훅 Block=true 시 즉시 `"⚠ 요청이 훅 정책에 의해 차단되었습니다."` 반환
- **HookFired 이벤트 로그**: 훅 실행 시 JSONL에 eventKind, hookCount, blocked 기록
- **하위 호환**: 레거시 AgentHooks(command 스크립트) 병행 유지
- **빌드**: 경고 0, 오류 0

---

---

## Phase 17-D — 스킬 시스템 고도화 (v1.8.0) ✅ 완료

> **목표**: paths: glob 패턴 기반 스킬 자동 주입 + context:fork 서브에이전트 격리 실행

### 변경 파일

| 파일 | 변경 내용 |
|------|----------|
| `AgentLoopService.Skills.cs` (신규, 95줄) | `InjectPathBasedSkills()` — 파일 경로 매칭 스킬 시스템 메시지 in-place 주입, SkillActivated 이벤트 로그. `RunSkillInForkAsync()` — context:fork 스킬 격리 LLM 실행, SkillCompleted 이벤트 로그. |
| `SkillManagerTool.cs` | `SetForkRunner()` 콜백 필드 추가. `exec` 액션 + `arguments` 파라미터 추가. `ExecSkillAsync()`: PrepareSkillBodyAsync 준비 → IsForkContext 시 fork runner 호출 → 일반 스킬은 시스템 프롬프트 반환. |
| `AgentLoopService.cs` | 생성자: `SkillManagerTool`에 `RunSkillInForkAsync` fork runner 주입. |
| `AgentLoopService.Execution.cs` | 도구 성공 후 `InjectPathBasedSkills(result.FilePath, messages)` 호출. |

### 구현 세부사항

- **paths: 자동 주입**: 파일 도구(`file_read`, `file_write` 등) 성공 후 `result.FilePath`로 GlobMatcher 매칭 → 해당 스킬의 시스템 프롬프트를 메시지에 자동 주입. `## 현재 파일에 자동 적용된 스킬` 마커로 in-place 교체.
- **context:fork**: `SkillManagerTool.exec` 호출 시 `skill.IsForkContext == true`이면 격리된 LLM 컨텍스트(도구 없음)에서 실행. 결과를 `[Fork 스킬 결과]` 형식으로 반환.
- **이벤트 로그**: `SkillActivated`(paths: 매칭 시), `SkillCompleted`(fork 실행 완료 시) JSONL 기록
- **DI 패턴**: `DelegateAgentTool.SetSubAgentRunner` 패턴 동일 적용 — AgentLoopService 생성자에서 콜백 주입
- **빌드**: 경고 0, 오류 0

---

---

## Phase 17-E — 메모리/컨텍스트 고도화 (v1.8.0) ✅ 완료

> **목표**: @include 지시어·4-layer 계층 메모리·경로 기반 규칙 주입·컨텍스트 압축 완전 구현

### 변경 파일

| 파일 | 변경 내용 |
|------|----------|
| `AxMdIncludeResolver.cs` (신규) | `ResolveAsync()` — AX.md·rules/*.md의 `@파일경로` 지시어 재귀 해석. 최대 5단계 깊이, 순환 참조 감지. 40K자 초과 시 경고. |
| `PathScopedRuleInjector.cs` (신규) | `.ax/rules/*.md` `paths:` 프론트매터 파싱 → GlobMatcher로 현재 파일 경로 매칭 → 해당 규칙만 컨텍스트에 주입. |
| `HierarchicalMemoryService.cs` (신규) | 4-layer(Managed/User/Project/Local) AX.md 파일 수집·병합. 우선순위 낮은 레이어부터 쌓아 높은 레이어가 재정의. |
| `AgentLoopService.Memory.cs` (신규, 105줄) | `InjectHierarchicalMemoryAsync()` — 세션 시작 시 계층 메모리를 시스템 메시지에 in-place 주입. `InjectPathScopedRulesAsync()` — 파일 도구 성공 후 경로 규칙 주입. |
| `AgentLoopService.cs` | 세션 시작: `InjectHierarchicalMemoryAsync()` 호출. 도구 성공 후: `InjectPathScopedRulesAsync(filePath)` 호출. |

### 구현 세부사항

- **4-layer 우선순위**: Managed(%APPDATA%\AxCopilot\system.md) → User(%USERPROFILE%\.axcopilot\AX.md) → Project(cwd/AX.md + .ax/rules/*.md) → Local(cwd/AX.local.md)
- **@include 재귀**: `@상대경로` 지시어 탐지 → 파일 읽기 → 재귀 치환 (최대 5단계, 방문 집합으로 순환 감지)
- **경로 규칙 in-place**: `## 현재 파일에 적용된 경로 규칙` 마커로 기존 섹션 탐지 후 교체 → 중복 방지
- **자동 압축**: ContextCondenser.cs — 컨텍스트 토큰 초과 시 오래된 메시지 요약 후 압축. AutoCompactMonitor.cs — 주기적 자동 압축 감시.
- **설정 연동**: `EnableMemorySystem` (기본 true)
- **빌드**: 경고 0, 오류 0

---

---

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

> **목표**: acceptEdits 권한 모드·패턴 기반 Allow/Deny 규칙·복합 Bash 명령 분해

### 변경 파일

| 파일 | 변경 내용 |
|------|----------|
| `AgentLoopService.Permissions.cs` (신규, 97줄) | `GetPermissionService()` — 모드 변경 시 PermissionDecisionService 재생성 및 캐시. `EvaluateToolPermission()` — Chain: Deny규칙→Allow규칙→AcceptEdits/Plan/Bypass→기본(Ask). |
| `PermissionSystem.cs` | `PermissionDecisionService` 빌드 로직. AcceptEditsHandler: 파일 편집 도구 자동 승인, bash/process 확인 유지. PatternRuleHandler: glob 패턴 기반 Allow/Deny. |
| `CompoundCommandParser.cs` (신규) | `&&`, `\|\|`, `;`, `\|` 연산자로 bash 명령 분해. `SplitCommands()` → 모든 서브 명령 개별 검사. |
| `AppSettings.PermissionsConfig.cs` | `AllowRules`/`DenyRules` 리스트 추가. 패턴 예: `process(git *)` 허용, `process(rm -rf *)` 차단. |

### 구현 세부사항

- **acceptEdits 모드**: file_write/file_edit/file_manage 도구 → 자동 승인. bash/process → 여전히 Ask 확인.
- **패턴 매칭**: `tool(pattern)` 형식. `*` 와일드카드 지원. 대소문자 무시.
- **Chain of Responsibility**: Deny규칙(즉시차단) → Allow규칙(CheckRequired 스킵) → 모드 기본값(AcceptEdits/Plan/Bypass/Ask)
- **복합 명령**: `git add . && git commit -m "msg"` → `git add .`·`git commit -m "msg"` 각각 검사
- **하드코딩 차단**: `rd /s`, `format`, `reg delete`, `del /s /q` 등 위험 명령 항상 차단
- **빌드**: 경고 0, 오류 0

---

---

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

> **목표**: 쓰기 도구 실행 전후 원본·변경 캡처 → Diff 패널 실시간 표시 + @멘션 자동 컨텍스트 수집

### 변경 파일

| 파일 | 변경 내용 |
|------|----------|
| `AgentLoopService.DiffTracker.cs` (신규, 99줄) | `CaptureOriginalFileContent()` — 쓰기 도구 실행 전 원본 캡처. `TrackFileModificationForDiff()` — 성공 후 변경 내용을 MultiFileDiffViewModel에 기록. `ResetDiffTracker()` — 세션 시작 시 초기화. |
| `AutoContextCollector.cs` (신규) | `@파일명` 멘션·절대 경로·따옴표 파일명·백틱 참조 Regex 패턴으로 사용자 메시지에서 파일 참조 자동 감지. `CollectContextAsync()` — 감지된 파일 내용 읽어 컨텍스트에 추가. |
| `AgentLoopService.Execution.cs` | 쓰기 도구 실행 전: `CaptureOriginalFileContent(filePath)`. 성공 후: `TrackFileModificationForDiff(filePath, result)`. |
| `AgentLoopService.cs` | 사용자 메시지 처리 전: `AutoContextCollector.CollectContextAsync(userMessage, messages)`. 세션 시작: `ResetDiffTracker()`. |

### 구현 세부사항

- **Diff 추적 도구**: file_write, file_edit, file_manage 성공 시만 Diff 기록 (읽기 전용 도구 제외)
- **MultiFileDiffViewModel**: ChatWindow Diff 패널에 바인딩. 변경된 파일 목록 + 선택 파일 Diff 표시.
- **자동 컨텍스트**: `@파일명.ext` → 파일 읽기 후 `## 자동 수집된 컨텍스트 [파일명]` 블록으로 시스템 메시지에 추가
- **도구 위험도 표시**: ToolRiskMapper 연계 — ToolCall 이벤트에 `[⚠ 높음]`/`[• 보통]` 태그 접미사
- **설정 연동**: `EnableDiffTracker` (기본 true)
- **빌드**: 경고 0, 오류 0

---

최종 업데이트: 2026-04-04 (Phase 22~52 + Phase 17-UI-A~E + Phase 17-A~G 구현 완료)

