Files
AX-Copilot/docs/NEXT_ROADMAP.md
lacvet 1313c65e5e [Phase 17-D] 스킬 시스템 고도화 — paths:glob 자동주입 + context:fork 격리 실행
AgentLoopService.Skills.cs (신규, 95줄):
- InjectPathBasedSkills(): 파일 도구 성공 후 filePath로 GlobMatcher 매칭
  → 매칭 스킬 시스템 프롬프트를 시스템 메시지에 in-place 주입
  → SkillActivated JSONL 이벤트 로그 기록
- RunSkillInForkAsync(): context:fork 스킬 격리 LLM 실행 (도구 없음)
  → SkillCompleted JSONL 이벤트 로그 기록

SkillManagerTool.cs:
- SetForkRunner(Func<SkillDefinition, string, CancellationToken, Task<string>>) 추가
- exec 액션 + arguments 파라미터 추가
- ExecSkillAsync(): PrepareSkillBodyAsync 인자 치환
  → IsForkContext=true: fork runner 호출 → [Fork 스킬 결과] 반환
  → 일반 스킬: 시스템 프롬프트 + 준비된 본문 반환

AgentLoopService.cs:
- 생성자: SkillManagerTool.SetForkRunner(RunSkillInForkAsync) 주입

AgentLoopService.Execution.cs:
- 도구 성공 직후 InjectPathBasedSkills(result.FilePath, messages) 호출

빌드: 경고 0, 오류 0

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-04 00:39:25 +09:00

200 KiB

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

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


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

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

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

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

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

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

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

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

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

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

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

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

애니메이션 스펙:

  • 사이드바 확장/축소: DoubleAnimation Duration=0:0:0.2 EasingFunction=CubicEase EaseInOut
    • Width: 240 ↔ 48 (GridLength 직접 애니메이션, GridLengthAnimation 헬퍼 클래스 사용)
  • 설정 패널 슬라이드: DoubleAnimation Duration=0:0:0.2 EasingFunction=CubicEase EaseOut
    • Width: 0 ↔ 300
  • 아이콘 회전 (사이드바 토글 화살표): RotateTransform 0 ↔ 180도

17-UI-3: AgentWindowViewModel

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

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

    // ── 자식 ViewModel ──
    public AgentSidebarViewModel Sidebar { get; }
    public AgentSessionHeaderViewModel SessionHeader { get; }
    public AgentInlineSettingsViewModel InlineSettings { get; }
    public AgentInputAreaViewModel InputArea { get; }
    public ObservableCollection<ChatMessageViewModel> Messages { get; }

    // ── 커맨드 ──
    public ICommand ToggleSidebarCommand { get; }        // IsSidebarExpanded 토글
    public ICommand ToggleSettingsPanelCommand { get; }  // IsSettingsPanelOpen 토글
    public ICommand NewSessionCommand { get; }
    public ICommand SendMessageCommand { get; }
    public ICommand InterruptCommand { get; }

    // ── 의존성 ──
    public AgentWindowViewModel(
        AgentLoopService agentLoop,
        SkillLoaderService skillLoader,
        AgentMemoryService memory,
        SettingsService settings,
        IAgentSessionRepository sessionRepo)
    { ... }

    // ── 기존 AgentChatWindow 통합 ──
    // AgentLoopService.MessageReceived → Messages 추가
    // AgentLoopService.ToolExecuting → SessionHeader.IsRunning = true
    // AgentLoopService.Completed → SessionHeader.IsRunning = false
    private void WireAgentEvents() { ... }
}

17-UI-4: AgentSessionHeaderViewModel

public class AgentSessionHeaderViewModel : ViewModelBase
{
    // ── 모델 선택 ──
    public ObservableCollection<string> AvailableModels { get; }
    private string _selectedModel;
    public string SelectedModel
    {
        get => _selectedModel;
        set { SetProperty(ref _selectedModel, value); ApplyModelChange(); }
    }

    // ── 모드 상태 ──
    public PlanMode PlanMode { get; set; }          // Off | Always | Auto
    public PermissionMode PermissionMode { get; set; } // Default | AcceptEdits | Plan | Bypass
    public AgentTab ActiveTab { get; set; }          // Chat | Cowork | Code
    public string ActivePreset { get; set; }

    // ── 실행 상태 ──
    public bool IsRunning { get; set; }
    public string StatusText { get; set; }           // "도구 실행 중...", "생각 중..."
    public int CurrentIteration { get; set; }
    public int MaxIterations { get; set; }

    // ── 도구 상태 아이콘 (우측 상단 아이콘 행) ──
    // MCP 연결됨, 훅 활성, 스킬 N개 로드됨 등을 아이콘으로 표시
    public bool IsMcpConnected { get; set; }
    public int ActiveHookCount { get; set; }
    public int LoadedSkillCount { get; set; }

    // ── 커맨드 ──
    public ICommand ChangePlanModeCommand { get; }   // 클릭 시 Off→Always→Auto 순환
    public ICommand ChangePermissionModeCommand { get; }
    public ICommand ChangeTabCommand { get; }        // Chat/Cowork/Code
    public ICommand ChangePresetCommand { get; }
    public ICommand ToggleSettingsCommand { get; }   // AgentWindowViewModel.ToggleSettingsPanelCommand 위임
    public ICommand InterruptCommand { get; }
    public ICommand ExportPdfCommand { get; }
}

XAML 바인딩 예시 (AgentSessionHeaderBar.xaml):

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

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

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

17-UI-5: AgentSidebarViewModel

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

    // ── 프리셋 목록 ──
    public ObservableCollection<PresetViewModel> Presets { get; }
    public PresetViewModel? ActivePreset { get; set; }

    // ── 확장 상태 ──
    // IsExpanded = true: 240px (아이콘+라벨), false: 48px (아이콘만)
    public bool IsExpanded { get; set; }

    // ── 커맨드 ──
    public ICommand NewSessionCommand { get; }
    public ICommand SelectSessionCommand { get; }    // param: SessionSummaryViewModel
    public ICommand DeleteSessionCommand { get; }
    public ICommand SelectPresetCommand { get; }
    public ICommand EditPresetCommand { get; }
    public ICommand ImportSkillCommand { get; }
}

17-UI-6: AgentInlineSettingsPanel (SettingsWindow 분리)

SettingsWindow에서 이전되는 항목:

[이전 대상 — AgentInlineSettingsPanel]
- LLM 서비스 선택 (Chat/Cowork/Code별)
- 모델 선택 (탭별)
- 플랜 모드 (Off/Always/Auto)
- 권한 모드 (Default/AcceptEdits/Plan)
- MaxAgentIterations (탭별)
- MaxRetryOnError
- EnablePostToolVerification (탭별)
- EnableCoworkVerification / EnableCodeVerification
- 컨텍스트 파일 포함 설정
- 시스템 프롬프트 미리보기/편집
- 활성 프리셋 편집

[SettingsWindow에 남는 항목]
- 일반: 테마, 폰트, AI 활성화 토글
- 런처: 단축키, 독 바, Everything 경로
- MCP 서버 등록 (전문 설정)
- 훅 규칙 편집기 (전문 설정)
- 권한 패턴 규칙 편집기 (전문 설정)
- LSP 설정
- 암호화/보안 설정
public class AgentInlineSettingsViewModel : ViewModelBase
{
    // ── 탭별 LLM 설정 (ActiveTab에 따라 바인딩 전환) ──
    public string LlmService { get; set; }
    public string ModelName { get; set; }
    public int MaxIterations { get; set; }
    public bool EnableVerification { get; set; }
    public PlanMode PlanMode { get; set; }
    public PermissionMode PermissionMode { get; set; }

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

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

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

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

17-UI-7: AgentInputArea

public class AgentInputAreaViewModel : ViewModelBase
{
    public string InputText { get; set; }
    public bool IsMultiline { get; set; }           // Shift+Enter로 전환
    public ObservableCollection<AttachedFileViewModel> AttachedFiles { get; }
    public bool IsAtMentionOpen { get; set; }        // @ 입력 시 팝업
    public bool IsSlashMenuOpen { get; set; }        // / 입력 시 스킬 메뉴
    public ObservableCollection<SkillSuggestionViewModel> SlashSuggestions { get; }

    // 커맨드
    public ICommand SendCommand { get; }
    public ICommand AttachFileCommand { get; }
    public ICommand ClearAttachmentsCommand { get; }
    public ICommand OpenSlashMenuCommand { get; }    // / 키 감지
    public ICommand InsertAtMentionCommand { get; }  // @ 키 감지
}

XAML (AgentInputArea.xaml) 핵심:

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

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

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

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

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

17-UI-8: 통합 포인트

기존 클래스 변경 내용
AgentChatWindow.xaml.cs AgentWindow로 교체 (리네임 + 재구성). App.xaml.csOpenAgentWindow() 호출부 교체
AgentChatViewModel.cs AgentWindowViewModel로 흡수·통합
SettingsWindow.xaml AX Agent 탭 내 에이전트 설정 섹션 제거, "채팅 화면에서 설정 가능" 안내 문구 추가
AgentLoopService.cs 이벤트 구독 포인트 변경 없음. ViewModel에서 이벤트 연결 방식만 변경
App.xaml.cs _agentWindow 필드를 AgentWindow 타입으로 변경

17-UI-9: 구현 순서

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

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

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

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

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

// 반성 메모리 저장소 인터페이스
public interface IReflexionRepository
{
    Task SaveAsync(ReflexionEntry entry);
    Task<IReadOnlyList<ReflexionEntry>> GetByTaskTypeAsync(string taskType, int limit = 5);
    Task<string> BuildContextPromptAsync(string taskType);
    // 저장 위치: %APPDATA%\AxCopilot\reflexion\<taskType>.jsonl
}

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

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

    // AgentLoopService.Completed 이벤트에서 호출
    public Task<ReflexionEntry> EvaluateAsync(
        AgentSession session,
        AgentResult result,
        CancellationToken ct);

    // 평가 프롬프트: 완성도 점수, 강점/약점, 교훈 3가지 추출
    private string BuildEvaluationPrompt(AgentSession session, AgentResult result) { ... }
}

// 작업 유형 분류기
public class TaskTypeClassifier
{
    // 사용자 입력 → 작업 유형 분류 (규칙 기반 + LLM 보조)
    public string Classify(string userMessage) { ... }
    // "파일 수정", "코드 생성", "문서 작성", "검색", "분석", "테스트" 등
    private static readonly Dictionary<string, string[]> _keywords = ...;
}

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

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

    // RunAsync() 시작 시: 반성 컨텍스트 주입
    private async Task<string> BuildSystemPromptAsync(string userMessage)
    {
        var taskType = _taskClassifier.Classify(userMessage);
        var reflexionContext = await _reflexionRepo.BuildContextPromptAsync(taskType);
        // 기존 시스템 프롬프트 끝에 reflexionContext 추가
    }

    // RunAsync() 완료 후: 자기평가 저장
    private async Task OnSessionCompletedAsync(AgentSession session, AgentResult result)
    {
        var entry = await _reflexion.EvaluateAsync(session, result, CancellationToken.None);
        await _reflexionRepo.SaveAsync(entry);
    }
}

17-A-3: 검증 대상 확대 (17-A2)

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

    // 검증 체크리스트 (사용자 편집 가능)
    // 저장: %APPDATA%\AxCopilot\verification_checklist.json
    public Task<VerificationChecklist> LoadChecklistAsync() { ... }
    public Task SaveChecklistAsync(VerificationChecklist checklist) { ... }
}

public record VerificationChecklist
{
    [JsonPropertyName("code_checks")]
    public string[] CodeChecks { get; init; } = new[]
    {
        "구문 오류 없음",
        "참조 무결성 확인",
        "null 참조 없음"
    };

    [JsonPropertyName("file_checks")]
    public string[] FileChecks { get; init; } = new[]
    {
        "파일이 실제로 수정됨",
        "인코딩 올바름 (UTF-8)"
    };
}

17-A-4: 설정 스키마 변경

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

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

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

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

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

17-B-1: TaskState (Working Memory)

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

    // 직렬화: %APPDATA%\AxCopilot\sessions\<sessionId>\task_state.json
    public Task SaveAsync(string baseDir) { ... }
    public static Task<TaskState> LoadAsync(string sessionId, string baseDir) { ... }
}

public record DecisionLogEntry
{
    public DateTime Timestamp { get; init; }
    public string Decision { get; init; }
    public string Reason { get; init; }
    public string[] Alternatives { get; init; }
}

// TaskState 관리 서비스
public class TaskStateService
{
    private readonly string _baseDir;
    private TaskState? _current;

    public TaskState Current => _current ?? throw new InvalidOperationException();

    public Task InitializeAsync(string sessionId) { ... }
    public Task UpdateCurrentTaskAsync(string taskDescription) { ... }
    public Task AddReferencedFileAsync(string filePath) { ... }
    public Task LogDecisionAsync(string decision, string reason, string[] alternatives) { ... }
    public Task UpdateContextSummaryAsync(string summary) { ... }

    // AgentLoopService.ContextCompacting 이벤트에서 호출
    // 압축 전 현재 TaskState를 시스템 프롬프트에 추가
    public string BuildCompactContextInjection() { ... }
}

17-B-2: Event-Sourced 이벤트 로그

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

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

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

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

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

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

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

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

// JSONL 기반 이벤트 로그 저장소
// 저장: %APPDATA%\AxCopilot\sessions\<sessionId>\events.jsonl
public class AgentEventLog
{
    private readonly string _filePath;
    private long _seq = 0;

    public Task AppendAsync(AgentEventType type, object payload, string? parentId = null) { ... }
    public IAsyncEnumerable<AgentEventRecord> ReadAllAsync() { ... }
    public IAsyncEnumerable<AgentEventRecord> ReadFromAsync(long fromSeq) { ... }
}

// AgentLoopService에서 이벤트 기록
public class AgentLoopService
{
    private AgentEventLog? _eventLog;

    // 기존 _events 컬렉션 대신 또는 병렬로 이벤트 로그 기록
    private async Task RecordEventAsync(AgentEventType type, object payload)
    {
        if (_eventLog != null)
            await _eventLog.AppendAsync(type, payload);
    }
}

17-B-3: 설정 스키마 변경

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

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

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

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

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

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

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

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

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

// 훅 실행기 인터페이스 (Strategy)
public interface IHookExecutor
{
    HookType Type { get; }
    Task<HookResult> ExecuteAsync(HookContext context, CancellationToken ct);
}

// 훅 컨텍스트 (이벤트 정보 + 도구 입력 등)
public record HookContext
{
    public HookEvent Event { get; init; }
    public string? ToolName { get; init; }          // PreToolUse, PostToolUse 시
    public string? ToolInput { get; init; }         // 도구 입력 JSON
    public string? ToolOutput { get; init; }        // PostToolUse 시 도구 출력
    public string? UserMessage { get; init; }       // UserPromptSubmit 시
    public string? ChangedFilePath { get; init; }   // FileChanged 시
    public string SessionId { get; init; }
    public Dictionary<string, string> EnvVars { get; init; } // $TOOL_NAME, $ARGUMENTS 등
}

// 훅 결과
public record HookResult
{
    public bool Block { get; init; } = false;       // true: 도구 실행 차단
    public string? BlockReason { get; init; }
    public string? AdditionalContext { get; init; } // 시스템 프롬프트에 추가할 텍스트
    public string? UpdatedInput { get; init; }      // 수정된 도구 입력 (updatedInput)
    public PermissionDecision? PermissionDecision { get; init; } // allow | deny
    public IReadOnlyList<string>? WatchPaths { get; init; }     // SessionStart 시 등록
    public string? StatusMessage { get; init; }     // 스피너 커스텀 메시지
}

public enum PermissionDecision { Allow, Deny, Ask }

// ── 구현체 ──

// 1. command 타입: 외부 프로세스 실행
public class CommandHookExecutor : IHookExecutor
{
    public HookType Type => HookType.Command;

    public async Task<HookResult> ExecuteAsync(HookContext context, CancellationToken ct)
    {
        // stdout → stdout 읽어서 HookResult 파싱
        // exit code 2 → Block = true
        // JSON 출력 → AdditionalContext, UpdatedInput 파싱
    }
}

// 2. http 타입: HTTP 웹훅
public class HttpHookExecutor : IHookExecutor
{
    public HookType Type => HookType.Http;
    // POST JSON 페이로드 → 응답 JSON 파싱 → HookResult
}

// 3. prompt 타입: LLM 검사 (소형 모델로 빠른 판단)
public class PromptHookExecutor : IHookExecutor
{
    public HookType Type => HookType.Prompt;
    private readonly ILlmClientFactory _llmFactory;

    public async Task<HookResult> ExecuteAsync(HookContext context, CancellationToken ct)
    {
        // 설정된 prompt 템플릿 + 컨텍스트 변수 치환
        // LLM 호출 (기본 claude-haiku-4-5, 타임아웃 15초)
        // 응답에서 "BLOCK"/"ALLOW" 및 이유 파싱
    }

    // 훅 설정 필드
    public record PromptHookConfig
    {
        public string Prompt { get; init; }
        public string Model { get; init; } = "claude-haiku-4-5";
        public int TimeoutSeconds { get; init; } = 15;
    }
}

// 4. agent 타입: 미니 에이전트 루프
public class AgentHookExecutor : IHookExecutor
{
    public HookType Type => HookType.Agent;
    private readonly AgentLoopService _agentLoop;

    public async Task<HookResult> ExecuteAsync(HookContext context, CancellationToken ct)
    {
        // 격리된 미니 에이전트 루프 실행
        // 파일 읽기·명령 실행·검증 가능
        // 최대 반복: 3회 (하드코딩 또는 설정)
        // 결과를 HookResult로 변환
    }
}

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

    public HookExecutorFactory(
        CommandHookExecutor cmd,
        HttpHookExecutor http,
        PromptHookExecutor prompt,
        AgentHookExecutor agent)
    {
        _executors = new Dictionary<HookType, IHookExecutor>
        {
            [HookType.Command] = cmd,
            [HookType.Http] = http,
            [HookType.Prompt] = prompt,
            [HookType.Agent] = agent,
        };
    }

    public IHookExecutor GetExecutor(HookType type)
        => _executors.TryGetValue(type, out var exec) ? exec
           : throw new NotSupportedException($"훅 타입 미지원: {type}");
}

17-C-3: 훅 속성 모델 확장

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

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

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

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

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

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

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

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

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

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

17-C-4: HookRunnerService 확장

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

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

    // 훅 실행 (모든 이벤트 공통)
    public async Task<HookRunResult> RunAsync(
        HookEvent hookEvent,
        HookContext context,
        CancellationToken ct)
    {
        var defs = _configRepo.GetHooksForEvent(hookEvent, context.ToolName);

        // once 훅: 실행 후 설정에서 제거
        // async 훅: 비동기 실행, 결과 기다리지 않음
        // asyncRewake 훅: 완료 시 AgentLoopService.ResumeAsync() 호출
        // if 조건: EvaluateCondition(def.Condition, context) 통과 시만 실행
    }

    // FileChanged 감시 시작 (SessionStart 훅 watchPaths에서 호출)
    public void RegisterWatchPaths(IReadOnlyList<string> patterns)
    {
        foreach (var pattern in patterns)
            _fileWatcher.Watch(pattern, path =>
                RunAsync(HookEvent.FileChanged,
                    new HookContext { ChangedFilePath = path, ... }, CancellationToken.None));
    }

    // Once 훅 자기 제거
    private void RemoveOnceHook(HookDefinition def) { ... }
}

public record HookRunResult
{
    public bool AnyBlocked { get; init; }
    public string? CombinedContext { get; init; }   // 여러 훅의 AdditionalContext 합산
    public string? UpdatedInput { get; init; }       // 마지막 updatedInput
    public PermissionDecision? PermissionDecision { get; init; }
}

17-C-5: AgentLoopService 통합

// AgentLoopService.cs 에서 새 이벤트 발화 위치:
public class AgentLoopService
{
    // 1. 사용자 메시지 수신 시 → UserPromptSubmit 훅
    private async Task<string?> OnUserMessageAsync(string message)
    {
        var ctx = new HookContext { Event = HookEvent.UserPromptSubmit, UserMessage = message, ... };
        var result = await _hookRunner.RunAsync(HookEvent.UserPromptSubmit, ctx, _ct);
        if (result.AnyBlocked) return null; // 차단
        return result.UpdatedInput ?? message; // 수정된 메시지 반환
    }

    // 2. 세션 시작 시 → SessionStart 훅
    private async Task OnSessionStartAsync()
    {
        var result = await _hookRunner.RunAsync(HookEvent.SessionStart, ..., _ct);
        if (result.WatchPaths?.Count > 0)
            _hookRunner.RegisterWatchPaths(result.WatchPaths);
    }

    // 3. 컨텍스트 압축 전후 → PreCompact/PostCompact 훅
    private async Task CompactContextAsync()
    {
        await _hookRunner.RunAsync(HookEvent.PreCompact, ..., _ct);
        // ... 실제 압축 ...
        await _hookRunner.RunAsync(HookEvent.PostCompact, ..., _ct);
    }

    // 4. 권한 요청 시 → PermissionRequest 훅
    private async Task<PermissionDecision> RequestPermissionAsync(string toolName, string input)
    {
        var ctx = new HookContext { Event = HookEvent.PermissionRequest, ToolName = toolName, ToolInput = input };
        var result = await _hookRunner.RunAsync(HookEvent.PermissionRequest, ctx, _ct);
        return result.PermissionDecision ?? PermissionDecision.Ask; // 훅 미설정 시 기존 UI 표시
    }
}

17-C-6: 설정 스키마 변경

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

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

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

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

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

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

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

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

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

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

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

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

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

17-D-1: SkillFrontmatter 확장

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

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

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

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

    [JsonPropertyName("paths")]
    public IReadOnlyList<string> Paths { get; init; } = Array.Empty<string>();
    // glob 패턴 목록: ["**/*.py", "src/**/*.cs"]
    // 해당 파일 작업 시 자동 컨텍스트 주입

    [JsonPropertyName("model")]
    public string? Model { get; init; }
    // 스킬별 모델 오버라이드: "claude-opus-4-6", "claude-haiku-4-5" 등

    [JsonPropertyName("user_invocable")]
    public bool UserInvocable { get; init; } = true;
    // false → /스킬 목록에서 숨김, AI가 자동 활성화만 가능

    [JsonPropertyName("hooks")]
    public SkillHooksConfig? Hooks { get; init; }
    // 스킬 실행 중에만 적용되는 훅
}

public enum SkillContext { Default, Fork }

public class SkillHooksConfig
{
    [JsonPropertyName("pre_skill_execute")]
    public List<HookDefinition> PreSkillExecute { get; init; } = new();

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

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

17-D-2: PathBasedSkillActivator

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

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

    // 활성 스킬의 컨텍스트를 시스템 프롬프트 주입용 텍스트로 빌드
    public string BuildSkillContextInjection(IReadOnlyList<LoadedSkill> skills)
    {
        if (skills.Count == 0) return string.Empty;
        var sb = new StringBuilder();
        sb.AppendLine("## 현재 파일에 자동 적용된 스킬");
        foreach (var skill in skills)
        {
            sb.AppendLine($"### {skill.Name}");
            sb.AppendLine(skill.Body);
        }
        return sb.ToString();
    }

    // GlobMatcher: ** 와일드카드, *.ext 패턴 지원
    private static class GlobMatcher
    {
        public static bool IsMatch(string path, string pattern) { ... }
    }
}

17-D-3: ForkContextSkillRunner

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

    public async Task<SkillResult> ExecuteInForkAsync(
        LoadedSkill skill,
        string userInput,
        CancellationToken ct)
    {
        // 1. 격리된 AgentSession 생성 (부모 대화 히스토리 제외)
        // 2. 스킬의 시스템 프롬프트만 포함
        // 3. 모델 오버라이드 적용 (skill.Frontmatter.Model)
        // 4. 스킬 범위 훅 등록 (skill.Frontmatter.Hooks)
        // 5. 미니 에이전트 루프 실행 (최대 10 반복)
        // 6. 결과 텍스트 반환 (메인 대화에 reply로 주입)
    }
}

public record SkillResult
{
    public bool IsSuccess { get; init; }
    public string Output { get; init; } = string.Empty;
    public string? ErrorMessage { get; init; }
    public int Iterations { get; init; }
}

17-D-4: SkillLoaderService 확장

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

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

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

17-D-5: AgentLoopService 통합

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

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

    // 스킬 실행 시: fork vs default 분기
    public async Task<string> ExecuteSkillAsync(LoadedSkill skill, string input, CancellationToken ct)
    {
        if (skill.Frontmatter.Context == SkillContext.Fork)
        {
            var result = await _forkRunner.ExecuteInForkAsync(skill, input, ct);
            return result.Output;
        }
        else
        {
            // 기존 방식: 메인 컨텍스트에서 실행
            return await ExecuteSkillInMainContextAsync(skill, input, ct);
        }
    }
}

17-D-6: 설정 없음 (프론트매터 기반)

스킬 시스템 고도화는 SKILL.md 프론트매터 확장이 핵심. 별도 AppSettings 변경 최소화. SkillLoaderService에서 신규 프론트매터 필드 파싱만 추가.


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

목표: @include 지시어·경로 기반 규칙 주입·/compact 명령·파일 되감기

17-E-1: AxMdIncludeResolver

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

    // @파일경로 지시어 모두 해석하여 최종 텍스트 반환
    public async Task<string> ResolveAsync(
        string content,
        string basePath,
        int depth = 0,
        HashSet<string>? visited = null)
    {
        if (depth >= MaxDepth) return content; // 최대 깊이 초과
        visited ??= new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        // @로 시작하는 줄 감지: "@./shared/common.md", "@../global/rules.md"
        var regex = new Regex(@"^@(.+)$", RegexOptions.Multiline);
        // ... 파일 읽기·재귀·순환 감지 ...
    }

    // 파일 크기 40,000자 초과 경고
    public bool IsOversized(string content) => content.Length > MaxFileSize;

    // 순환 참조 감지: visited 집합으로 추적
    private void DetectCircular(string filePath, HashSet<string> visited)
    {
        if (visited.Contains(filePath))
            throw new InvalidOperationException($"순환 참조 감지: {filePath}");
        visited.Add(filePath);
    }
}

17-E-2: PathScopedRuleInjector

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

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

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

    // 시스템 프롬프트 주입용 텍스트 빌드
    public string BuildInjection(IReadOnlyList<RuleFile> rules)
    {
        var sb = new StringBuilder("## 프로젝트 규칙\n\n");
        foreach (var rule in rules)
            sb.AppendLine(rule.Body).AppendLine();
        return sb.ToString();
    }
}

public record RuleFile
{
    public string FilePath { get; init; }
    public RuleFrontmatter Frontmatter { get; init; }
    public string Body { get; init; }
}

public record RuleFrontmatter
{
    [JsonPropertyName("name")]
    public string Name { get; init; } = string.Empty;

    [JsonPropertyName("paths")]
    public IReadOnlyList<string> Paths { get; init; } = Array.Empty<string>();
    // 빈 배열 → 항상 적용
    // ["src/**/*.py"] → Python 파일 작업 시만 적용
}

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

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

    // /compact 슬래시 명령 처리
    public async Task<CompactionResult> CompactAsync(
        string sessionId,
        CompactionOptions opts,
        CancellationToken ct)
    {
        // 1. PreCompact 훅 실행
        await _hookRunner.RunAsync(HookEvent.PreCompact, ..., ct);

        // 2. 현재 TaskState를 새 컨텍스트 요약에 포함
        var stateContext = _taskState.BuildCompactContextInjection();

        // 3. LLM으로 대화 이력 요약 생성
        var summary = await GenerateSummaryAsync(sessionId, stateContext, ct);

        // 4. 대화 이력 교체 (요약으로)
        _agentLoop.ReplaceHistoryWithSummary(sessionId, summary);

        // 5. PostCompact 훅 실행
        await _hookRunner.RunAsync(HookEvent.PostCompact, ..., ct);

        return new CompactionResult { Summary = summary, SavedTokens = ... };
    }

    // rewind_files: 특정 메시지 이후 에이전트가 수정한 파일 되돌리기
    public async Task RewindFilesAsync(string sessionId, string afterMessageId)
    {
        // 1. AgentEventLog에서 afterMessageId 이후 file_edit 이벤트 수집
        // 2. 각 이벤트의 이전 파일 내용(백업)을 복원
        // 3. git checkout -- <files> 또는 내부 백업에서 복원
    }
}

public record CompactionOptions
{
    public bool KeepTaskState { get; init; } = true;
    public bool KeepRecentN { get; init; } = true;
    public int RecentNMessages { get; init; } = 5;  // 최근 N개 메시지는 유지
}

17-E-4: /compact 슬래시 명령 등록

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

    private readonly ContextCompactionService _compaction;

    public async Task<SlashCommandResult> ExecuteAsync(
        string args, AgentSession session, CancellationToken ct)
    {
        var result = await _compaction.CompactAsync(session.Id, new CompactionOptions(), ct);
        return SlashCommandResult.Success(
            $"컨텍스트 압축 완료. 절약된 토큰: {result.SavedTokens:N0}");
    }
}

17-E-5: 파일 크기 경고 UI

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

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

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

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

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

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

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

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

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

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

public enum PermissionBehavior { Allow, Deny, Ask }

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

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

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

// 구현: Deny 규칙 핸들러
public class DenyRuleHandler : PermissionHandler
{
    private readonly IReadOnlyList<PermissionRule> _denyRules;

    public override PermissionDecision? Handle(string toolName, string input)
    {
        if (_denyRules.Any(r => Matches(r, toolName, input)))
            return PermissionDecision.Deny;
        return _next?.Handle(toolName, input);
    }

    private bool Matches(PermissionRule rule, string toolName, string input)
        => rule.ToolName == toolName &&
           (rule.Pattern == null || GlobMatcher.IsMatch(input, rule.Pattern));
}

// 구현: Allow 규칙 핸들러
public class AllowRuleHandler : PermissionHandler
{
    private readonly IReadOnlyList<PermissionRule> _allowRules;
    public override PermissionDecision? Handle(string toolName, string input) { ... }
}

// 구현: AcceptEdits 모드 핸들러
public class AcceptEditsHandler : PermissionHandler
{
    private readonly PermissionMode _mode;
    private static readonly HashSet<string> _editTools = new() { "file_edit", "file_write", "script_create" };

    public override PermissionDecision? Handle(string toolName, string input)
    {
        if (_mode == PermissionMode.AcceptEdits && _editTools.Contains(toolName))
            return PermissionDecision.Allow;  // 파일 편집 자동승인
        return _next?.Handle(toolName, input);
    }
}

// 권한 결정 서비스 (Chain 조립)
public class PermissionDecisionService
{
    private readonly PermissionHandler _chain;

    public PermissionDecisionService(AppSettings settings)
    {
        // Chain 조립: Deny → Allow → AcceptEdits → Default(Ask)
        var denyHandler = new DenyRuleHandler(settings.Permissions.DenyRules);
        var allowHandler = new AllowRuleHandler(settings.Permissions.AllowRules);
        var acceptEditsHandler = new AcceptEditsHandler(settings.Permissions.Mode);
        var defaultHandler = new DefaultAskHandler();

        denyHandler.SetNext(allowHandler).SetNext(acceptEditsHandler).SetNext(defaultHandler);
        _chain = denyHandler;
    }

    public PermissionDecision Decide(string toolName, string input)
        => _chain.Handle(toolName, input) ?? PermissionDecision.Ask;
}

17-F-3: MCP 도구 권한

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

public class McpPermissionFilter
{
    private readonly PermissionDecisionService _permissions;

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

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

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

        return true;
    }
}

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

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

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

    [JsonPropertyName("allow")]
    public List<PermissionRule> AllowRules { get; set; } = new();
    // 예: [{"tool":"process","pattern":"git *","behavior":"allow"}]

    [JsonPropertyName("deny")]
    public List<PermissionRule> DenyRules { get; set; } = new();
    // 예: [{"tool":"process","pattern":"rm -rf *","behavior":"deny"}]
}

17-F-5: 권한 편집기 UI (SettingsWindow — 전문 설정 탭)

[설정창 > 권한 탭]

권한 모드: [Default ▾]  (Default / AcceptEdits / Plan / BypassPermissions)

── 허용 규칙 ──────────────────────────────
[도구: process ] [패턴: git *      ] [허용 ▾] [삭제]
[도구: process ] [패턴: dotnet *   ] [허용 ▾] [삭제]
[+ 규칙 추가]

── 차단 규칙 ──────────────────────────────
[도구: process ] [패턴: rm -rf *   ] [차단 ▾] [삭제]
[도구: mcp__외부서버 ] [패턴: (전체) ] [차단 ▾] [삭제]
[+ 규칙 추가]
// PermissionsTabViewModel (SettingsWindow 내 탭)
public class PermissionsTabViewModel : ViewModelBase
{
    public PermissionMode SelectedMode { get; set; }
    public ObservableCollection<PermissionRuleViewModel> AllowRules { get; }
    public ObservableCollection<PermissionRuleViewModel> DenyRules { get; }
    public ICommand AddAllowRuleCommand { get; }
    public ICommand AddDenyRuleCommand { get; }
    public ICommand DeleteRuleCommand { get; }  // param: PermissionRuleViewModel
    public ICommand SaveCommand { get; }
}

17-F-6: MCP HTTP+SSE 전송 추가

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

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

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

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

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

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

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

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

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

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

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

// 기존 DiffPanel 확장
public class MultiFileDiffViewModel : ViewModelBase
{
    public ObservableCollection<FileDiffViewModel> FileDiffs { get; }
    public int TotalChangedFiles => FileDiffs.Count;
    public int TotalAddedLines { get; }
    public int TotalRemovedLines { get; }

    // 전체 수락/거부
    public ICommand AcceptAllCommand { get; }
    public ICommand RejectAllCommand { get; }

    // 파일별 수락/거부
    public ICommand AcceptFileCommand { get; }   // param: FileDiffViewModel
    public ICommand RejectFileCommand { get; }   // param: FileDiffViewModel
}

public class FileDiffViewModel : ViewModelBase
{
    public string FilePath { get; }
    public ObservableCollection<DiffHunkViewModel> Hunks { get; }
    public bool IsExpanded { get; set; } = true;  // 파일별 접기/펼치기
    public DiffFileStatus Status { get; }  // Modified | Added | Deleted | Renamed

    public ICommand AcceptCommand { get; }
    public ICommand RejectCommand { get; }
}

XAML 핵심:

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

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

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

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

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

    public async Task<IReadOnlyList<FileContext>> CollectAsync(
        string userMessage, string projectRoot, CancellationToken ct)
    {
        var matches = _filePatterns.Matches(userMessage);
        var results = new List<FileContext>();

        foreach (Match m in matches)
        {
            var path = m.Groups[1].Value ?? m.Groups[2].Value;
            var fullPath = Path.Combine(projectRoot, path);
            if (File.Exists(fullPath))
            {
                var content = await File.ReadAllTextAsync(fullPath, ct);
                results.Add(new FileContext { Path = path, Content = content });
            }
        }
        return results;
    }
}

// 도구 위험도 정적 매핑
public static class ToolRiskMapper
{
    public static ToolRisk GetRisk(string toolName) => toolName switch
    {
        "file_read" or "code_search" or "list_directory" => ToolRisk.Low,
        "file_edit" or "script_create" or "git_commit" => ToolRisk.Medium,
        "process" or "bash" or "file_delete" or "registry_write" => ToolRisk.High,
        _ when toolName.StartsWith("mcp__") => ToolRisk.Medium,
        _ => ToolRisk.Medium
    };
}

public enum ToolRisk { Low, Medium, High }

Phase 18-A — 멀티에이전트 팀 + Worktree 격리 (v2.0)

목표: 코디네이터 에이전트 모드·Worktree 격리·위임 도구·백그라운드 에이전트

18-A-1: 코디네이터 에이전트

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

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

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

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

public record CoordinatorPlan
{
    public string OriginalRequest { get; init; }
    public IReadOnlyList<SubTask> Tasks { get; init; }
}

public record SubTask
{
    public string Id { get; init; }
    public string AgentType { get; init; }        // "code-reviewer", "researcher", "implementer"
    public string Description { get; init; }
    public IReadOnlyList<string> Dependencies { get; init; }  // 선행 SubTask Id 목록
    public SubTaskStatus Status { get; set; }     // Pending | Running | Completed | Failed
    public string? Result { get; set; }
}

public enum SubTaskStatus { Pending, Running, Completed, Failed }

18-A-2: WorktreeManager

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

    // 새 worktree 생성 (임시 브랜치)
    public async Task<Worktree> CreateAsync(
        string repoRoot,
        string branchName,
        CancellationToken ct)
    {
        var worktreePath = Path.Combine(Path.GetTempPath(), "ax-worktree-" + Guid.NewGuid().ToString("N")[..8]);
        // git worktree add <path> -b <branch>
        await RunGitAsync($"worktree add {worktreePath} -b {branchName}", repoRoot, ct);
        var wt = new Worktree { Path = worktreePath, Branch = branchName, RepoRoot = repoRoot };
        _active.Add(wt);
        return wt;
    }

    // 변경 사항 병합 (승인 시)
    public async Task MergeAsync(Worktree worktree, string targetBranch, CancellationToken ct)
    {
        await RunGitAsync($"merge {worktree.Branch}", worktree.RepoRoot, ct);
    }

    // worktree 제거 (거부 시)
    public async Task DisposeAsync(Worktree worktree, bool keepBranch = false, CancellationToken ct = default)
    {
        await RunGitAsync($"worktree remove {worktree.Path} --force", worktree.RepoRoot, ct);
        if (!keepBranch)
            await RunGitAsync($"branch -D {worktree.Branch}", worktree.RepoRoot, ct);
        _active.Remove(worktree);
    }

    private Task RunGitAsync(string args, string cwd, CancellationToken ct) { ... }

    public void Dispose() { /* 남은 worktree 정리 */ }
}

public record Worktree
{
    public string Path { get; init; }
    public string Branch { get; init; }
    public string RepoRoot { get; init; }
    public DateTime CreatedAt { get; init; } = DateTime.UtcNow;
}

18-A-3: DelegateAgentTool

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

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

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

    public async Task<ToolResult> ExecuteAsync(JsonObject args, CancellationToken ct)
    {
        var agentType = args["agent_type"]!.GetValue<string>();
        var task = args["task"]!.GetValue<string>();
        var context = args["context"]?.GetValue<string>() ?? string.Empty;
        var isolation = args["isolation"]?.GetValue<string>() ?? "none";

        // 에이전트 타입별 메모리 로드
        var memory = await _agentMemory.LoadMemoryAsync(agentType);

        // worktree 격리 필요 시 생성
        Worktree? worktree = null;
        if (isolation == "worktree")
            worktree = await _worktreeManager.CreateAsync(
                GetCurrentRepoRoot(), $"ax-{agentType}-{DateTime.UtcNow:yyMMddHHmm}", ct);

        // 서브에이전트 실행
        var result = await _backgroundAgents.StartAsync(new AgentTask
        {
            AgentType = agentType,
            Description = task,
            Context = context + (memory != null ? $"\n\n## 이전 학습:\n{memory}" : string.Empty),
            WorkingDirectory = worktree?.Path ?? GetCurrentDirectory()
        }, ct);

        // 결과 크기 제한: 100,000자
        if (result.Length > 100_000)
            result = result[..100_000] + "\n\n[결과가 100,000자를 초과하여 잘림]";

        return ToolResult.Ok(result);
    }
}

18-A-4: BackgroundAgentService

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

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

    // 비동기 실행 시작 (즉시 반환, 완료 시 이벤트)
    public async Task<string> StartAsync(AgentTask task, CancellationToken ct)
    {
        var id = Guid.NewGuid().ToString("N")[..8];
        var bgTask = new BackgroundAgentTask { Id = id, Task = task, StartedAt = DateTime.UtcNow };
        _running[id] = bgTask;

        _ = Task.Run(async () =>
        {
            try
            {
                // 격리된 AgentLoopService 인스턴스 생성
                var result = await RunSubAgentLoopAsync(task, ct);
                bgTask.Result = result;
                bgTask.Status = BackgroundAgentStatus.Completed;
                AgentCompleted?.Invoke(this, new AgentCompletedEventArgs { Task = bgTask, Result = result });
            }
            catch (Exception ex)
            {
                bgTask.Status = BackgroundAgentStatus.Failed;
                bgTask.Error = ex.Message;
                AgentCompleted?.Invoke(this, new AgentCompletedEventArgs { Task = bgTask, Error = ex.Message });
            }
            finally { _running.TryRemove(id, out _); }
        }, ct);

        return id;
    }

    public IReadOnlyList<BackgroundAgentTask> GetActive()
        => _running.Values.ToList();
}

// App.xaml.cs 에서 트레이 알림 연결
// _backgroundAgents.AgentCompleted += (s, e) =>
//     _trayIcon.ShowBalloonTip(e.Task.AgentType + " 완료", e.Result?.Length > 100 ? e.Result[..100] + "..." : e.Result, ToolTipIcon.Info);

18-A-5: AgentTypeMemoryRepository

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

    public async Task<string?> LoadMemoryAsync(string agentType)
    {
        var path = GetPath(agentType);
        return File.Exists(path) ? await File.ReadAllTextAsync(path) : null;
    }

    public async Task SaveMemoryAsync(string agentType, string content)
    {
        Directory.CreateDirectory(Path.GetDirectoryName(GetPath(agentType))!);
        await File.WriteAllTextAsync(GetPath(agentType), content);
    }

    // 학습 내용 추가 (기존 내용 끝에 append)
    public async Task AppendLearnAsync(string agentType, string learning)
    {
        var existing = await LoadMemoryAsync(agentType) ?? string.Empty;
        var entry = $"\n- [{DateTime.Now:yyyy-MM-dd HH:mm}] {learning}";
        await SaveMemoryAsync(agentType, existing + entry);
    }

    private string GetPath(string agentType)
        => Path.Combine(_baseDir, agentType, "MEMORY.md");
}

18-A-6: 코디네이터 모드 UI

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

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

// CoordinatorPlanViewModel: SubTask 목록 + 진행 상태 표시
public class CoordinatorPlanViewModel : ViewModelBase
{
    public ObservableCollection<SubTaskViewModel> Tasks { get; }
    public int CompletedCount { get; }
    public int TotalCount { get; }
    public double Progress => TotalCount > 0 ? (double)CompletedCount / TotalCount : 0;
}

Phase 18-B — 에이전트 리플레이/디버깅 (v2.0)

목표: Phase 17-B의 이벤트 로그를 활용한 세션 재생 및 분기 재실행

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

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

    // 특정 시점에서 분기 (새 세션 생성)
    public async Task<string> ForkFromAsync(string sessionId, long forkAtSeqNo, CancellationToken ct)
    {
        var newSessionId = Guid.NewGuid().ToString("N");
        await ReplayToAsync(sessionId, forkAtSeqNo, ct);
        // 새 세션 ID로 계속
        return newSessionId;
    }

    // ViewModel: 타임라인 UI
    // WorkflowAnalyzerViewModel 에 리플레이 타임라인 패널 추가
}

public class ReplayTimelineViewModel : ViewModelBase
{
    public ObservableCollection<TimelineEventViewModel> Events { get; }
    public long CurrentPosition { get; set; }
    public ICommand SeekToCommand { get; }    // param: long seqNo
    public ICommand ForkFromCommand { get; }  // param: long seqNo
    public ICommand PlayPauseCommand { get; }
}

Phase 18-C — 플러그인 갤러리 + 생태계 확장 (v2.0)

18-C-1: 플러그인 갤러리

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

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

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

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

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

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

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

// 플러그인 설치 서비스
public class PluginInstallService
{
    // zip 파일에서 설치
    public async Task<InstallResult> InstallFromZipAsync(string zipPath, CancellationToken ct)
    {
        // 1. zip 압축 해제 → 임시 디렉토리
        // 2. manifest.json 파싱·검증
        // 3. %APPDATA%\AxCopilot\plugins\<id>\ 에 복사
        // 4. 플러그인 타입에 따라 등록:
        //    - Skill → SkillLoaderService.Reload()
        //    - Tool → ToolRegistry.Register()
    }

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

    // 설치된 플러그인 목록
    public IReadOnlyList<InstalledPlugin> GetInstalled() { ... }

    // 플러그인 제거
    public Task UninstallAsync(string pluginId, CancellationToken ct) { ... }
}

Phase L3 — 런처 에코시스템 (v2.0)

# 기능 연결 Phase 핵심 클래스
L3-1 플러그인 갤러리 18-C1 PluginGalleryViewModel, PluginInstallService
L3-2 웹 검색 AI 요약 18-C5 WebSearchSummaryHandler, ContentExtractor
L3-3 AI 스니펫 18-C2 AiSnippetHandler, SnippetTemplateService
L3-4 파라미터 퀵링크 18-C3 QuickLinkHandler, UrlTemplateEngine
L3-7 알림 센터 통합 18-A4 NotificationCenterService (Windows Toast API)

구현 의존성 그래프

Phase 17 구현 순서:

[17-UI-1~4]   AgentWindow 골격 + ViewModel
      ↓
[17-UI-5~6]   Sidebar + InlineSettings (SettingsWindow 이전)
      ↓
[17-B-1~2]    TaskState + EventLog (인프라 먼저)
      ↓
[17-C-1~5]    Hook 이벤트/타입 확장 (TaskState 이용)
      ↓
[17-D-1~5]    Skill fork/paths (Hook 인프라 이용)
      ↓
[17-E-1~4]    @include + 경로 규칙 (Skill 시스템과 병렬)
      ↓
[17-F-1~5]    Permission Chain + acceptEdits
      ↓
[17-A-1~4]    Reflexion (모든 인프라 완성 후)
      ↓
[17-G-1~2]    MultiFileDiff + AutoContext (독립, 병렬 가능)

Phase 18 구현 순서:

[18-A-2]   WorktreeManager (git 인프라)
      ↓
[18-A-4]   BackgroundAgentService
      ↓
[18-A-3]   DelegateAgentTool (WorktreeManager + BackgroundAgent 이용)
      ↓
[18-A-1]   CoordinatorAgent (DelegateTool 이용)
      ↓
[18-A-5]   AgentTypeMemoryRepository
      ↓
[18-B]     ReplayService (EventLog Phase 17-B 이용)
      ↓
[18-C]     플러그인 갤러리 (독립)

버전별 출시 계획

버전 코드명 포함 Phase 핵심 신규 클래스 수
v1.8.0 에이전트 인프라 17-UI, 17-A~G ~35개 신규 클래스 완료
v1.8.1 인프라 안정화 버그픽스 + 17-* 마이너
v2.0 에이전트 팀 18-A~C, L3 ~25개 신규 클래스 완료
v2.1 CC 동등성 19-A~G (Claude Code 갭 해소) ~30개 신규 클래스
v2.2 SDK 프로토콜 20-A~C (임베딩 + 외부 연동) ~15개 신규 클래스
v2.3 AX Agent UI 전면 개편 21 (Claude.ai+Codex 레이아웃) UI 계층 60% 재작성
v3.0 크로스플랫폼 LP-1~3 (Avalonia) UI 계층 40% 재작성

Phase 19 — Claude Code 동등성 달성 (v2.1)

목표: docs/claude-code-docs-main/ 문서 분석 결과 발견된 AX Agent 갭을 해소하여 Claude Code 수준의 에이전트 인프라를 달성한다.

갭 분석 요약

기능 영역 Claude Code AX Agent 현황 우선순위 Phase
계층형 메모리 (4층) 시스템/유저/프로젝트/로컬 + @include AX.md 단일 파일 19-A
권한 패턴 매칭 git *, npm run * 패턴 + 복합 명령 분해 경로 기반 Allow/Deny만 19-B
훅 이벤트 완성 14+ 이벤트, Shell/HTTP/LLM/Agent 타입 일부 이벤트, Shell 위주 19-C
스킬 인라인 실행 !``cmd`` 실행 치환, 유저급 스킬, 네임스페이스 프리셋 파일 치환 없음 19-D
세션 재개/포크 --resume, forkSession(), 태깅 JSONL 리플레이만 19-E
출력 예산 관리 maxResultSizeChars, 임시파일 스필오버 고정 8000자 절단 19-F
/init 명령 프로젝트 분석 → AX.md 자동 생성 없음 19-G

Phase 19-A — 계층형 메모리 시스템 (v2.1)

목표: AX.md 단일 파일 → 4계층 캐스케이드 메모리 시스템. 경로 기반 규칙 파일 지원.

4계층 메모리 아키텍처

계층 경로 범위 우선순위
L1 시스템 %APPDATA%\AxCopilot\system.md 앱 전역 관리자 규칙 최하위
L2 유저 %USERPROFILE%\.axcopilot\AX.md 유저 전체 선호
L3 프로젝트 <cwd>\AX.md + <cwd>\.axcopilot\rules\*.md 팀 공유 규칙
L4 로컬 <cwd>\AX.local.md 개인 프로젝트 오버라이드 (gitignore) 최상위

핵심 클래스

// HierarchicalMemoryLoader.cs
public class HierarchicalMemoryLoader
{
    /// <summary>CWD에서 루트까지 탐색하며 계층별 메모리 파일 수집.</summary>
    public async Task<MemoryContext> LoadAsync(string workFolder, CancellationToken ct)
    {
        var layers = new List<MemoryLayer>();
        layers.Add(await LoadSystemLayerAsync(ct));      // L1
        layers.Add(await LoadUserLayerAsync(ct));        // L2
        layers.AddRange(await LoadProjectLayersAsync(workFolder, ct)); // L3 (CWD→루트)
        layers.Add(await LoadLocalLayerAsync(workFolder, ct));  // L4
        return new MemoryContext(layers);
    }

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

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

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

AgentLoopService 통합

// AgentLoopService.cs — 컨텍스트 조립 부분 교체
private async Task<string> BuildSystemContextAsync(CancellationToken ct)
{
    // 기존: 단일 AX.md 로드
    // 변경: 계층형 로더 사용
    var memCtx = await _memoryLoader.LoadAsync(_workFolder, ct);
    var pathRules = _ruleScopeLoader.GetApplicableRules(_currentEditingFile, _cachedRules);
    return memCtx.Compose() + "\n\n" + string.Join("\n\n", pathRules);
}

슬래시 명령 추가

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

설정 항목

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

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

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

핵심 클래스

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

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

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

    /// <summary>복합 명령의 모든 서브 커맨드에 대해 권한 검사.
    /// 하나라도 Deny이면 전체 Deny.</summary>
    public PermissionDecision CheckAll(string compoundCommand,
        CommandPatternMatcher matcher,
        IReadOnlyList<PermissionRuleEntry> rules) { ... }
}

// 항상 차단되는 명령 목록 (하드코딩)
public static class AlwaysBlockedCommands
{
    public static readonly string[] Patterns =
    [
        "rd /s", "rmdir /s",           // 재귀 폴더 삭제
        "format *",                     // 드라이브 포맷
        "reg delete *",                // 레지스트리 삭제
        "del /f /s /q *",              // 강제 재귀 파일 삭제
        "netsh * delete *",            // 방화벽 규칙 삭제
        "schtasks /delete *",          // 작업 스케줄러 삭제
    ];
}

PermissionRuleEntry 확장

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

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

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

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

도구 가시성 필터링

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

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

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

추가 훅 이벤트

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

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

훅 타입 완성 (4종)

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

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

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

    // HTTP 훅
    public string? Url { get; set; }
    public Dictionary<string, string> Headers { get; set; } = new();

    // LLM 훅 (검증용 Claude 호출)
    public string? LlmPrompt { get; set; }
    public string? LlmModel { get; set; }

    // Agent 훅 (풀 에이전트 루프)
    public string? AgentPrompt { get; set; }
    public string[]? AgentTools { get; set; }
}

// Exit code semantics
// 0 = 성공, 계속
// 2 = 차단/주입 (stderr → Claude에게 전달, 동작 차단 또는 루프 계속)
// 기타 = stderr → 사용자에만 표시, 계속

// $AX_ENV_FILE: CwdChanged 훅에서 환경변수 주입
// 예: echo "AX_PROJECT_TYPE=rust" >> $AX_ENV_FILE

PreToolUse 입력 수정 기능

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

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

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

스킬 인라인 명령 실행

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

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

    /// <summary>스킬 본문의 !`cmd` 블록을 실행하고 결과로 치환합니다.
    /// 예: !`git log --oneline -10` → 최근 10개 커밋 목록으로 치환</summary>
    public async Task<string> ProcessAsync(string skillContent, string workFolder,
        CancellationToken ct)
    {
        foreach (Match m in InlineCmd.Matches(skillContent))
        {
            var cmd = m.Groups[1].Value;
            var output = await ExecuteCommandAsync(cmd, workFolder, ct);
            skillContent = skillContent.Replace(m.Value, output);
        }
        return skillContent;
    }
}

스킬 프론트매터 확장

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

유저급 스킬 경로

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

스킬 네임스페이스

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

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

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

세션 관리 API

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

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

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

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

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

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

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

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

    public async Task<string?> GetAsync(string agentType, string workFolder,
        MemoryScope scope = MemoryScope.Project, CancellationToken ct = default) { ... }

    public async Task SaveAsync(string agentType, string workFolder,
        string content, MemoryScope scope = MemoryScope.Project,
        CancellationToken ct = default) { ... }
}

Phase 19-F — 출력 예산 + 컨텍스트 최적화 (v2.1)

목표: 고정 8000자 절단 → 도구별 출력 예산 + 대형 출력 임시파일 스필오버. 컨텍스트 조립 메모이제이션.

출력 예산 관리

// ToolOutputBudget.cs
public static class ToolOutputBudget
{
    // 도구별 기본 최대 출력 크기
    private static readonly Dictionary<string, int> DefaultBudgets = new()
    {
        ["file_read"]      = 12_000,
        ["grep"]           = 8_000,
        ["bash"]           = 10_000,
        ["git_tool"]       = 6_000,
        ["http_tool"]      = 8_000,
        ["directory_list"] = 4_000,
        ["multi_read"]     = 20_000,
        // 기타 도구: 기본 6000자
    };

    public static int GetBudget(string toolName)
        => DefaultBudgets.GetValueOrDefault(toolName, 6_000);
}

// ToolOutputSpillover.cs
public class ToolOutputSpillover
{
    private readonly string _tempDir = Path.Combine(Path.GetTempPath(), "AxCopilot", "spill");

    /// <summary>출력이 예산 초과 시 임시파일에 저장하고 경로+미리보기 반환.</summary>
    public async Task<string> ProcessAsync(string toolName, string output, CancellationToken ct)
    {
        var budget = ToolOutputBudget.GetBudget(toolName);
        if (output.Length <= budget) return output;

        var path = Path.Combine(_tempDir, $"{Guid.NewGuid():N}.txt");
        Directory.CreateDirectory(_tempDir);
        await File.WriteAllTextAsync(path, output, ct);

        var preview = output[..Math.Min(500, output.Length)];
        return $"[출력이 {output.Length:N0}자로 예산({budget:N0}자)을 초과하여 임시파일에 저장됨]\n" +
               $"파일 경로: {path}\n미리보기:\n{preview}\n...";
    }
}

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

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

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

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

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

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

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

핵심 클래스

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

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

public record ProjectAnalysis(
    string Language,
    string Framework,
    string? TestFramework,
    string? CiSystem,
    IReadOnlyList<string> KeyDirectories,
    string? ReadmeSummary);

// /init 슬래시 명령
public class InitSlashCommand : ISlashCommand
{
    public string Name => "init";
    public string Description => "프로젝트를 분석하여 AX.md를 생성합니다";
    public async Task ExecuteAsync(string args, IChatContext ctx)
    {
        // 1. 분석 실행 + 진행 상황 스트리밍
        // 2. 생성된 AX.md 미리보기 표시
        // 3. 사용자 확인 후 파일 저장
        // 4. 필요 시 추천 스킬 제안
    }
}

지원 언어/프레임워크 탐지

탐지 파일 언어 프레임워크
*.csproj, *.sln C# .NET, ASP.NET, WPF
package.json JavaScript/TypeScript React, Vue, Next.js
Cargo.toml Rust
pyproject.toml, requirements.txt Python Django, FastAPI, Flask
pom.xml, build.gradle Java/Kotlin Spring
go.mod Go

Phase 20 — SDK 제어 프로토콜 (v2.2)

목표: AX Agent를 외부 툴/스크립트에서 임베드 가능한 JSON 스트리밍 프로토콜로 노출.

# 기능 핵심 클래스
20-A 양방향 JSON 스트리밍 서버 AgentSdkServer, SdkControlRequest, SdkMessage
20-B 커스텀 에이전트 정의 (세션별) CustomAgentDefinition, AgentTypeRegistry
20-C SDK 훅 콜백 (외부 권한 처리) SdkHookCallbackHandler, SdkPermissionDelegate
// SdkControlRequest 타입
public enum SdkControlRequestType
{
    Initialize,          // 세션 설정, 커스텀 에이전트 정의, 훅 등록
    SetPermissionMode,   // 권한 모드 변경
    SetModel,            // 모델 전환
    Interrupt,           // 현재 턴 취소
    CanUseTool,          // 권한 처리기 응답
    GetContextUsage,     // 컨텍스트 사용량 조회
    RewindFiles,         // 메시지 이후 파일 변경 되돌리기
    HookCallback,        // SDK측 훅 이벤트 전달
}

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

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

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

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

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

목표: Claude Code의 16종 슬래시 명령을 AX Agent에 구현. /init(19-G)을 제외한 15종 명령 추가. 기존 스킬 기반 / 호출과 공존하되, 내장 명령은 스킬보다 우선 처리.

구현 완료 (2026-04-03): ISlashCommand 인터페이스, SlashCommandRegistry, InputRouter, 13종 명령 (/compact, /clear, /memory, /model, /plan, /commit, /review, /mcp, /permissions, /hooks, /config, /skills, /help) + SlashAutoCompleteProvider.

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

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

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

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

    // UI 상호작용 (ViewModel 위임)
    Task SendSystemMessageAsync(string message);
    Task<string?> PromptUserAsync(string question);  // 사용자 입력 대기
    Task ShowToastAsync(string message);
    void ClearMessages();
    void UpdateSessionHeader();             // 모델/모드 변경 시 헤더 갱신
}

// 슬래시 명령 레지스트리 (Factory + Registry)
public class SlashCommandRegistry
{
    private readonly Dictionary<string, ISlashCommand> _commands = new(StringComparer.OrdinalIgnoreCase);
    private readonly Dictionary<string, string> _aliases = new(StringComparer.OrdinalIgnoreCase);

    public void Register(ISlashCommand command)
    {
        _commands[command.Name] = command;
        foreach (var alias in command.Aliases)
            _aliases[alias] = command.Name;
    }

    public ISlashCommand? Resolve(string input)
    {
        // "/compact focus on API changes" → name="compact", args="focus on API changes"
        var name = input.Split(' ', 2)[0].TrimStart('/');
        if (_commands.TryGetValue(name, out var cmd)) return cmd;
        if (_aliases.TryGetValue(name, out var canonical))
            return _commands.GetValueOrDefault(canonical);
        return null; // null → 스킬 검색으로 폴백
    }

    public IReadOnlyList<ISlashCommand> GetAll() => _commands.Values.ToList();

    // 기본 명령 등록
    public static SlashCommandRegistry CreateDefault(IAgentChatContext ctx)
    {
        var reg = new SlashCommandRegistry();
        reg.Register(new CompactCommand());
        reg.Register(new ClearCommand());
        reg.Register(new MemoryCommand());
        reg.Register(new ConfigCommand());
        reg.Register(new HooksCommand());
        reg.Register(new McpCommand());
        reg.Register(new PermissionsCommand());
        reg.Register(new ModelCommand());
        reg.Register(new PlanCommand());
        reg.Register(new CommitCommand());
        reg.Register(new ReviewCommand());
        reg.Register(new SkillsCommand());
        reg.Register(new HelpCommand());
        reg.Register(new InitCommand());  // 19-G에서 구현
        return reg;
    }
}

22-2: /compact — 컨텍스트 압축

public class CompactCommand : ISlashCommand
{
    public string Name => "compact";
    public string[] Aliases => Array.Empty<string>();
    public string Description => "대화 기록을 요약하여 컨텍스트 사용량을 줄입니다";
    public string? ArgumentHint => "[요약 지시사항]";
    public bool RequiresActiveSession => true;

    public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct)
    {
        // 1. PreCompact 훅 실행
        var hookResult = await ctx.HookRunner.RunAsync(HookEvent.PreCompact, new HookContext
        {
            Event = HookEvent.PreCompact,
            SessionId = ctx.AgentLoop.CurrentSessionId,
        }, ct);
        if (hookResult.AnyBlocked)
        {
            await ctx.SendSystemMessageAsync($"[컴팩션 차단됨] {hookResult.CombinedContext}");
            return;
        }

        // 2. 현재 대화 메시지를 LLM에게 요약 요청
        var messages = ctx.AgentLoop.GetCurrentMessages();
        var summaryPrompt = string.IsNullOrEmpty(arguments)
            ? "위 대화를 핵심 결정사항, 작업 파일, 남은 작업 중심으로 간결하게 요약하세요."
            : $"위 대화를 다음 지시에 따라 요약하세요: {arguments}";

        var summary = await ctx.Llm.GenerateAsync(
            BuildCompactPrompt(messages, summaryPrompt), ct);

        // 3. TaskState 갱신 (Working Memory 보존)
        if (ctx.AgentLoop.TaskState != null)
            await ctx.AgentLoop.TaskState.UpdateContextSummaryAsync(summary);

        // 4. 메시지 목록을 요약 메시지 1개로 교체
        ctx.AgentLoop.ReplaceMessagesWithSummary(summary);

        // 5. PostCompact 훅 실행
        await ctx.HookRunner.RunAsync(HookEvent.PostCompact, new HookContext
        {
            Event = HookEvent.PostCompact,
        }, ct);

        await ctx.SendSystemMessageAsync($"[컴팩션 완료] 대화가 요약되었습니다. 컨텍스트 사용량이 줄었습니다.");
    }

    private string BuildCompactPrompt(IReadOnlyList<ChatMessage> messages, string instruction)
    {
        var sb = new StringBuilder();
        sb.AppendLine("# 대화 기록");
        foreach (var msg in messages)
            sb.AppendLine($"[{msg.Role}] {msg.Content[..Math.Min(msg.Content.Length, 500)]}");
        sb.AppendLine();
        sb.AppendLine(instruction);
        return sb.ToString();
    }
}

22-3: /clear — 대화 초기화

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

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

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

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

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

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

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

public class MemoryCommand : ISlashCommand
{
    public string Name => "memory";
    public string[] Aliases => Array.Empty<string>();
    public string Description => "AX.md 메모리 파일을 편집합니다 (전역/프로젝트/로컬)";
    public string? ArgumentHint => "[global|project|local]";
    public bool RequiresActiveSession => false;

    public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct)
    {
        // 범위 결정
        var scope = arguments.Trim().ToLowerInvariant() switch
        {
            "global" or "user" => MemoryScope.User,
            "local" => MemoryScope.Local,
            _ => MemoryScope.Project, // 기본값
        };

        // 파일 경로 결정
        var path = scope switch
        {
            MemoryScope.User => Path.Combine(
                Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
                ".axcopilot", "AX.md"),
            MemoryScope.Local => Path.Combine(ctx.WorkFolder, "AX.local.md"),
            _ => Path.Combine(ctx.WorkFolder, "AX.md"),
        };

        // 파일이 없으면 템플릿 생성
        if (!File.Exists(path))
        {
            Directory.CreateDirectory(Path.GetDirectoryName(path)!);
            await File.WriteAllTextAsync(path,
                $"# AX Copilot 프로젝트 메모리 ({scope})\n\n여기에 지시사항을 작성하세요.\n", ct);
        }

        // 외부 에디터 열기 (기본 텍스트 에디터)
        Process.Start(new ProcessStartInfo(path) { UseShellExecute = true });

        await ctx.SendSystemMessageAsync(
            $"메모리 파일을 열었습니다: `{path}`\n편집 후 저장하면 다음 세션부터 반영됩니다.");
    }
}

public enum MemoryScope { User, Project, Local }

22-5: /model — 세션 모델 전환

public class ModelCommand : ISlashCommand
{
    public string Name => "model";
    public string[] Aliases => Array.Empty<string>();
    public string Description => "현재 세션의 AI 모델을 변경합니다";
    public string? ArgumentHint => "[모델명]";
    public bool RequiresActiveSession => true;

    public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct)
    {
        if (string.IsNullOrWhiteSpace(arguments))
        {
            // 현재 모델 + 사용 가능한 모델 목록 표시
            var current = ctx.AgentLoop.CurrentModel;
            var available = ctx.Settings.Settings.Llm.RegisteredModels
                .Select(m => m.DisplayName).ToList();
            await ctx.SendSystemMessageAsync(
                $"현재 모델: **{current}**\n사용 가능: {string.Join(", ", available)}");
            return;
        }

        // 모델 변경
        var success = ctx.AgentLoop.TrySetModel(arguments.Trim());
        if (success)
        {
            ctx.UpdateSessionHeader();
            await ctx.SendSystemMessageAsync($"모델이 **{arguments.Trim()}**(으)로 변경되었습니다.");
        }
        else
        {
            await ctx.SendSystemMessageAsync($"모델 '{arguments.Trim()}'을(를) 찾을 수 없습니다.");
        }
    }
}

22-6: /plan — 플랜 모드 토글

public class PlanCommand : ISlashCommand
{
    public string Name => "plan";
    public string[] Aliases => Array.Empty<string>();
    public string Description => "플랜 모드를 토글하거나 계획을 생성합니다";
    public string? ArgumentHint => "[open|<계획 설명>]";
    public bool RequiresActiveSession => true;

    public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct)
    {
        var arg = arguments.Trim().ToLowerInvariant();

        if (arg == "open")
        {
            // 현재 계획 표시
            var plan = ctx.AgentLoop.CurrentPlan;
            if (plan == null)
                await ctx.SendSystemMessageAsync("현재 활성 계획이 없습니다.");
            else
                await ctx.SendSystemMessageAsync($"## 현재 계획\n{plan}");
            return;
        }

        if (string.IsNullOrEmpty(arg))
        {
            // 플랜 모드 토글
            var newMode = ctx.AgentLoop.TogglePlanMode();
            ctx.UpdateSessionHeader();
            await ctx.SendSystemMessageAsync(
                $"플랜 모드: **{newMode}** ({newMode switch {
                    PlanMode.Off => "변경 사항을 바로 실행합니다",
                    PlanMode.Always => "실행 전 항상 계획을 작성하고 승인을 기다립니다",
                    PlanMode.Auto => "복잡한 작업에만 자동으로 계획을 작성합니다",
                    _ => "" }})");
            return;
        }

        // 주어진 설명으로 계획 생성
        await ctx.AgentLoop.CreatePlanAsync(arg, ct);
    }
}

22-7: /commit — AI 커밋 메시지

public class CommitCommand : ISlashCommand
{
    public string Name => "commit";
    public string[] Aliases => Array.Empty<string>();
    public string Description => "AI가 생성한 메시지로 git 커밋을 만듭니다";
    public string? ArgumentHint => null;
    public bool RequiresActiveSession => false;

    public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct)
    {
        // 1. git status + git diff 확인
        var statusResult = await ProcessHelper.RunAsync("git", "status --porcelain", ctx.WorkFolder, ct);
        if (string.IsNullOrWhiteSpace(statusResult.StdOut))
        {
            await ctx.SendSystemMessageAsync("커밋할 변경 사항이 없습니다.");
            return;
        }

        var diffResult = await ProcessHelper.RunAsync("git", "diff --cached", ctx.WorkFolder, ct);
        var diffAll = string.IsNullOrWhiteSpace(diffResult.StdOut)
            ? (await ProcessHelper.RunAsync("git", "diff", ctx.WorkFolder, ct)).StdOut
            : diffResult.StdOut;

        // 2. LLM에게 커밋 메시지 생성 요청
        var prompt = $"""
            아래 git diff를 분석하고 간결한 커밋 메시지를 작성하세요.
            - 첫 줄: 50자 이내 제목 (무엇을 왜)
            - 빈 줄 후 본문: 변경 이유와 영향 (선택)
            - 비밀 파일(.env 등) 포함 여부 경고

            ```diff
            {diffAll[..Math.Min(diffAll.Length, 8000)]}
            ```

            git status:
            {statusResult.StdOut}
            """;

        var commitMsg = await ctx.Llm.GenerateAsync(prompt, ct);

        // 3. 사용자 확인
        await ctx.SendSystemMessageAsync($"## 제안된 커밋 메시지\n```\n{commitMsg}\n```\n\n이 메시지로 커밋할까요? (yes/no)");
        var answer = await ctx.PromptUserAsync("커밋 확인");
        if (answer?.Trim().ToLowerInvariant() is not ("yes" or "y" or "ㅇ"))
        {
            await ctx.SendSystemMessageAsync("커밋이 취소되었습니다.");
            return;
        }

        // 4. git add + commit 실행
        await ProcessHelper.RunAsync("git", "add -A", ctx.WorkFolder, ct);
        var commitResult = await ProcessHelper.RunAsync("git",
            $"commit -m \"{commitMsg.Replace("\"", "\\\"")}\"", ctx.WorkFolder, ct);

        await ctx.SendSystemMessageAsync(
            commitResult.ExitCode == 0
                ? $"커밋 완료: {commitResult.StdOut}"
                : $"커밋 실패: {commitResult.StdErr}");
    }
}

22-8: /mcp — MCP 서버 관리

public class McpCommand : ISlashCommand
{
    public string Name => "mcp";
    public string[] Aliases => Array.Empty<string>();
    public string Description => "MCP 서버 상태 확인 및 활성화/비활성화";
    public string? ArgumentHint => "[enable|disable [서버명]]";
    public bool RequiresActiveSession => false;

    public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct)
    {
        var parts = arguments.Trim().Split(' ', 2, StringSplitOptions.RemoveEmptyEntries);
        var action = parts.Length > 0 ? parts[0].ToLowerInvariant() : "";
        var serverName = parts.Length > 1 ? parts[1] : null;

        if (string.IsNullOrEmpty(action))
        {
            // 상태 표시
            var sb = new StringBuilder("## MCP 서버 상태\n");
            foreach (var client in ctx.McpManager.GetAllClients())
            {
                var status = client.IsConnected ? "🟢 연결됨" : "🔴 끊김";
                sb.AppendLine($"- **{client.ServerName}** {status} ({client.Tools.Count}개 도구)");
            }
            await ctx.SendSystemMessageAsync(sb.ToString());
            return;
        }

        switch (action)
        {
            case "enable":
                if (serverName != null)
                    await ctx.McpManager.EnableServerAsync(serverName, ct);
                else
                    await ctx.McpManager.EnableAllAsync(ct);
                await ctx.SendSystemMessageAsync($"MCP 서버 활성화: {serverName ?? "전체"}");
                break;

            case "disable":
                if (serverName != null)
                    ctx.McpManager.DisableServer(serverName);
                else
                    ctx.McpManager.DisableAll();
                await ctx.SendSystemMessageAsync($"MCP 서버 비활성화: {serverName ?? "전체"}");
                break;

            case "reconnect":
                if (serverName != null)
                    await ctx.McpManager.ReconnectAsync(serverName, ct);
                await ctx.SendSystemMessageAsync($"MCP 서버 재연결: {serverName}");
                break;

            default:
                await ctx.SendSystemMessageAsync("사용법: /mcp [enable|disable|reconnect] [서버명]");
                break;
        }
    }
}

22-9: /permissions, /hooks, /config, /skills, /review, /help

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

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

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

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

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

// /hooks — 훅 설정 표시
public class HooksCommand : ISlashCommand
{
    public string Name => "hooks";
    public string[] Aliases => Array.Empty<string>();
    public string Description => "활성화된 훅 설정을 표시합니다";

    public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct)
    {
        var hooks = ctx.HookRunner.GetAllDefinitions();
        var sb = new StringBuilder("## 활성 훅\n");
        foreach (var (eventName, defs) in hooks)
        {
            sb.AppendLine($"### {eventName}");
            foreach (var def in defs)
            {
                var matcher = string.IsNullOrEmpty(def.Matcher) ? "(전체)" : def.Matcher;
                sb.AppendLine($"- [{def.Type}] {matcher}: `{def.Command ?? def.Prompt ?? def.Url}`");
            }
        }
        await ctx.SendSystemMessageAsync(sb.ToString());
    }
}

// /config — 설정 열기
public class ConfigCommand : ISlashCommand
{
    public string Name => "config";
    public string[] Aliases => new[] { "settings" };
    public string Description => "설정 화면을 엽니다";

    public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct)
    {
        // 인라인 설정 패널 토글 (Phase 21 UI) 또는 SettingsWindow 열기
        ctx.ToggleSettingsPanel();
    }
}

// /skills — 스킬 목록
public class SkillsCommand : ISlashCommand
{
    public string Name => "skills";
    public string[] Aliases => Array.Empty<string>();
    public string Description => "사용 가능한 스킬 목록을 표시합니다";

    public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct)
    {
        var skills = ctx.SkillLoader.GetUserInvocableSkills();
        var sb = new StringBuilder("## 사용 가능한 스킬\n");
        foreach (var skill in skills)
        {
            var hint = string.IsNullOrEmpty(skill.Frontmatter.ArgumentHint)
                ? "" : $" `{skill.Frontmatter.ArgumentHint}`";
            sb.AppendLine($"- **/{skill.Name}**{hint} — {skill.Frontmatter.Description}");
        }
        await ctx.SendSystemMessageAsync(sb.ToString());
    }
}

// /review — PR 리뷰
public class ReviewCommand : ISlashCommand
{
    public string Name => "review";
    public string[] Aliases => Array.Empty<string>();
    public string Description => "AI 코드 리뷰를 실행합니다";
    public string? ArgumentHint => "[PR 번호 또는 diff]";

    public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct)
    {
        // git diff 가져오기
        string diff;
        if (!string.IsNullOrWhiteSpace(arguments) && int.TryParse(arguments.Trim(), out var prNum))
        {
            // GitHub PR diff 가져오기 (gh CLI 사용)
            var result = await ProcessHelper.RunAsync("gh", $"pr diff {prNum}", ctx.WorkFolder, ct);
            diff = result.StdOut;
        }
        else
        {
            // 현재 브랜치 diff
            var result = await ProcessHelper.RunAsync("git",
                "diff main...HEAD", ctx.WorkFolder, ct);
            diff = result.StdOut;
        }

        if (string.IsNullOrWhiteSpace(diff))
        {
            await ctx.SendSystemMessageAsync("리뷰할 변경 사항이 없습니다.");
            return;
        }

        // LLM 코드 리뷰 요청
        var prompt = $"""
            아래 코드 변경 사항을 리뷰하세요:
            1. 변경 사항 개요
            2. 코드 품질 및 스타일
            3. 잠재적 버그 또는 보안 문제
            4. 개선 제안

            ```diff
            {diff[..Math.Min(diff.Length, 12000)]}
            ```
            """;

        var review = await ctx.Llm.GenerateAsync(prompt, ct);
        await ctx.SendSystemMessageAsync($"## 코드 리뷰\n{review}");
    }
}

// /help — 도움말
public class HelpCommand : ISlashCommand
{
    public string Name => "help";
    public string[] Aliases => Array.Empty<string>();
    public string Description => "사용 가능한 명령 목록을 표시합니다";

    public async Task ExecuteAsync(string arguments, IAgentChatContext ctx, CancellationToken ct)
    {
        var registry = ctx.SlashCommandRegistry;
        var sb = new StringBuilder("## AX Agent 명령\n\n");
        foreach (var cmd in registry.GetAll().OrderBy(c => c.Name))
        {
            var hint = cmd.ArgumentHint != null ? $" {cmd.ArgumentHint}" : "";
            var aliases = cmd.Aliases.Length > 0
                ? $" (별칭: {string.Join(", ", cmd.Aliases.Select(a => $"/{a}"))})" : "";
            sb.AppendLine($"- **/{cmd.Name}**{hint} — {cmd.Description}{aliases}");
        }
        sb.AppendLine("\n스킬 명령은 `/skills`로 확인하세요.");
        await ctx.SendSystemMessageAsync(sb.ToString());
    }
}

22-10: 입력 라우팅 통합

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

    /// <summary>사용자 입력을 슬래시 명령 → 스킬 → 일반 메시지 순으로 라우팅.</summary>
    public async Task<InputRouteResult> RouteAsync(string input, IAgentChatContext ctx, CancellationToken ct)
    {
        if (!input.StartsWith("/"))
            return InputRouteResult.Message(input); // 일반 메시지

        var spaceIdx = input.IndexOf(' ');
        var commandPart = spaceIdx > 0 ? input[..spaceIdx] : input;
        var argsPart = spaceIdx > 0 ? input[(spaceIdx + 1)..] : "";

        // 1. 내장 슬래시 명령 확인
        var cmd = _slashCommands.Resolve(commandPart);
        if (cmd != null)
        {
            await cmd.ExecuteAsync(argsPart, ctx, ct);
            return InputRouteResult.Handled;
        }

        // 2. 스킬 검색
        var skillName = commandPart.TrimStart('/');
        var skill = _skillLoader.FindByName(skillName);
        if (skill != null)
            return InputRouteResult.Skill(skill, argsPart);

        // 3. 미인식 → 일반 메시지로 전달
        return InputRouteResult.Message(input);
    }
}

public record InputRouteResult(InputRouteType Type, string? Message = null,
    LoadedSkill? Skill = null, string? SkillArgs = null)
{
    public static InputRouteResult Handled => new(InputRouteType.Handled);
    public static InputRouteResult Message(string msg) => new(InputRouteType.Message, msg);
    public static InputRouteResult Skill(LoadedSkill skill, string args)
        => new(InputRouteType.Skill, Skill: skill, SkillArgs: args);
}

public enum InputRouteType { Handled, Message, Skill }

Phase 19-D 확장 — 스킬 시스템 CC 동등성 보완 (v2.1)

기존 17-D에 없는 3가지 핵심 기능 추가: 인라인 셸 커맨드, 네임스페이싱, 명명된 인수

19-D-EXT-1: 인라인 셸 커맨드 (!backtick`)

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

    /// <summary>
    /// 스킬 본문에서 !`command` 패턴을 찾아 셸 실행 결과로 교체합니다.
    /// 호출 시점: 스킬이 로드된 후, LLM에 프롬프트로 전달하기 직전.
    /// </summary>
    public async Task<string> ProcessAsync(string skillBody, string workFolder, CancellationToken ct)
    {
        var matches = InlineCmdPattern.Matches(skillBody);
        if (matches.Count == 0) return skillBody;

        var result = skillBody;
        foreach (Match match in matches.Reverse()) // 뒤에서부터 교체 (인덱스 유지)
        {
            var command = match.Groups[1].Value;
            var output = await ExecuteCommandAsync(command, workFolder, ct);
            result = result.Remove(match.Index, match.Length)
                          .Insert(match.Index, output);
        }
        return result;
    }

    private async Task<string> ExecuteCommandAsync(string command, string workFolder, CancellationToken ct)
    {
        try
        {
            using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
            cts.CancelAfter(TimeSpan.FromSeconds(10)); // 인라인 명령 타임아웃 10초

            var psi = new ProcessStartInfo
            {
                FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "cmd.exe" : "bash",
                Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
                    ? $"/c {command}" : $"-c \"{command.Replace("\"", "\\\"")}\"",
                WorkingDirectory = workFolder,
                UseShellExecute = false,
                RedirectStandardOutput = true,
                RedirectStandardError = true,
                CreateNoWindow = true,
            };

            using var proc = Process.Start(psi);
            if (proc == null) return "[실행 실패]";

            var stdout = await proc.StandardOutput.ReadToEndAsync(cts.Token);
            await proc.WaitForExitAsync(cts.Token);

            return stdout.Trim();
        }
        catch (OperationCanceledException)
        {
            return "[타임아웃]";
        }
        catch (Exception ex)
        {
            return $"[오류: {ex.Message}]";
        }
    }
}

19-D-EXT-2: 스킬 네임스페이싱 (서브디렉토리 콜론 구분)

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

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

    /// <summary>콜론 구분 이름으로 스킬 검색 (부분 일치 지원).</summary>
    public LoadedSkill? FindByName(string name)
    {
        // 정확한 일치
        var exact = _skills.FirstOrDefault(s =>
            s.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
        if (exact != null) return exact;

        // 마지막 세그먼트 일치 (database:migrate → "migrate"로도 검색 가능)
        var lastSegment = name.Contains(':') ? name : name;
        return _skills.FirstOrDefault(s =>
            s.Name.EndsWith($":{lastSegment}", StringComparison.OrdinalIgnoreCase) ||
            s.Name.Equals(lastSegment, StringComparison.OrdinalIgnoreCase));
    }

    // 디렉토리 스캔 시 재귀적으로 SKILL.md 검색
    private void ScanSkillDirectory(string dir, string baseDir, List<LoadedSkill> results)
    {
        var skillFile = Path.Combine(dir, "SKILL.md");
        if (File.Exists(skillFile))
        {
            var name = ResolveSkillName(dir, baseDir);
            var content = File.ReadAllText(skillFile);
            var (frontmatter, body) = ParseFrontmatter(content);
            results.Add(new LoadedSkill(name, frontmatter, body, skillFile));
        }

        foreach (var subDir in Directory.GetDirectories(dir))
            ScanSkillDirectory(subDir, baseDir, results);
    }
}

19-D-EXT-3: 명명된 인수 ($name 구문)

// SkillArgumentSubstitution.cs
public static class SkillArgumentSubstitution
{
    /// <summary>
    /// 스킬 본문에서 $ARGUMENTS와 명명된 인수를 치환합니다.
    /// - $ARGUMENTS → 전체 인수 문자열
    /// - $name → 프론트매터 arguments: [name, directory] 기반 위치 매핑
    /// - {0}, {1} → 인덱스 기반 인수
    /// </summary>
    public static string Substitute(string body, SkillFrontmatter frontmatter, string userInput)
    {
        var result = body;

        // 1. $ARGUMENTS → 전체 인수
        result = result.Replace("$ARGUMENTS", userInput);

        // 2. 명명된 인수 (프론트매터에 arguments가 정의된 경우)
        if (frontmatter.Arguments.Count > 0)
        {
            var parts = SplitArguments(userInput);
            for (var i = 0; i < frontmatter.Arguments.Count; i++)
            {
                var argName = frontmatter.Arguments[i];
                var argValue = i < parts.Count ? parts[i] : "";
                result = result.Replace($"${argName}", argValue);
            }
        }

        // 3. 인덱스 기반 인수 ({0}, {1}, ...)
        var indexParts = SplitArguments(userInput);
        for (var i = 0; i < indexParts.Count; i++)
            result = result.Replace($"{{{i}}}", indexParts[i]);

        return result;
    }

    private static List<string> SplitArguments(string input)
    {
        // 따옴표로 감싼 인수 지원: "hello world" foo bar
        var parts = new List<string>();
        var current = new StringBuilder();
        var inQuote = false;

        foreach (var ch in input)
        {
            if (ch == '"') { inQuote = !inQuote; continue; }
            if (ch == ' ' && !inQuote && current.Length > 0)
            {
                parts.Add(current.ToString());
                current.Clear();
                continue;
            }
            current.Append(ch);
        }
        if (current.Length > 0) parts.Add(current.ToString());
        return parts;
    }
}

19-D-EXT-4: SkillExecutionPipeline 통합

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

    public async Task<SkillResult> ExecuteAsync(
        LoadedSkill skill, string userInput, string workFolder,
        AgentLoopService agentLoop, CancellationToken ct)
    {
        // 1. PreSkillExecute 훅
        await _hookRunner.RunAsync(HookEvent.PreSkillExecute, new HookContext
        {
            Event = HookEvent.PreSkillExecute,
            ToolName = skill.Name,
            UserMessage = userInput,
        }, ct);

        // 2. 인수 치환 ($ARGUMENTS, $name, {0})
        var body = SkillArgumentSubstitution.Substitute(
            skill.Body, skill.Frontmatter, userInput);

        // 3. 인라인 셸 명령 실행 (!`command`)
        body = await _inlineCmd.ProcessAsync(body, workFolder, ct);

        // 4. 모델 오버라이드 확인
        var model = skill.Frontmatter.Model;

        // 5. 컨텍스트 분기 여부 확인
        if (skill.Frontmatter.Context == SkillContext.Fork)
        {
            var result = await _forkRunner.ExecuteInForkAsync(skill, body, ct);
            // PostSkillExecute 훅
            await _hookRunner.RunAsync(HookEvent.PostSkillExecute, new HookContext
            {
                Event = HookEvent.PostSkillExecute,
                ToolName = skill.Name,
                ToolOutput = result.Output,
            }, ct);
            return result;
        }

        // 6. 메인 컨텍스트에서 실행 (body를 시스템 프롬프트에 주입)
        return new SkillResult
        {
            IsSuccess = true,
            Output = body, // AgentLoopService가 이를 프롬프트로 사용
        };
    }
}

Phase 19-B 확장 — 복합 Bash 명령 파싱 (v2.1)

기존 19-B에 누락된 복합 명령 파싱 상세 구현

19-B-EXT-1: CompoundCommandParser

// 복합 Bash 명령을 서브커맨드로 분리하여 각각 권한 검사
public class CompoundCommandParser
{
    // &&, ||, ;, | 로 분리 (따옴표, 서브셸 내부는 분리하지 않음)
    public static IReadOnlyList<string> Parse(string command)
    {
        var result = new List<string>();
        var current = new StringBuilder();
        var inSingleQuote = false;
        var inDoubleQuote = false;
        var parenDepth = 0;

        for (var i = 0; i < command.Length; i++)
        {
            var ch = command[i];
            var next = i + 1 < command.Length ? command[i + 1] : '\0';

            // 따옴표 토글
            if (ch == '\'' && !inDoubleQuote) { inSingleQuote = !inSingleQuote; current.Append(ch); continue; }
            if (ch == '"' && !inSingleQuote) { inDoubleQuote = !inDoubleQuote; current.Append(ch); continue; }

            // 서브셸
            if (!inSingleQuote && !inDoubleQuote)
            {
                if (ch == '(') parenDepth++;
                if (ch == ')') parenDepth--;
            }

            // 분리자 검출 (따옴표/서브셸 밖에서만)
            if (!inSingleQuote && !inDoubleQuote && parenDepth == 0)
            {
                if ((ch == '&' && next == '&') || (ch == '|' && next == '|'))
                {
                    if (current.Length > 0) result.Add(current.ToString().Trim());
                    current.Clear();
                    i++; // 두 글자 연산자 건너뜀
                    continue;
                }
                if (ch == ';' || ch == '|')
                {
                    if (current.Length > 0) result.Add(current.ToString().Trim());
                    current.Clear();
                    continue;
                }
            }

            current.Append(ch);
        }

        if (current.Length > 0) result.Add(current.ToString().Trim());
        return result;
    }
}

// PermissionEvaluator 확장
public class PermissionEvaluator
{
    private readonly IReadOnlyList<PermissionRule> _allowRules;
    private readonly IReadOnlyList<PermissionRule> _denyRules;

    /// <summary>
    /// 복합 명령의 각 서브커맨드를 독립적으로 검사합니다.
    /// 하나의 서브커맨드라도 deny되면 전체 명령이 차단됩니다.
    /// </summary>
    public PermissionDecision EvaluateCompound(string toolName, string command)
    {
        if (toolName != "bash" && toolName != "process")
            return EvaluateSingle(toolName, command);

        var subCommands = CompoundCommandParser.Parse(command);
        foreach (var sub in subCommands)
        {
            var decision = EvaluateSingle(toolName, sub.Trim());
            if (decision == PermissionDecision.Deny)
                return PermissionDecision.Deny;
        }

        // 모든 서브커맨드가 allow이면 allow, 아니면 ask
        return subCommands.All(sub => EvaluateSingle(toolName, sub) == PermissionDecision.Allow)
            ? PermissionDecision.Allow
            : PermissionDecision.Ask;
    }

    // 항상 차단되는 위험 패턴
    private static readonly string[] AlwaysEscalate = new[]
    {
        "sed -i",              // 파일 인플레이스 편집
        "rm -rf /",            // 루트 삭제
        "chmod 777",           // 위험한 권한
        "> ~/.bashrc",         // 셸 설정 변경
        "> ~/.zshrc",
        ".claude/",            // 설정 디렉토리 조작
        ".git/",               // git 내부 조작
    };
}

Phase 17-C 확장 — 훅 환경변수 시스템 (v2.1)

CC의 훅 환경변수 체계를 구현: $CLAUDE_FILE_PATH, $CLAUDE_TOOL_NAME 등

17-C-EXT-1: HookEnvironmentBuilder

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

        // 공통 변수
        env["AX_SESSION_ID"] = context.SessionId;
        env["AX_HOOK_EVENT"] = context.Event.ToString();
        env["AX_CWD"] = context.WorkFolder ?? "";
        env["AX_PERMISSION_MODE"] = context.PermissionMode ?? "default";

        // 도구 관련 변수 (PreToolUse, PostToolUse)
        if (!string.IsNullOrEmpty(context.ToolName))
        {
            env["AX_TOOL_NAME"] = context.ToolName;
            env["CLAUDE_TOOL_NAME"] = context.ToolName; // CC 호환
        }
        if (!string.IsNullOrEmpty(context.ToolInput))
        {
            env["AX_TOOL_INPUT"] = context.ToolInput;
            env["CLAUDE_TOOL_INPUT"] = context.ToolInput; // CC 호환
        }

        // 파일 경로 변수 (Write, Edit 도구 시)
        if (!string.IsNullOrEmpty(context.FilePath))
        {
            env["AX_FILE_PATH"] = context.FilePath;
            env["CLAUDE_FILE_PATH"] = context.FilePath; // CC 호환
        }

        // 파일 변경 변수 (FileChanged)
        if (!string.IsNullOrEmpty(context.ChangedFilePath))
            env["AX_CHANGED_FILE"] = context.ChangedFilePath;

        // CwdChanged 전용: CLAUDE_ENV_FILE
        if (context.Event == HookEvent.CwdChanged)
        {
            var envFile = Path.Combine(Path.GetTempPath(), "AxCopilot",
                $"env_{context.SessionId}.txt");
            Directory.CreateDirectory(Path.GetDirectoryName(envFile)!);
            env["CLAUDE_ENV_FILE"] = envFile;
            env["AX_ENV_FILE"] = envFile;
        }

        // 사용자 프롬프트 (UserPromptSubmit)
        if (!string.IsNullOrEmpty(context.UserMessage))
            env["AX_USER_PROMPT"] = context.UserMessage;

        return env;
    }
}

// CommandHookExecutor에서 환경변수 주입
public class CommandHookExecutor : IHookExecutor
{
    public async Task<HookResult> ExecuteAsync(HookContext context, CancellationToken ct)
    {
        var psi = new ProcessStartInfo { ... };

        // 환경변수 주입
        foreach (var (key, value) in HookEnvironmentBuilder.Build(context))
            psi.Environment[key] = value;

        // stdin에 JSON 입력 전달 (CC 호환)
        var inputJson = JsonSerializer.Serialize(context, _jsonOpts);
        // ... 프로세스 실행, stdin에 inputJson 쓰기 ...
    }
}

Phase 23 — 최종 CC 동등성 검증 + 누락 기능 (v2.1) 완료

목표: Phase 19~22 이후 남은 소규모 갭을 해소하고, 전체 기능 매트릭스 검증

구현 완료 (2026-04-03): AutoCompactMonitor, IConditionalTool + ToolEnvironmentContext, SlashAutoCompleteProvider. 또한 Phase 19-D-EXT(SkillArgumentSubstitution, SkillInlineCommandProcessor → SkillService 통합), Phase 19-B-EXT(CompoundCommandParser → PermissionDecisionService 통합), Phase 17-C-EXT(HookEnvironmentBuilder → ExtendedHookRunner 통합) 모두 완료.

23-1: argument-hint 자동완성 UI

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

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

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

        // 내장 명령
        foreach (var cmd in _commands.GetAll())
        {
            if (cmd.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
                results.Add(new AutoCompleteItem($"/{cmd.Name}", cmd.Description, cmd.ArgumentHint));
        }

        // 스킬
        foreach (var skill in _skills.GetUserInvocableSkills())
        {
            if (skill.Name.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
                results.Add(new AutoCompleteItem($"/{skill.Name}",
                    skill.Frontmatter.Description, skill.Frontmatter.ArgumentHint));
        }

        return results.OrderBy(r => r.Name).ToList();
    }
}

public record AutoCompleteItem(string Name, string Description, string? ArgumentHint);

23-2: 세션 /compact 자동 트리거

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

    /// <summary>매 에이전트 반복 후 호출 → 컨텍스트가 임계치 초과 시 자동 컴팩션.</summary>
    public async Task<bool> CheckAndCompactAsync(
        AgentLoopService agentLoop, LlmService llm, CancellationToken ct)
    {
        var usage = _tokenEstimator.EstimateTokenCount(agentLoop.GetCurrentMessages());
        var maxTokens = agentLoop.CurrentModelMaxTokens;
        var usagePercent = (int)((double)usage / maxTokens * 100);

        if (usagePercent < _thresholdPercent) return false;

        LogService.Info($"컨텍스트 {usagePercent}% 사용 → 자동 컴팩션 시작");

        // CompactCommand와 동일한 로직 (DRY: 공유 서비스로 분리)
        await CompactionService.CompactAsync(agentLoop, llm, instruction: null, ct);
        return true;
    }
}

// 설정 추가
public class LlmSettings
{
    [JsonPropertyName("auto_compact_enabled")]
    public bool AutoCompactEnabled { get; set; } = true;

    [JsonPropertyName("auto_compact_threshold_percent")]
    public int AutoCompactThresholdPercent { get; set; } = 80;
}

23-3: 도구별 isEnabled() 자가 비활성화

// IAgentTool 인터페이스 확장 (ISP 준수: 옵션 인터페이스)
public interface IConditionalTool
{
    /// <summary>현재 환경에서 이 도구가 사용 가능한지 확인합니다.</summary>
    bool IsEnabled(ToolEnvironmentContext env);
}

public record ToolEnvironmentContext(
    AppSettings Settings,
    string ActiveTab,         // "Chat" | "Cowork" | "Code"
    string? WorkFolder,
    bool HasGitRepo,
    bool HasNodeRuntime,
    bool HasPythonRuntime);

// ToolRegistry에서 도구 목록 생성 시 필터링
public class ToolRegistry
{
    public IReadOnlyList<IAgentTool> GetActiveTools(ToolEnvironmentContext env)
    {
        return _tools.Where(t =>
        {
            if (t is IConditionalTool conditional)
                return conditional.IsEnabled(env);
            return true; // 조건 없는 도구는 항상 활성
        }).ToList();
    }
}

23-4: CC 기능 매트릭스 최종 검증표

CC 기능 AX Phase 상태
4-layer memory (managed/user/project/local) 19-A Phase 22 구현
@include 5단계 기존 구현됨
rules/*.md + paths frontmatter 기존 구현됨
18종 훅 이벤트 25 Phase 25 구현 (PostToolUseFailure, Stop 포함)
4종 훅 타입 (command/http/prompt/agent) 25 Phase 25 구현
훅 환경변수 ($CLAUDE_FILE_PATH 등) 25 Phase 25 HookEnvironmentBuilder
훅 if 조건부 실행 25 Phase 25 EvaluateCondition (==, !=, contains, &&)
훅 once/async/asyncRewake 25 Phase 25 AsyncRewakeTriggered 이벤트
스킬 $ARGUMENTS 치환 24 Phase 24 SkillArgumentSubstitution
스킬 인라인 셸 실행 24 Phase 24 SkillInlineCommandProcessor
스킬 네임스페이싱 (colon) 24 Phase 24 LoadSkillsRecursive
스킬 명명된 인수 ($name) 24 Phase 24 SkillDefinition.Arguments
스킬 context:fork 24 Phase 24 IsForkContext frontmatter
스킬 paths 자동 활성화 24 Phase 24 PathBasedSkillActivator
스킬 model 오버라이드 24 Phase 24 ModelOverride frontmatter
스킬 user-invocable 24 Phase 24 UserInvocable 필터링
스킬 범위 훅 24 Phase 24 ScopedHooks frontmatter
4종 권한 모드 (default/acceptEdits/plan/bypass) 22 Phase 22 구현
복합 Bash 명령 파싱 (&&/||/;/|) 22 Phase 22 CompoundCommandParser
glob 패턴 권한 매칭 (Bash(git *)) 기존 구현됨
MCP 도구 차단 (mcp__server 규칙) 기존 구현됨
도구 가시성 필터링 기존 구현됨
슬래시 명령 14종 22 + 26 Phase 22/26 SlashCommandRegistry
출력 예산 + 스필오버 27 Phase 27 ToolResultSizer
세션 fork/resume/tag 28 Phase 28 SessionManager
자동 컴팩션 29-A Phase 29 AutoCompactMonitor 통합
도구 isEnabled() 자가 비활성화 29-B Phase 29 IConditionalTool + ToolEnvironmentContext
/init 프로젝트 초기화 26 Phase 26 InitCommand
@include AxMd 지시어 27 Phase 27 AxMdIncludeResolver
멀티에이전트 + worktree 기존 SubAgentTool + DelegateAgentTool + WorktreeManager
에이전트 타입별 메모리 기존 AgentTypeMemoryRepository + MemoryTool + /memory 명령
4-layer 계층 메모리 통합 조회 30-C Phase 30 HierarchicalMemoryService
SDK 제어 프로토콜 (JSON 스트리밍) 31 Phase 31 AgentSdkServer + SdkProtocol
커스텀 에이전트 정의 (SDK) 31-C Phase 31 CustomAgentDefinition + AgentTypeRegistry
SDK 훅 콜백 (외부 권한 핸들링) 31-D Phase 31 SdkHookCallbackHandler
3-Pane Claude.ai UI (사이드바+헤더+설정 패널+입력) 32 Phase 32 AgentSettingsPanel/SidebarView/HeaderBar/InputArea

구현 의존성 그래프 (Phase 19+ 갱신)

[19-A] HierarchicalMemory ─────────────────────────────────────────┐
      ↓                                                              │
[19-B] PermissionPatternMatch ← [19-B-EXT] CompoundParsing          │
      ↓                                                              ↓
[19-C] HookEvents ← [17-C-EXT] HookEnvVars                         │
      ↓                                                              │
[19-D] SkillSystem ← [19-D-EXT] InlineCmd + Namespace + NamedArgs  │
      ↓                              ↓                               │
[19-E] SessionManager ──────────────────────────────────────────────┘
      ↓
[19-F] OutputBudget
      ↓
[19-G] /init ← [22] SlashCommands (15종) ← [23] AutoCompact + isEnabled
                     ↓
[20-A] SDK ──→ [20-B] CustomAgent ──→ [20-C] SDK 훅 콜백
                                                ↓
                                         [21-1~7] UI 전면 개편

버전별 출시 계획 (갱신)

버전 코드명 포함 Phase 핵심 신규 클래스 수
v1.8.0 에이전트 인프라 17-UI, 17-A~G ~35개 신규 클래스 완료
v1.8.1 인프라 안정화 버그픽스 + 17-* 마이너
v2.0 에이전트 팀 18-A~C, L3 ~25개 신규 클래스 완료
v2.1 CC 완전 동등성 19-A~G + 19-EXT + 22 + 23 ~50개 신규 클래스
v2.2 SDK 프로토콜 20-A~C (임베딩 + 외부 연동) ~15개 신규 클래스
v2.3 AX Agent UI 전면 개편 21 (Claude.ai+Codex 레이아웃) UI 계층 60% 재작성
v3.0 크로스플랫폼 LP-1~3 (Avalonia) UI 계층 40% 재작성

v2.1 세부 구현 순서 (권장)

Week 1-2: [19-A] 4-layer Memory + [19-B + EXT] Permission + CompoundParsing
Week 3:   [17-C-EXT] HookEnvVars + [19-C] HookEvents 완성
Week 4:   [19-D-EXT] Skill InlineCmd + Namespace + NamedArgs
Week 5:   [22] SlashCommands 16종 (ISlashCommand + Registry + 구현체)
Week 6:   [19-E] SessionManager (fork/resume/tag)
Week 7:   [19-F] OutputBudget + Spillover + [19-G] /init
Week 8:   [23] AutoCompact + isEnabled + 최종 검증


Phase 24 — 스킬 고급 기능 (v2.1) 완료

목표: CC 스킬 시스템의 나머지 갭 해소 — 네임스페이싱, 고급 프론트매터, 실행 격리.

구현 완료 (2026-04-03):

24-A: 콜론 네임스페이싱

  • SkillService.LoadSkillsRecursive() — 하위 디렉토리를 재귀 탐색, database/migrate/SKILL.mddatabase:migrate 자동 변환
  • 중간 디렉토리(SKILL.md 없음)는 건너뛰고 더 깊이 탐색

24-B: 고급 프론트매터 필드

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

24-C: SkillDefinition 확장

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

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

목표: 훅 조건부 실행, asyncRewake, 누락 이벤트 추가. 도구 결과 오버플로우 방지.

구현 완료 (2026-04-03):

25-A: Hook if 조건부 실행

  • EvaluateCondition() 완전 구현 — "field == value", "field != value", "field contains value" 지원
  • && 연결로 복합 조건 지원
  • 지원 필드: tool_name, event, session_id, work_folder, user_message, changed_file

25-B: Hook asyncRewake + 이벤트 확장

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

25-C: 도구 결과 크기 제한 (ToolResultSizer)

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

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

목표: CC의 /init 명령 동등 — 코드베이스 분석 → AX.md 자동 생성.

구현 완료 (2026-04-03):

InitCommand (/init)

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


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

목표: 이미 정의되었으나 미사용 상태인 인프라(ToolResultSizer, PathBasedSkillActivator, AxMdIncludeResolver)를 실제 코드에 통합.

구현 완료 (2026-04-03):

27-A: ToolResultSizer → AgentLoopService 통합

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

27-B: PathBasedSkillActivator → 시스템 프롬프트 통합

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

27-C: AxMdIncludeResolver → AX.md 로딩 통합

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

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

목표: CC의 세션 관리(save/resume/fork/tag)와 동등한 세션 영속성 시스템.

구현 완료 (2026-04-03):

SessionManager

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

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

목표: Phase 22~28에서 생성된 클래스들을 실제 코드 경로에 연결하고, CC 기능 매트릭스를 검증.

구현 완료 (2026-04-03):

29-A: AutoCompactMonitor → AgentLoopService 통합

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

29-B: IConditionalTool + ToolEnvironmentContext 통합

  • AgentLoopService line 339: ToolEnvironmentContext 빌드 후 GetActiveTools(disabled, env) 오버로드 사용
  • .git 폴더 존재 여부로 HasGitRepo 자동 감지
  • GitToolIConditionalTool 구현 (HasGitRepo=false 시 도구 목록에서 자동 제외)

29-C: SessionManager → AgentLoopService 통합

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

29-D: CC 기능 매트릭스 상태 갱신

  • 33개 기능 항목 중 29개 구현 완료, 4개 📋 계획 (SDK, 멀티에이전트, 에이전트 메모리, 3-Pane UI)
  • Phase 22~29 구현 결과 반영

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

목표: DIP 원칙 적용, CC 4-layer 계층 메모리 통합, 기존 구현 누락 항목 매트릭스 반영.

구현 완료 (2026-04-03):

30-A: CC 기능 매트릭스 재갱신

  • 기존 멀티에이전트(SubAgentTool, DelegateAgentTool, WorktreeManager) 및 에이전트 메모리(AgentTypeMemoryRepository, MemoryTool, /memory) 확인 → 반영
  • 35개 항목 중 33개 구현 완료, 2개 📋 계획 (SDK, 3-Pane UI)

30-B: IAgentMemoryRepository 인터페이스 추출

  • IAgentMemoryRepository 인터페이스 신규 — LoadMemoryAsync, SaveMemoryAsync, AppendLearnAsync, GetAgentTypes, DeleteMemory
  • AgentTypeMemoryRepositoryIAgentMemoryRepository 구현
  • DelegateAgentTool → 구체 클래스 대신 IAgentMemoryRepository 인터페이스에 의존 (DIP)

30-C: HierarchicalMemoryService — 4-layer 메모리 통합 조회

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

30-D: DelegateAgentTool에 IConditionalTool 적용

  • AI 비활성화(AiEnabled=false) 시 delegate 도구 자동 비활성화

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

목표: CC의 JSON 스트리밍 SDK와 동등한 프로그래밍 방식 에이전트 제어 프로토콜. 외부 IDE, 스크립트, CI/CD에서 AX Agent를 stdin/stdout JSON으로 제어.

구현 완료 (2026-04-03):

31-A: SDK 메시지 타입 정의 (Services/Agent/Sdk/SdkProtocol.cs)

  • SdkControlRequestType — 9종: Initialize, UserMessage, SetPermissionMode, SetModel, Interrupt, GetContextUsage, RewindFiles, CanUseToolResponse, Terminate
  • SdkControlRequest — Host → CLI 요청 (Type + Id + Payload)
  • SdkMessageType — 11종: InitializeResult, AssistantToken, AssistantMessage, ToolUseStart, ToolProgress, ToolResult, SessionResult, HookEvent, CanUseTool, Notification, Error
  • SdkMessage — CLI → Host 응답/이벤트
  • 데이터 클래스: SdkInitializePayload, SdkUserMessagePayload, SdkTokenData, SdkToolUseStartData, SdkToolResultData, SdkSessionResultData, SdkCanUseToolData, SdkErrorData

31-B: AgentSdkServer (Services/Agent/Sdk/AgentSdkServer.cs)

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

31-C: CustomAgentDefinition + AgentTypeRegistry (Services/Agent/Sdk/AgentTypeRegistry.cs)

  • CustomAgentDefinition — name, description, prompt, model, allowedTools, disallowedTools, maxTurns, permissionMode
  • AgentTypeRegistry — CRUD + 목록 조회, Initialize 시 등록

31-D: SdkHookCallbackHandler (Services/Agent/Sdk/SdkHookCallbackHandler.cs)

  • 도구 사용 허가 요청 → Host로 CanUseTool 전송 → 30초 타임아웃 대기 → Allow/Deny
  • 훅 이벤트 알림 → 등록된 이벤트만 Host에 전달
  • TaskCompletionSource 기반 비동기 응답 대기

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

목표: CC의 Claude.ai + Codex 레이아웃과 동등한 3-Pane UI 구조를 수립. 새 UserControl 컴포넌트 4종 + ChatWindow 통합.

구현 완료 (2026-04-03):

32-A: AgentSettingsPanel (Views/Controls/AgentSettingsPanel.xaml/.cs)

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

32-B: AgentSidebarView (Views/Controls/AgentSidebarView.xaml/.cs)

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

32-C: AgentSessionHeaderBar (Views/Controls/AgentSessionHeaderBar.xaml/.cs)

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

32-D: AgentInputArea (Views/Controls/AgentInputArea.xaml/.cs)

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

32-E: ChatWindow.xaml 3-Pane 통합

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

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

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

33-A: AgentLoopService — 매직 넘버 상수화

  • Defaults 내부 정적 클래스 신설 (16개 상수)
  • MaxIterations, MaxRetryOnError, MaxTestFixIterations, MaxPlanRegenerationRetries 등
  • ToolCallThresholdForExpansion, ContextCompressionTargetRatio, AutoCompactThresholdPercent
  • QueryNameMaxLength, QueryTitleMaxLength, ThinkingTextMaxLength, VerificationSummaryMaxLength 등

33-B: AgentLoopService — RunAsync 분해 (ProcessSingleToolCallAsync 추출)

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

33-C: AutoCompactMonitor + ContextCondenser 통합

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

33-D: SessionManager DI 기반 개선

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

33-E: ChatWindow — ThemeResourceHelper 통합 적용

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

33-F: ActiveTab 레이스 컨디션 + 에러 핸들링 정리

  • activeTabSnapshot 캡처 — RunAsync 진입 시 스냅샷, 루프 전체에서 일관 사용
  • BuildContext(tabOverride) — 스냅샷 기반 컨텍스트 생성
  • RunPostToolVerificationAsync: ActiveTabcontext.ActiveTab 참조
  • ExecuteToolsInParallelAsync: 감사 로그 ActiveTabcontext.ActiveTab
  • bare catch {} 5건 → catch (Exception ex) { LogService.Warn(...) } 패턴으로 교체
  • bare catch { break; } → 오류 메시지 로깅 추가

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

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

34-A: SendMessageAsync Cowork/Code 중복 제거

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

34-B: ThemeResourceHelper 완전 적용

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

34-C: PopupMenuHelper 정적 헬퍼 생성 (Views/PopupMenuHelper.cs)

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

34-D: AgentLoopService 잔여 정리

  • 마지막 bare catch {}catch (Exception) + 주석 명시 (document_plan JSON 파싱)

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

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

35-A: Bare catch 전량 정리 (109개 파일)

  • catch { }catch { return X; }모든 bare catchcatch (Exception) + 상황별 주석
  • 전체 코드베이스 109개 .cs 파일에 PowerShell 일괄 치환 적용
  • 빈 catch, return 포함 catch, 값 할당 catch 등 모든 변형 포함

35-B: ColorConverter.ConvertFromString 헬퍼 추출

  • ThemeResourceHelper.HexBrush(hex) / HexColor(hex) 정적 메서드 추가
  • ChatWindow.xaml.cs 47건 new SolidColorBrush((Color)ColorConverter.ConvertFromString(...))ThemeResourceHelper.HexBrush(...) 일괄 치환
  • SettingsWindow, ShortcutHelpWindow, HelpDetailWindow, StatisticsViewModel 등 7개 추가 파일 34건 치환
  • 81건 장황한 색상 변환 패턴 제거

35-C: ChatWindow _settings.Settings.Llm 체인 액세스 캐싱

  • private LlmSettings Llm => _settings.Settings.Llm; 편의 프로퍼티 추가
  • 92개 _settings.Settings.Llm. 체인 → Llm. 단축 치환
  • 가독성 향상 + 향후 리팩터링 시 단일 변경 지점 확보

35-D: AgentContext.Settings 주입 — 서비스 로케이터 제거

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

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

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

36-A: Application.Current as AppCurrentApp 정적 프로퍼티 일원화

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

36-B: PopupMenuHelper 실제 적용 (ChatWindow 3개 팝업)

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

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

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

분할 결과

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

핵심 성과

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

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

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

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

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

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

FontFamily 캐싱 (25개 파일)

ThemeResourceHelper에 5개 정적 필드 추가:

  • SegoeMdl2new FontFamily("Segoe MDL2 Assets") (기존)
  • Consolasnew FontFamily("Consolas") (기존)
  • CascadiaCodenew FontFamily("Cascadia Code, Consolas, monospace") (신규)
  • ConsolasCodenew FontFamily("Consolas, Cascadia Code, Segoe UI") (신규)
  • ConsolasCourierNewnew FontFamily("Consolas, Courier New") (신규)

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

LauncherWindow 파셜 분할

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

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.VisibilitySidebar.Visibility. PermissionHeaderChip_Click(): PermissionHeaderChipSessionHeaderBar.
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 생성자: SkillManagerToolRunSkillInForkAsync fork runner 주입.
AgentLoopService.Execution.cs 도구 성공 후 InjectPathBasedSkills(result.FilePath, messages) 호출.

구현 세부사항

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

최종 업데이트: 2026-04-04 (Phase 2252 + Phase 17-UI-AE + Phase 17-A~D 구현 완료)