AX Agent 도구·스킬 정합성 재구성 및 실행 품질 보강

변경 목적:
- AX Agent의 도구 이름, 내부 설정, 스킬 정책, 실행 루프 사이의 불일치를 줄이고 전체 동작 품질을 높인다.
- claw-code 수준의 일관된 동작 품질을 참고하되 AX 구조에 맞는 고유한 카탈로그·정규화 레이어로 재구성한다.

핵심 수정사항:
- 도구 canonical id, legacy alias, 탭 노출, 설정 카테고리, read-only 분류를 중앙 카탈로그로 통합했다.
- ToolRegistry, AgentLoopService, 병렬 실행 분류, 권한 처리, 훅 처리, 스킬 allowed-tools 해석이 같은 이름 체계를 사용하도록 정리했다.
- Agent 설정/일반 설정/도움말의 도구 카드와 훅 편집기, 스킬 설명을 현재 런타임 구조에 맞게 갱신했다.
- 컨텍스트 압축, intent gate, spawn agents, session learning, model prompt adapter, workspace context 관련 변경과 테스트 추가를 함께 반영했다.
- 문서 이력과 비교/로드맵 문서를 최신 상태로 갱신했다.

검증 결과:
- dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\verify_toolcat\ -p:IntermediateOutputPath=obj\verify_toolcat\ : 경고 0 / 오류 0
- dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter AgentToolCatalogTests -p:OutputPath=bin\verify_toolcat_tests\ -p:IntermediateOutputPath=obj\verify_toolcat_tests\ : 통과 8
This commit is contained in:
2026-04-14 17:52:46 +09:00
parent fa33b98f7e
commit 8cb08576d5
200 changed files with 13522 additions and 5764 deletions
+7
View File
@@ -7,6 +7,13 @@ Windows 전용 시맨틱 런처 & 워크스페이스 매니저
개발 참고: Claw Code 동등성 작업 추적 문서
`docs/claw-code-parity-plan.md`
- 업데이트: 2026-04-14 17:46 (KST)
- 도구 이름과 내부설정의 결합도를 낮추기 위해 `AgentToolCatalog` 기반의 중앙 메타데이터 레이어를 추가했습니다. 이제 레거시 도구 이름(`git`, `lsp`, `zip`, `project_rule` 등)은 canonical id로 정규화되어 런타임, 권한, 병렬 분류, 설정 화면이 같은 이름 체계를 사용합니다.
- 설정 화면도 같은 카탈로그를 보도록 정리했습니다. Agent 설정/일반 설정의 도구 카드와 훅 편집기는 canonical 이름을 기준으로 표시하고 저장하며, 기존 `disabledTools`, 훅 대상 도구, 도구 권한 키는 호환 alias를 통해 자동 흡수합니다.
- 스킬/헬프 설명도 현재 구조에 맞게 완화했습니다. 스킬은 기본 폴더와 추가 폴더를 함께 로드하는 흐름으로 설명을 갱신했고, 직접 호출 스킬과 런타임 정책 연결 스킬을 같이 보여주도록 문구를 다듬었습니다.
- 검증: `dotnet build src/AxCopilot/AxCopilot.csproj -c Release -v minimal -p:OutputPath=bin\\verify_toolcat\\ -p:IntermediateOutputPath=obj\\verify_toolcat\\` 경고 0 / 오류 0
- 검증: `dotnet test src/AxCopilot.Tests/AxCopilot.Tests.csproj -c Release -v minimal --filter AgentToolCatalogTests -p:OutputPath=bin\\verify_toolcat_tests\\ -p:IntermediateOutputPath=obj\\verify_toolcat_tests\\` 통과 8
- 업데이트: 2026-04-12 23:58 (KST)
- Cowork 문서 생성이 단조로워지고 마지막 요약이 약해진 부분을 다시 보강했습니다. 문서 태스크 가이드에 반복적인 동일 패턴 섹션 작성을 피하고, 목적에 따라 요약/핵심 발견/비교표/타임라인/권고안/부록 같은 richer section pattern을 쓰도록 다시 유도했습니다.
- Cowork 시스템 프롬프트도 같은 기준으로 조정했습니다. 새 문서 생성 시 filler paragraph 대신 bullets, tables, structured comparison을 적극적으로 쓰게 하고, 최종 응답에는 생성 후 점검 항목까지 포함한 구조화 요약을 남기도록 복원했습니다.
+8 -8
View File
@@ -162,35 +162,35 @@ del /q "%~1\*.runtimeconfig.json" 2>nul
exit /b 0
:fail_dist
echo [FAILED] dist ??€????밴쉐 ??쎈솭
echo [FAILED] dist 폴더 생성 실패
goto :end_fail
:fail_app
echo [FAILED] main app publish ??쎈솭
echo [FAILED] main app publish 실패
goto :end_fail
:fail_obfuscation
echo [FAILED] obfuscation ??m€???쎈솭
echo [FAILED] obfuscation 단계 실패
goto :end_fail
:fail_encryptor
echo [FAILED] AxKeyEncryptor publish ??쎈솭
echo [FAILED] AxKeyEncryptor publish 실패
goto :end_fail
:fail_payload
echo [FAILED] payload.zip ??밴쉐 ??쎈솭
echo [FAILED] payload.zip 생성 실패
goto :end_fail
:fail_installer
echo [FAILED] installer build ??쎈솭
echo [FAILED] installer build 실패
goto :end_fail
:fail_installer_copy
echo [FAILED] installer exe 癰귣벊沅???쎈솭
echo [FAILED] installer exe 복사 실패
goto :end_fail
:fail_running
echo [FAILED] running AX Copilot process could not be stopped cleanly
echo [FAILED] 실행 중인 AX Copilot 프로세스를 종료할 수 없습니다
goto :end_fail
:end_fail
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 519 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 615 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 553 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 247 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 555 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 488 B

Binary file not shown.
Binary file not shown.
BIN
View File
Binary file not shown.
-95
View File
@@ -1,95 +0,0 @@
---
name: adr-writer
label: 아키텍처 결정 기록 (ADR)
description: 아키텍처 결정 사항을 표준 ADR 형식으로 문서화합니다.
icon: \uE82D
allowed-tools:
- file_read
- file_write
- folder_map
- grep
- search_codebase
tabs: code
---
아키텍처 결정 사항을 ADR(Architecture Decision Record) 형식으로 문서화하세요.
## ADR이란?
소프트웨어 아키텍처에서 내린 중요한 결정의 배경, 대안, 근거를 기록하는 경량 문서입니다.
미래의 팀원이 "왜 이렇게 결정했는지"를 이해할 수 있게 합니다.
## 워크플로우
1. **결정 사항 확인**: 사용자에게 다음을 파악
- 어떤 결정을 내렸는가 (또는 내려야 하는가)
- 관련 코드/시스템 영역
- 고려한 대안들
2. **코드 분석** (선택): 관련 코드 구조를 읽어 현재 상태 파악
3. **ADR 작성**: 표준 형식으로 문서 생성
4. **파일 저장**: `docs/adr/` 폴더에 번호 형식으로 저장
## ADR 표준 형식
```markdown
# ADR-[번호]: [결정 제목]
**상태**: 제안됨 | 승인됨 | 폐기됨 | 대체됨
**날짜**: YYYY-MM-DD
**결정자**: [이름/팀]
## 맥락 (Context)
어떤 상황에서 이 결정이 필요한가?
- 기술적 배경
- 비즈니스 요구사항
- 제약 조건
## 결정 (Decision)
무엇을 결정했는가?
- 선택한 방안의 구체적 내용
- 적용 범위
## 대안 (Alternatives)
### 대안 1: [이름]
- 장점: ...
- 단점: ...
- 비용/복잡도: ...
### 대안 2: [이름]
- 장점: ...
- 단점: ...
- 비용/복잡도: ...
## 근거 (Rationale)
왜 이 결정을 선택했는가?
- 대안 대비 장점
- 트레이드오프 분석
- 참고 자료/벤치마크
## 결과 (Consequences)
### 긍정적
- ...
### 부정적
- ...
### 리스크
- ...
## 관련 문서
- ADR-[관련번호]: [제목]
- [외부 참고 링크]
```
## 파일 명명 규칙
- 위치: `docs/adr/` (없으면 생성)
- 파일명: `ADR-NNNN-제목-요약.md` (예: `ADR-0001-데이터베이스-선택.md`)
- 번호: 기존 ADR 파일 조회 후 자동 부여
## 규칙
- 결정의 "왜"를 중심으로 작성 (코드를 읽으면 "무엇"은 알 수 있음)
- 대안은 최소 2개 이상 제시
- 트레이드오프를 솔직하게 기록 (완벽한 선택은 없음)
- 짧고 명확하게 (1~2페이지 이내)
- 한국어로 작성
-59
View File
@@ -1,59 +0,0 @@
---
name: api-docs
label: API 문서 생성
description: 코드에서 API 엔드포인트를 분석하여 마크다운/HTML API 문서를 자동 생성합니다.
icon: \uE8A1
allowed-tools:
- folder_map
- grep
- file_read
- file_write
- search_codebase
tabs: code
---
작업 폴더의 소스 코드를 분석하여 API 문서를 생성하세요.
## 분석 대상
- REST API 엔드포인트 (Controller, Route 어노테이션)
- 함수/메서드 시그니처 및 주석
- 요청/응답 모델 (DTO, Schema)
- 인증/권한 요구사항
## 작업 절차
1. `folder_map` — 프로젝트 구조 파악
2. `grep` — API 엔드포인트 패턴 검색 (`[HttpGet]`, `@GetMapping`, `router.get`, `@app.route` 등)
3. `file_read` — 컨트롤러/라우터 파일 분석
4. `grep` — 요청/응답 모델 클래스 검색
5. `file_read` — 모델 구조 분석
6. `file_write` — API 문서 생성
## 출력 형식
마크다운으로 작성하되 다음 구조를 따르세요:
```
# API 문서
## 개요
- Base URL, 인증 방식, 공통 헤더
## 엔드포인트
### [POST] /api/users
- **설명**: 사용자 생성
- **인증**: Bearer Token 필요
- **요청 본문**:
| 필드 | 타입 | 필수 | 설명 |
|------|------|------|------|
| name | string | ✓ | 사용자 이름 |
- **응답**: 201 Created
```json
{ "id": 1, "name": "..." }
```
- **에러 코드**: 400, 401, 409
```
## 주의사항
- 코드에서 실제 확인된 내용만 문서화하세요. 추측하지 마세요.
- 주석이나 Swagger/OpenAPI 어노테이션이 있으면 우선 활용하세요.
- 인증, 페이징, 에러 처리 등 공통 패턴은 별도 섹션으로 정리하세요.
-70
View File
@@ -1,70 +0,0 @@
---
name: batch-rename
label: 파일 일괄 이름 변경
description: 패턴 매칭, 번호 붙이기, 날짜 추가 등 파일 이름을 일괄 변경합니다.
icon: \uE8AC
tabs: cowork
allowed-tools:
- folder_map
- file_read
- file_manage
- clipboard_tool
---
파일 이름을 규칙에 따라 일괄 변경하세요. 변경 전 반드시 미리보기를 제공합니다.
## 작업 절차
1. **대상 파일 스캔**: folder_map으로 작업 폴더의 파일 목록을 수집
2. **이름 변경 규칙 확인**: 사용자에게 다음 옵션을 확인
- 대상 파일 필터 (확장자, 이름 패턴)
- 변경 규칙 (아래 지원 규칙 참조)
- 적용 순서 (이름순, 날짜순, 크기순)
3. **변경 미리보기 생성**: 변경 전/후 이름을 표로 표시
```
| # | 현재 이름 | 변경 후 이름 |
|---|----------------------|----------------------|
| 1 | IMG_20260101_001.jpg | 2026-01-01_001.jpg |
| 2 | IMG_20260101_002.jpg | 2026-01-01_002.jpg |
```
4. **사용자 확인**: 미리보기를 보여주고 진행 여부를 확인
5. **일괄 변경 실행**: file_manage로 파일 이름을 순차 변경
6. **결과 보고**: 변경 성공/실패 건수와 상세 내역을 안내
## 지원 이름 변경 규칙
### 패턴 치환
- **문자열 치환**: "IMG_" → "사진_"
- **정규식 치환**: `(\d{4})(\d{2})(\d{2})` → `$1-$2-$3`
- **대소문자 변환**: 소문자, 대문자, 타이틀 케이스
### 번호 붙이기
- **순번 추가**: `문서_001.pdf`, `문서_002.pdf`, ...
- **시작 번호**: 사용자 지정 (기본: 1)
- **자릿수**: 자동 계산 (파일 수 기준)
- **위치**: 접두사 또는 접미사
### 날짜 추가
- **오늘 날짜**: `보고서_2026-03-30.docx`
- **파일 수정일**: 파일의 실제 수정 날짜 사용
- **날짜 형식**: YYYY-MM-DD, YYYYMMDD, YY.MM.DD
### 정리
- **공백 처리**: 공백 → 언더스코어/하이픈
- **특수문자 제거**: 파일명에서 특수문자 제거
- **확장자 변경**: `.jpeg` → `.jpg`
- **접두사/접미사 추가 또는 제거**
## 충돌 처리
- 변경 후 이름이 이미 존재하면 자동으로 번호 추가 (`_1`, `_2`)
- 충돌 건은 미리보기에서 ⚠️ 표시로 경고
- 원본 파일 덮어쓰기 절대 금지
## 규칙
- **미리보기 없이 직접 변경하지 않음** — 반드시 미리보기 후 사용자 확인
- 하위 폴더 포함 여부는 사용자에게 확인
- 숨김 파일(.으로 시작)은 기본 제외
- 변경 실패 시 이미 변경된 파일은 원복하지 않으므로, 중요한 경우 백업 권장
- 한 번에 1,000개 이상의 파일 변경 시 경고
한국어로 안내하세요.
-74
View File
@@ -1,74 +0,0 @@
---
name: changelog
label: 변경 이력 / 릴리즈 노트
description: Git 커밋 이력에서 자동으로 변경 이력과 릴리즈 노트를 생성합니다.
icon: \uE81C
allowed-tools:
- git_tool
- file_read
- file_write
- html_create
- text_summarize
tabs: code
---
Git 커밋 이력을 분석하여 변경 이력(CHANGELOG) 또는 릴리즈 노트를 생성하세요.
## 워크플로우
1. **이력 조회**: git_tool로 커밋 로그 수집
- 지정 기간 또는 태그 간 커밋
- 커밋 메시지 + 변경 파일 목록
2. **분류**: 커밋을 Conventional Commits 기준으로 분류
3. **Breaking Change 감지**: 시그니처 변경, API 삭제 등 감지
4. **문서 생성**: Markdown 또는 HTML로 출력
## Conventional Commits 분류
| 접두사 | 분류 | 설명 |
|--------|------|------|
| feat | ✨ 신기능 | 새로운 기능 추가 |
| fix | 🐛 버그 수정 | 버그 수정 |
| docs | 📝 문서 | 문서 변경 |
| style | 💄 스타일 | 코드 포맷팅 (동작 변경 없음) |
| refactor | ♻️ 리팩토링 | 코드 리팩토링 |
| perf | ⚡ 성능 | 성능 개선 |
| test | ✅ 테스트 | 테스트 추가/수정 |
| chore | 🔧 기타 | 빌드, 설정 변경 |
| BREAKING | 💥 Breaking | 하위 호환성 깨지는 변경 |
## 출력 형식
### CHANGELOG.md
```markdown
# Changelog
## [1.6.0] - 2026-03-30
### ✨ 신기능
- 멀티패스 문서 생성 엔진 (#123)
- PPT 네이티브 생성 도구
### 🐛 버그 수정
- 탭 전환 시 대화 유실 문제 해결
### 💥 Breaking Changes
- 없음
### 📝 문서
- 개발자 가이드 v1.6.0 업데이트
```
### 릴리즈 노트 (HTML)
사용자 친화적인 형식:
- 주요 변경사항 (스크린샷 포함 가능)
- 개선 사항
- 알려진 이슈
- 업그레이드 가이드
## 규칙
- Conventional Commits 형식이 아닌 커밋도 내용 분석으로 분류
- 중복/사소한 커밋은 병합하여 요약
- Breaking Change는 반드시 별도 섹션으로 강조
- 이슈 번호가 있으면 링크 포함
- 한국어로 작성
-37
View File
@@ -1,37 +0,0 @@
---
name: code-scaffold
label: 코드 스캐폴딩
description: 프로젝트 구조를 분석하고 새 기능의 코드 뼈대를 자동 생성합니다.
icon: \uE943
allowed-tools:
- folder_map
- file_read
- grep
- file_write
- search_codebase
tabs: code
---
작업 폴더의 프로젝트 구조를 분석하고 새 기능의 코드 뼈대를 생성하세요.
다음 도구를 사용하세요:
1. folder_map — 프로젝트 구조 파악
2. file_read — 기존 코드 패턴 분석
3. grep — 코딩 컨벤션 확인
4. file_write — 새 파일 생성
작업 순서:
1. 프로젝트 타입 감지 (언어, 프레임워크, 빌드 시스템)
2. 기존 코드 패턴 분석 (네이밍, 폴더 구조, 임포트 스타일)
3. 사용자 요청에 맞는 코드 뼈대 생성
생성 항목:
- 클래스/모듈 파일 (프로젝트 컨벤션에 맞춰)
- 인터페이스/타입 정의
- 단위 테스트 파일
- 필요한 설정/구성 파일
규칙:
- 기존 프로젝트의 코딩 스타일을 따르세요
- TODO 주석으로 구현이 필요한 부분을 표시하세요
- 한국어 주석을 추가하세요
-72
View File
@@ -1,72 +0,0 @@
---
name: commit-review
label: 커밋 메시지 리뷰
description: Git 커밋 메시지를 Conventional Commits 기준으로 검토하고 개선을 제안합니다.
icon: \uE8CB
allowed-tools:
- process
- git_tool
- file_read
- text_summarize
tabs: code
---
작업 폴더의 최근 Git 커밋 메시지를 검토하고 개선안을 제시하세요.
## 작업 절차
1. `process``git log --oneline -20` 으로 최근 커밋 목록 확인
2. `process``git log --format="%H%n%s%n%b%n---" -10` 으로 상세 메시지 확인
3. `process``git diff HEAD~1` 으로 최신 커밋 변경 내용 확인 (필요 시)
4. 각 커밋을 Conventional Commits 기준으로 분석
5. 결과를 정리하여 출력
## Conventional Commits 규칙
```
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
```
### type 종류
- `feat`: 새 기능
- `fix`: 버그 수정
- `docs`: 문서 변경
- `style`: 포맷팅 (코드 동작 변경 없음)
- `refactor`: 리팩토링
- `perf`: 성능 개선
- `test`: 테스트 추가/수정
- `chore`: 빌드/도구 설정
- `ci`: CI 설정 변경
## 검토 항목
1. **type 적절성**: 변경 내용과 type이 일치하는가
2. **설명 품질**: 50자 이내, 명령형, 명확한 내용
3. **본문 유무**: 복잡한 변경에 "왜" 설명이 있는가
4. **일관성**: 팀 내 커밋 스타일이 통일되어 있는가
5. **Breaking Change**: BREAKING CHANGE 푸터 또는 ! 표기
## 출력 형식
```
## 커밋 메시지 리뷰 결과
### 전체 요약
- 검토 커밋: N개
- 규칙 준수: N개 ✓ / 위반: N개 ✗
### 개별 리뷰
#### abc1234 "Fix login bug"
- ❌ type 없음 → `fix: resolve login authentication failure`
- ❌ 본문 없음 → 원인과 해결 방법 추가 권장
#### def5678 "feat: add user profile page"
- ✅ Conventional Commits 준수
- 💡 scope 추가 권장: `feat(profile): add user profile page`
```
## 주의사항
- 비판이 아닌 건설적 제안을 하세요.
- 팀의 기존 컨벤션이 있으면 그것을 우선 존중하세요.
- 개선된 메시지 예시를 항상 함께 제시하세요.
-51
View File
@@ -1,51 +0,0 @@
---
name: compare
label: 비교 분석표
description: 2개 이상 항목의 비교 분석 매트릭스를 생성합니다.
icon: \uE9D5
allowed-tools:
- html_create
- excel_create
- file_read
- document_read
- chart_create
tabs: all
---
사용자가 요청한 항목들을 체계적으로 비교 분석하는 매트릭스를 생성하세요.
## 워크플로우
1. **비교 대상 확인**: 어떤 항목들을 비교할지 파악
- 제품, 기술, 방안, 서비스, 도구, 프레임워크 등
2. **비교 기준 설정**: 적절한 비교 축을 설계
- 기능, 가격, 성능, 사용성, 확장성, 지원, 보안 등
3. **데이터 수집**: 참고 파일이 있으면 읽어서 반영
4. **분석표 생성**: HTML 또는 Excel로 비교 매트릭스 생성
5. **종합 평가**: 총평 + 추천 의견 제시
## 비교표 구성
### 기본 매트릭스
| 기준 | 항목 A | 항목 B | 항목 C |
|------|--------|--------|--------|
| 기능1 | ✅ 지원 | ⚠ 일부 | ❌ 미지원 |
| 기능2 | ... | ... | ... |
### 점수 비교 (radar 차트 활용)
- 각 항목을 1~10점으로 정량화
- chart_create로 레이더 차트 또는 바 차트 시각화
### 종합 평가
- 장점/단점 요약
- 상황별 추천 (예: "예산이 제한적이면 A, 확장성이 중요하면 B")
## 출력 형식
- **HTML** (권장): 색상 배지, 차트 포함 비주얼 보고서
- **Excel**: 정량 데이터 + 수식 기반 점수표
## 규칙
- 객관적 사실 기반으로 비교 (주관적 판단은 별도 섹션)
- 각 항목의 강점과 약점을 균형 있게 서술
- 출처/근거가 있으면 명시
- 한국어로 작성
-137
View File
@@ -1,137 +0,0 @@
---
name: csv-to-xlsx
label: CSV → Excel 변환
description: CSV 파일을 서식이 완성된 Excel(.xlsx)로 변환합니다. 헤더 고정, 필터, 조건부 서식, 자동 열 너비를 적용합니다.
icon: \uE9F9
allowed-tools:
- folder_map
- file_read
- file_write
- process
- format_convert
- data_pivot
- template_render
tabs: cowork
---
CSV 파일을 전문적인 서식이 적용된 Excel 파일로 변환하세요.
## 실행 경로 선택 (Python 가능/불가)
- 먼저 `process``python --version`을 확인하세요.
- Python 가능: 기존 Python 스크립트 경로로 변환/서식 자동화를 수행하세요.
- Python 불가: `format_convert`로 CSV를 XLSX로 변환하고, `data_pivot`으로 핵심 요약 시트를 구성한 뒤 `file_write`로 사용 안내를 남기세요.
## 사전 준비
먼저 필요한 패키지가 설치되어 있는지 확인하고, 없으면 설치하세요:
```
process: pip install openpyxl pandas
```
## 작업 절차
1. **파일 확인**: folder_map으로 작업 폴더에서 CSV 파일을 탐색
2. **CSV 분석**: file_read로 CSV 파일의 구조(컬럼, 행 수, 인코딩, 구분자) 파악
3. **변환 옵션 확인**: 사용자에게 다음 옵션을 확인
- 헤더 행 고정 여부 (기본: 활성)
- 자동 필터 적용 여부 (기본: 활성)
- 조건부 서식 대상 컬럼 (숫자 컬럼 자동 감지)
- 시트 이름 (기본: 파일명)
4. **Python 스크립트 작성**: file_write로 변환 스크립트 생성
5. **스크립트 실행**: `process`로 Python 스크립트 실행
6. **결과 확인**: 생성된 .xlsx 파일 경로와 요약 정보를 안내
## Python 스크립트 템플릿
```python
import pandas as pd
from openpyxl import load_workbook
from openpyxl.styles import Font, PatternFill, Alignment, Border, Side
from openpyxl.utils import get_column_letter
from openpyxl.formatting.rule import CellIsRule
# CSV 읽기 (인코딩 자동 감지)
for enc in ['utf-8', 'cp949', 'euc-kr', 'utf-8-sig']:
try:
df = pd.read_csv('input.csv', encoding=enc)
break
except (UnicodeDecodeError, Exception):
continue
# Excel 저장
output_path = 'output.xlsx'
df.to_excel(output_path, index=False, sheet_name='Sheet1')
# 서식 적용
wb = load_workbook(output_path)
ws = wb.active
# 헤더 스타일
header_font = Font(bold=True, color='FFFFFF', size=11)
header_fill = PatternFill(start_color='4472C4', end_color='4472C4', fill_type='solid')
header_align = Alignment(horizontal='center', vertical='center', wrap_text=True)
thin_border = Border(
left=Side(style='thin'),
right=Side(style='thin'),
top=Side(style='thin'),
bottom=Side(style='thin')
)
for col_idx, cell in enumerate(ws[1], 1):
cell.font = header_font
cell.fill = header_fill
cell.alignment = header_align
cell.border = thin_border
# 자동 열 너비
for col_idx in range(1, ws.max_column + 1):
max_length = 0
col_letter = get_column_letter(col_idx)
for row in ws.iter_rows(min_col=col_idx, max_col=col_idx):
for cell in row:
if cell.value:
max_length = max(max_length, len(str(cell.value)))
ws.column_dimensions[col_letter].width = min(max_length + 4, 50)
# 헤더 행 고정 (Freeze Panes)
ws.freeze_panes = 'A2'
# 자동 필터
ws.auto_filter.ref = ws.dimensions
# 숫자 컬럼 조건부 서식 (음수 빨강)
for col_idx in range(1, ws.max_column + 1):
col_letter = get_column_letter(col_idx)
sample_values = [ws.cell(row=r, column=col_idx).value for r in range(2, min(ws.max_row + 1, 12))]
if any(isinstance(v, (int, float)) for v in sample_values if v is not None):
cell_range = f'{col_letter}2:{col_letter}{ws.max_row}'
ws.conditional_formatting.add(cell_range,
CellIsRule(operator='lessThan', formula=['0'],
font=Font(color='FF0000')))
# 데이터 행 줄무늬 (가독성)
light_fill = PatternFill(start_color='D9E2F3', end_color='D9E2F3', fill_type='solid')
for row_idx in range(2, ws.max_row + 1):
for col_idx in range(1, ws.max_column + 1):
cell = ws.cell(row=row_idx, column=col_idx)
cell.border = thin_border
if row_idx % 2 == 0:
cell.fill = light_fill
wb.save(output_path)
print(f'변환 완료: {output_path} ({ws.max_row - 1}× {ws.max_column}열)')
```
## 서식 옵션
- **헤더 스타일**: 파란 배경 + 흰색 굵은 글씨 + 가운데 정렬
- **줄무늬**: 짝수 행 연한 파랑 배경 (가독성 향상)
- **열 너비**: 내용 기준 자동 조정 (최대 50)
- **조건부 서식**: 숫자 컬럼 음수 빨강 표시
- **Freeze Panes**: 헤더 행 고정
- **Auto Filter**: 전체 컬럼 필터 활성화
## 규칙
- 원본 CSV 파일은 수정하지 않음
- 인코딩 자동 감지 (UTF-8 → CP949 → EUC-KR 순)
- 대용량 파일 (100,000행 이상) 경고 후 진행
- 출력 파일명: 원본 파일명 기준 (.csv → .xlsx)
한국어로 안내하세요. 작업 폴더에 Python 스크립트와 결과 파일을 저장하세요.
-56
View File
@@ -1,56 +0,0 @@
---
name: data-convert
label: 데이터 변환기
description: 데이터 포맷 간 변환, 정제, 필터링, 통계 요약을 수행합니다.
icon: \uE8AB
allowed-tools:
- file_read
- file_write
- json_tool
- csv_create
- data_pivot
- excel_create
tabs: cowork
---
데이터 파일의 포맷 변환, 정제, 필터링, 통계 요약을 수행하세요.
## 지원 변환
| 입력 → | JSON | CSV | Excel | Markdown |
|--------|------|-----|-------|----------|
| JSON | — | ✅ | ✅ | ✅ |
| CSV | ✅ | — | ✅ | ✅ |
| Excel | ✅ | ✅ | — | ✅ |
| TSV | ✅ | ✅ | ✅ | ✅ |
## 워크플로우
1. **파일 분석**: 입력 파일의 포맷, 인코딩, 구조 파악
2. **데이터 정제** (선택):
- 빈 행/열 제거
- 중복 제거
- 데이터 타입 정리 (숫자 문자열 → 숫자)
- 결측치 처리 (제거 또는 기본값)
3. **필터링** (선택):
- 조건 기반 행 필터링
- 필요 컬럼만 추출
4. **변환**: 대상 포맷으로 변환
5. **통계 요약**: 기본 통계 제공
## 정제 옵션
- `remove_empty`: 빈 행 제거
- `remove_duplicates`: 중복 행 제거
- `trim`: 공백 제거
- `fill_na`: 결측치 채우기 (값 지정)
## 출력
- 변환된 파일 저장
- 변환 통계 (원본 행 수, 변환 후 행 수, 제거 행 수)
- 컬럼별 기본 통계 (수치 컬럼: 합계, 평균, 최소, 최대)
## 규칙
- 원본 파일은 수정하지 않음 (새 파일로 저장)
- 인코딩: UTF-8 기본 (EUC-KR 옵션)
- 대용량 파일 (10MB 이상) 경고
- 한국어로 안내
-137
View File
@@ -1,137 +0,0 @@
---
name: data-visualize-adv
label: 고급 데이터 시각화
description: Python matplotlib/seaborn을 사용하여 히트맵, 산점도, 상관관계 등 고급 시각화를 생성합니다.
icon: \uE9D9
allowed-tools:
- folder_map
- file_read
- file_write
- process
- data_pivot
- chart_create
- template_render
tabs: cowork
---
데이터를 고급 시각화 차트로 변환하세요.
## 실행 경로 선택 (Python 가능/불가)
- 먼저 `process``python --version`을 확인하세요.
- Python 가능: 기존 matplotlib/seaborn 경로를 사용하세요.
- Python 불가: `data_pivot`으로 통계를 계산하고 `chart_create` + `template_render` + `file_write`로 HTML/SVG 리포트를 생성하세요.
## 사전 준비
필요한 패키지를 설치하세요:
```
process: pip install matplotlib seaborn pandas numpy
```
## 작업 절차
1. **데이터 확인**: 사용자가 제공한 CSV/JSON/Excel 데이터 파일 확인
2. **데이터 로드**: pandas로 데이터 읽기
3. **Python 스크립트 작성**: file_write로 시각화 스크립트 생성
4. **스크립트 실행**: `process`로 실행
5. **결과 확인**: 생성된 차트 이미지 경로를 사용자에게 안내
## 시각화 유형별 템플릿
### 공통 설정
```python
import matplotlib.pyplot as plt
import seaborn as sns
import pandas as pd
import numpy as np
# 한글 폰트 설정
plt.rcParams['font.family'] = 'Malgun Gothic'
plt.rcParams['axes.unicode_minus'] = False
sns.set_theme(style='whitegrid', font='Malgun Gothic')
```
### 히트맵 (상관관계 행렬)
```python
df = pd.read_csv('data.csv')
corr = df.select_dtypes(include=[np.number]).corr()
fig, ax = plt.subplots(figsize=(10, 8))
sns.heatmap(corr, annot=True, fmt='.2f', cmap='RdBu_r',
center=0, square=True, linewidths=0.5, ax=ax)
ax.set_title('상관관계 히트맵')
plt.tight_layout()
plt.savefig('heatmap.png', dpi=150)
```
### 산점도 (Scatter Plot)
```python
df = pd.read_csv('data.csv')
fig, ax = plt.subplots(figsize=(10, 8))
sns.scatterplot(data=df, x='col_x', y='col_y', hue='category',
size='value', sizes=(20, 200), alpha=0.7, ax=ax)
ax.set_title('산점도')
plt.tight_layout()
plt.savefig('scatter.png', dpi=150)
```
### 시계열 분석
```python
df = pd.read_csv('data.csv', parse_dates=['date'])
fig, ax = plt.subplots(figsize=(12, 6))
sns.lineplot(data=df, x='date', y='value', hue='category', ax=ax)
ax.set_title('시계열 트렌드')
plt.xticks(rotation=45)
plt.tight_layout()
plt.savefig('timeseries.png', dpi=150)
```
### 분포 비교 (박스플롯 + 바이올린)
```python
fig, axes = plt.subplots(1, 2, figsize=(14, 6))
sns.boxplot(data=df, x='group', y='value', ax=axes[0])
axes[0].set_title('박스플롯')
sns.violinplot(data=df, x='group', y='value', ax=axes[1])
axes[1].set_title('바이올린 플롯')
plt.tight_layout()
plt.savefig('distribution.png', dpi=150)
```
### 다중 차트 대시보드
```python
fig, axes = plt.subplots(2, 2, figsize=(14, 12))
# 좌상: 히트맵
sns.heatmap(corr, annot=True, fmt='.1f', ax=axes[0,0])
# 우상: 산점도
sns.scatterplot(data=df, x='x', y='y', ax=axes[0,1])
# 좌하: 히스토그램
sns.histplot(data=df, x='value', kde=True, ax=axes[1,0])
# 우하: 박스플롯
sns.boxplot(data=df, x='group', y='value', ax=axes[1,1])
fig.suptitle('데이터 분석 대시보드', fontsize=16, fontweight='bold')
plt.tight_layout()
plt.savefig('dashboard.png', dpi=150)
```
### 페어플롯 (변수 간 관계 전체)
```python
g = sns.pairplot(df, hue='category', diag_kind='kde')
g.fig.suptitle('변수 간 관계', y=1.02)
plt.savefig('pairplot.png', dpi=150)
```
## 스타일 옵션
- seaborn 테마: `whitegrid`, `darkgrid`, `white`, `dark`, `ticks`
- 컬러 팔레트: `Set2`, `husl`, `coolwarm`, `RdBu_r`, `viridis`
- 출력 형식: png, svg, pdf
- 해상도: `dpi=150` (기본), `dpi=300` (인쇄용)
한국어로 안내하세요. 작업 폴더에 Python 스크립트와 결과 파일을 저장하세요.
-45
View File
@@ -1,45 +0,0 @@
---
name: data-visualize
label: 데이터 시각화
description: CSV/Excel 데이터를 분석하여 차트가 포함된 HTML 보고서를 생성합니다.
icon: \uE9D9
allowed-tools:
- folder_map
- file_read
- file_write
- data_pivot
- chart_create
- template_render
tabs: cowork
---
작업 폴더의 데이터 파일을 분석하고 시각화 보고서를 생성하세요.
다음 도구를 사용하세요:
1. folder_map — 데이터 파일 탐색
2. file_read — CSV/Excel 데이터 읽기
3. file_write — HTML 시각화 보고서 생성
각화 전략:
1. **데이터 파악**: 컬럼 타입, 결측치, 기본 통계량 확인
2. **적절한 차트 선택**:
- 시계열 → 라인 차트
- 비교 → 바 차트
- 비율 → 파이/도넛 차트
- 분포 → 히스토그램
- 상관관계 → 산점도
3. **HTML 보고서 생성**: 인라인 SVG 또는 CSS 기반 차트 (외부 라이브러리 없이)
보고서 구성:
## 데이터 요약
- 기본 통계 테이블
## 시각화
- 데이터 특성에 맞는 2~4개 차트
- 각 차트에 대한 해석
## 인사이트
- 데이터에서 발견한 주요 패턴
- 이상치 또는 주목할 포인트
한국어로 작성하세요. 차트는 CSS/SVG 기반으로 외부 의존성 없이 생성하세요.
-66
View File
@@ -1,66 +0,0 @@
---
name: db-schema
label: DB 스키마 분석
description: 데이터베이스 스키마를 분석하여 ERD 다이어그램과 테이블 문서를 생성합니다.
icon: \uE968
allowed-tools:
- folder_map
- grep
- file_read
- file_write
- search_codebase
tabs: code
---
작업 폴더의 코드에서 데이터베이스 스키마를 분석하고 문서화하세요.
## 분석 대상
- ORM 모델/엔티티 클래스 (Entity Framework, SQLAlchemy, Sequelize, JPA 등)
- 마이그레이션 파일
- SQL DDL 스크립트 (CREATE TABLE)
- 관계 정의 (FK, Navigation Property)
## 작업 절차
1. `folder_map` — 프로젝트 구조 파악
2. `grep` — 엔티티/모델 클래스 검색 (`DbSet`, `@Entity`, `Model.define`, `CREATE TABLE` 등)
3. `file_read` — 모델 파일 분석 (컬럼, 타입, 관계)
4. `grep` — 인덱스, 제약 조건 검색
5. `file_write` — 스키마 문서 + Mermaid ERD 생성
## 출력 형식
### 테이블 문서
각 테이블에 대해:
```
## Users 테이블
| 컬럼 | 타입 | Null | 기본값 | 설명 |
|------|------|------|--------|------|
| Id | int | NO | AUTO_INCREMENT | PK |
| Name | nvarchar(100) | NO | - | 사용자 이름 |
| CreatedAt | datetime | NO | GETDATE() | 생성일 |
- **인덱스**: IX_Users_Name (Name)
- **관계**: Orders (1:N), Profile (1:1)
```
### Mermaid ERD
```mermaid
erDiagram
Users ||--o{ Orders : "has"
Users ||--|| Profile : "has"
Users {
int Id PK
string Name
datetime CreatedAt
}
Orders {
int Id PK
int UserId FK
decimal Amount
}
```
## 주의사항
- 코드에서 실제 확인된 스키마만 문서화하세요.
- 관계(1:1, 1:N, N:M)를 정확히 파악하세요.
- 마이그레이션이 있으면 최종 상태를 기준으로 작성하세요.
-85
View File
@@ -1,85 +0,0 @@
---
name: dependency-audit
label: 의존성 분석
description: 프로젝트 의존성 그래프를 분석하고 보안 취약점, 라이선스, 업데이트 현황을 보고합니다.
icon: \uE964
allowed-tools:
- file_read
- grep
- folder_map
- glob
- html_create
- process
tabs: code
---
프로젝트의 패키지 의존성을 분석하여 보안, 라이선스, 업데이트 보고서를 생성하세요.
## 워크플로우
1. **패키지 파일 탐지**: glob으로 의존성 파일 검색
- .NET: `*.csproj`, `packages.config`, `Directory.Build.props`
- Node: `package.json`, `package-lock.json`, `yarn.lock`
- Python: `requirements.txt`, `Pipfile`, `pyproject.toml`
- Java: `pom.xml`, `build.gradle`
2. **의존성 목록 추출**: file_read로 파일 파싱
3. **분석 수행**: 각 의존성에 대해 검사
4. **보고서 생성**: html_create로 분석 보고서 생성
## 분석 항목
### 1. 직접 의존성 목록
| 패키지 | 현재 버전 | 최신 버전 | 업데이트 필요 |
|--------|----------|----------|-------------|
| ... | ... | ... | ✅/⚠/❌ |
### 2. 보안 취약점 (알려진 패턴)
- 알려진 취약 버전 패턴 탐지
- 폐기된(deprecated) 패키지 식별
- 유지보수 중단된 패키지 경고
### 3. 라이선스 검사
| 라이선스 | 호환성 | 패키지 |
|---------|--------|--------|
| MIT | ✅ 허용 | lib-a, lib-b |
| GPL-3.0 | ⚠ 주의 | lib-c |
| 상용 | ❌ 검토 필요 | lib-d |
### 4. 의존성 크기 분석
- 패키지별 예상 크기
- 전체 node_modules / NuGet 캐시 크기
- 불필요하게 큰 패키지 식별
### 5. 중복/충돌 검사
- 동일 기능 중복 패키지 (예: lodash + underscore)
- 버전 충돌 가능성
## 프레임워크별 검사 명령
### .NET
```
dotnet list package --outdated
dotnet list package --vulnerable
```
### Node.js
```
npm audit
npm outdated
```
### Python
```
pip list --outdated
pip-audit
```
## 출력
- HTML 보고서: 위험도별 색상 구분, 차트 포함
- 요약: 총 패키지 수, 업데이트 필요 수, 보안 이슈 수, 라이선스 경고 수
## 규칙
- 외부 서버 접속 없이 로컬 파일 분석만 수행
- process 도구 사용 시 `dotnet list` / `npm audit` 등 읽기 전용 명령만
- 패키지를 직접 업데이트하지 않음 (보고서만)
- 한국어로 작성
-109
View File
@@ -1,109 +0,0 @@
---
name: diagram-generator
label: 다이어그램 생성
description: Python matplotlib/graphviz를 사용하여 플로차트, 시퀀스, ER 다이어그램 등을 생성합니다.
icon: \uE9D9
allowed-tools:
- file_read
- file_write
- process
- chart_create
- template_render
tabs: cowork
---
사용자의 요구에 맞는 다이어그램을 Python으로 생성하세요.
## 실행 경로 선택 (Python 가능/불가)
- 먼저 `process``python --version`을 확인하세요.
- Python 가능: 기존 Graphviz/matplotlib 경로를 사용하세요.
- Python 불가: `template_render``file_write`로 Mermaid 기반 다이어그램 문서를 생성하고, 필요 시 `chart_create`로 대체 시각화를 제공하세요.
## 사전 준비
필요한 패키지를 설치하세요:
```
process: pip install matplotlib graphviz
```
시스템에 Graphviz가 설치되어 있어야 합니다 (https://graphviz.org/download/).
## 작업 절차
1. **요구사항 파악**: 다이어그램 유형, 노드/관계, 스타일 확인
2. **Python 스크립트 작성**: file_write로 .py 파일 생성
3. **스크립트 실행**: `process`로 Python 스크립트 실행
4. **결과 확인**: 생성된 이미지 파일 경로를 사용자에게 안내
## 다이어그램 유형별 템플릿
### 플로차트 (Graphviz)
```python
from graphviz import Digraph
dot = Digraph(comment='Flowchart', format='png')
dot.attr(rankdir='TB', fontname='Malgun Gothic')
dot.attr('node', shape='box', style='rounded,filled', fillcolor='#E8F0FE')
dot.node('start', '시작', shape='ellipse', fillcolor='#34A853', fontcolor='white')
dot.node('process1', '데이터 수집')
dot.node('decision', '조건 확인?', shape='diamond', fillcolor='#FBBC04')
dot.node('process2', '처리')
dot.node('end', '종료', shape='ellipse', fillcolor='#EA4335', fontcolor='white')
dot.edge('start', 'process1')
dot.edge('process1', 'decision')
dot.edge('decision', 'process2', label='')
dot.edge('decision', 'end', label='아니오')
dot.edge('process2', 'end')
dot.render('flowchart', cleanup=True)
```
### 시퀀스 다이어그램 (matplotlib)
```python
import matplotlib.pyplot as plt
import matplotlib.patches as patches
fig, ax = plt.subplots(1, 1, figsize=(10, 8))
# 액터 라이프라인, 메시지 화살표 등을 matplotlib으로 직접 그리기
ax.set_xlim(0, 10)
ax.set_ylim(0, 10)
ax.invert_yaxis()
ax.axis('off')
plt.savefig('sequence.png', dpi=150, bbox_inches='tight')
```
### ER 다이어그램 (Graphviz)
```python
from graphviz import Graph
er = Graph('ER', format='png', engine='neato')
er.attr('node', shape='box', style='filled', fillcolor='#E8F0FE')
er.node('user', 'User\n─────\nid (PK)\nname\nemail')
er.node('order', 'Order\n─────\nid (PK)\nuser_id (FK)\ntotal')
er.edge('user', 'order', label='1:N')
er.render('er_diagram', cleanup=True)
```
### 조직도 (Graphviz)
```python
from graphviz import Digraph
org = Digraph(format='png')
org.attr(rankdir='TB')
org.attr('node', shape='box', style='rounded,filled', fillcolor='#E3F2FD')
org.node('ceo', 'CEO')
org.node('cto', 'CTO')
org.node('cfo', 'CFO')
org.edges([('ceo', 'cto'), ('ceo', 'cfo')])
org.render('org_chart', cleanup=True)
```
## 스타일 옵션
- 폰트: `fontname='Malgun Gothic'` (한글 지원)
- 색상: HTML 컬러 코드 지원
- 출력 형식: png, svg, pdf
- 레이아웃 엔진: dot(계층), neato(스프링), circo(원형), fdp(포스)
한국어로 안내하세요. 작업 폴더에 Python 스크립트와 결과 파일을 저장하세요.
-109
View File
@@ -1,109 +0,0 @@
---
name: docx-creator
label: Word 문서 생성
description: Python을 사용하여 전문적인 Word 문서(.docx)를 생성합니다. 작업 폴더의 양식 파일을 자동 활용합니다.
icon: \uE8A5
allowed-tools:
- folder_map
- document_read
- file_read
- file_write
- process
- document_assemble
- format_convert
tabs: cowork
---
사용자의 요구에 맞는 전문적인 Word 문서를 Python으로 생성하세요.
## 실행 경로 선택 (Python 가능/불가)
- 먼저 `process``python --version`을 확인하세요.
- Python 가능: 기존 python-docx 경로를 사용하세요.
- Python 불가: `document_assemble`로 문서 본문을 구성하고 `format_convert`로 docx 산출을 시도하세요. 실패 시 Markdown/HTML 결과와 변환 가이드를 함께 제공하세요.
## 사전 준비
먼저 python-docx 패키지가 설치되어 있는지 확인하고, 없으면 설치하세요:
```
process: pip install python-docx
```
## 양식 활용 (템플릿 모드)
작업 폴더에 양식 파일이 있으면 **반드시** 활용하세요:
1. **양식 탐색**: `folder_map`으로 작업 폴더를 스캔하여 `.docx` 파일 확인
2. **양식 후보 판별**:
- 파일명에 "양식", "template", "서식", "표준", "기본" 포함
- 또는 사용자가 명시적으로 "XX 양식으로 작성해줘" 요청
- 또는 사용자가 특정 .docx 파일명을 언급
3. **양식 구조 파악**: `document_read`로 양식 파일의 구조(스타일, 헤더, 섹션)를 먼저 확인
4. **양식 기반 생성**:
```python
doc = Document('양식_보고서.docx') # ← 빈 Document() 대신 양식 로드
# 양식의 스타일, 머리글/바닥글, 로고, 페이지 설정이 자동 상속됨
# 기존 본문 내용을 지우고 새 내용만 추가
for paragraph in doc.paragraphs:
paragraph.clear() # 기존 내용 제거
# 또는 필요에 따라 특정 섹션만 교체
```
5. **양식이 없으면**: 아래 기본 템플릿으로 새 문서 생성
## 작업 절차
1. **요구사항 파악**: 사용자가 원하는 문서의 종류, 구조, 내용을 확인
2. **양식 확인**: folder_map으로 작업 폴더에 양식 .docx 파일이 있는지 확인
3. **Python 스크립트 작성**: file_write로 .py 파일 생성
4. **스크립트 실행**: `process`로 Python 스크립트 실행
5. **결과 확인**: 생성된 .docx 파일 경로를 사용자에게 안내
## Python 스크립트 템플릿
```python
from docx import Document
from docx.shared import Inches, Pt, Cm
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.enum.style import WD_STYLE_TYPE
import os
# 양식 파일 자동 감지
template_keywords = ['양식', 'template', '서식', '표준', '기본']
template_file = None
for f in os.listdir('.'):
if f.endswith('.docx') and any(kw in f.lower() for kw in template_keywords):
template_file = f
break
# 양식이 있으면 활용, 없으면 새 문서
if template_file:
doc = Document(template_file)
print(f'양식 활용: {template_file}')
else:
doc = Document()
# 스타일 설정 (양식이 없을 때만)
style = doc.styles['Normal']
font = style.font
font.name = '맑은 고딕'
font.size = Pt(11)
# 제목
doc.add_heading('문서 제목', level=0)
# 본문
doc.add_paragraph('내용을 여기에 작성합니다.')
# 표
table = doc.add_table(rows=2, cols=3)
table.style = 'Light Grid Accent 1'
# 저장
doc.save('output.docx')
```
## 지원 기능
- 제목/소제목 계층 구조
- 표 (스타일, 병합, 서식)
- 이미지 삽입
- 머리글/바닥글
- 페이지 번호
- 목차
- 글머리 기호/번호 목록
- **양식 파일 기반 스타일 상속** (로고, 헤더, 페이지 설정 자동 유지)
한국어로 안내하세요. 작업 폴더에 Python 스크립트와 결과 파일을 저장하세요.
-55
View File
@@ -1,55 +0,0 @@
---
name: email-draft
label: 비즈니스 이메일 작성
description: 상황과 톤에 맞는 전문적인 비즈니스 이메일 초안을 생성합니다.
icon: \uE715
allowed-tools:
- clipboard_tool
- file_write
tabs: all
---
사용자의 요청에 맞는 전문적인 비즈니스 이메일을 작성하세요.
## 워크플로우
1. 사용자에게 다음 정보를 확인하세요:
- **수신자**: 이름, 직급, 부서 (알고 있다면)
- **목적**: 요청 / 보고 / 안내 / 감사 / 사과 / 협조 / 초대 / 회신
- **핵심 내용**: 전달할 주요 메시지 (1~3가지)
- **톤**: 공식(格式) / 반공식 / 친근
- **언어**: 한국어 / 영어 / 일본어
2. 이메일 초안을 작성합니다:
- 적절한 인사말로 시작
- 핵심 내용은 **굵게** 강조
- 요청 사항은 명확하고 구체적으로
- 마무리 인사와 서명 포함
3. 사용자에게 초안을 보여주고 수정 요청을 받습니다.
4. 최종본을 clipboard_tool로 클립보드에 복사합니다.
## 이메일 구조
```
제목: [간결하고 명확한 제목]
[수신자] 님께,
[인사말]
[본문 - 목적과 핵심 내용]
[요청 사항 또는 다음 단계]
[마무리 인사]
[서명]
```
## 규칙
- 한 문단은 3~4문장을 넘지 않도록 간결하게
- 수동적 표현보다 능동적 표현 선호
- 약어는 처음 사용 시 풀어 쓰기
- 긴급도에 따라 제목에 [긴급], [참고] 등 태그 사용
-152
View File
@@ -1,152 +0,0 @@
---
name: env-setup
label: 프로젝트 환경 설정
description: .gitignore, requirements.txt, .editorconfig 등 프로젝트 환경 설정 파일을 자동 생성합니다.
icon: \uE835
tabs: code
allowed-tools:
- folder_map
- file_read
- file_write
- process
---
프로젝트 유형에 맞는 환경 설정 파일을 자동으로 생성하세요.
## 작업 절차
1. **프로젝트 분석**: folder_map으로 프로젝트 구조를 파악하고 유형 판별
- `.py` 파일 → Python 프로젝트
- `package.json` 또는 `.js/.ts` 파일 → Node.js 프로젝트
- `.csproj` 또는 `.sln` 파일 → .NET 프로젝트
- `pom.xml` 또는 `.java` 파일 → Java 프로젝트
- 복합 프로젝트인 경우 모든 유형을 병합
2. **기존 설정 확인**: 이미 존재하는 설정 파일이 있는지 확인
- 있으면: 내용을 분석하여 누락된 항목만 추가 제안
- 없으면: 새로 생성
3. **생성할 파일 목록 제안**: 사용자에게 생성할 파일 목록을 보여주고 확인
4. **파일 생성**: file_write로 각 설정 파일 생성
5. **결과 안내**: 생성된 파일 목록과 주요 설정 내용 요약
## 프로젝트별 템플릿
### Python 프로젝트
생성 파일: `.gitignore`, `requirements.txt`, `.editorconfig`, `setup.cfg`, `.flake8`
**.gitignore (Python)**:
```
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
venv/
.venv/
*.egg-info/
dist/
build/
.eggs/
*.egg
.mypy_cache/
.pytest_cache/
.coverage
htmlcov/
.env
.idea/
.vscode/
*.log
```
**requirements.txt**: 프로젝트에서 import 문을 스캔하여 자동 생성
### Node.js 프로젝트
생성 파일: `.gitignore`, `.editorconfig`, `.nvmrc`, `.prettierrc`
**.gitignore (Node)**:
```
node_modules/
dist/
build/
.env
.env.local
*.log
npm-debug.log*
.DS_Store
coverage/
.nyc_output/
.idea/
.vscode/
*.tgz
```
### .NET 프로젝트
생성 파일: `.gitignore`, `.editorconfig`, `Directory.Build.props`
**.gitignore (.NET)**:
```
bin/
obj/
.vs/
*.user
*.suo
*.cache
packages/
*.nupkg
TestResults/
.idea/
*.DotSettings.user
```
### Java 프로젝트
생성 파일: `.gitignore`, `.editorconfig`
**.gitignore (Java)**:
```
*.class
*.jar
*.war
*.ear
target/
.gradle/
build/
.idea/
*.iml
.settings/
.classpath
.project
out/
```
## 공통 .editorconfig
```ini
root = true
[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[*.{yml,yaml}]
indent_size = 2
[*.{json,js,ts,jsx,tsx}]
indent_size = 2
[Makefile]
indent_style = tab
```
## 규칙
- 기존 설정 파일이 있으면 덮어쓰지 않고, 누락 항목만 제안
- .env 파일은 생성하지 않음 (보안 — 사용자가 직접 생성)
- 생성 전 파일 목록을 반드시 사용자에게 확인
- 프로젝트 루트에 생성 (하위 폴더에 생성하지 않음)
한국어로 안내하세요.
-63
View File
@@ -1,63 +0,0 @@
---
name: gen-test
label: 테스트 생성기
description: 지정 파일의 단위 테스트를 자동 생성하고 실행하여 커버리지를 분석합니다.
icon: \uE9D5
allowed-tools:
- file_read
- file_write
- test_loop
- grep
- folder_map
- dev_env_detect
- build_run
tabs: code
---
지정된 소스 파일의 단위 테스트를 자동 생성하고 실행하세요.
## 워크플로우
1. **환경 감지**: dev_env_detect로 프로젝트 타입, 테스트 프레임워크 확인
2. **대상 분석**: file_read로 테스트 대상 파일 분석
- 공개 메서드/함수 목록 추출
- 메서드별 입출력 타입 파악
- 의존성 확인 (Mock 필요 여부)
3. **테스트 생성**: 각 메서드에 대해 테스트 케이스 작성
- 정상 케이스 (Happy Path)
- 경계값 (Boundary)
- 예외/에러 케이스
- null/empty 입력
4. **실행 및 검증**: test_loop으로 테스트 실행
5. **결과 보고**: 성공/실패 요약, 커버리지 추정
## 테스트 작성 원칙
### 명명 규칙
- C#: `[메서드명]_[시나리오]_[기대결과]`
- Python: `test_[메서드명]_[시나리오]`
- JavaScript: `should [기대 동작] when [조건]`
### 테스트 구조 (AAA 패턴)
```
Arrange — 테스트 데이터 준비
Act — 대상 메서드 실행
Assert — 결과 검증
```
### 프레임워크별 지원
- C#: xUnit, NUnit, MSTest
- Python: pytest, unittest
- JavaScript: Jest, Mocha, Vitest
- Java: JUnit 5
## 출력
- 테스트 파일 생성 (프로젝트 컨벤션에 맞는 위치)
- 테스트 실행 결과 요약
- 커버리지 추정 (메서드별 테스트 유무)
## 규칙
- 기존 테스트가 있으면 스타일을 따르기
- 외부 의존성은 Mock/Stub 사용
- 테스트 간 독립성 보장 (상태 공유 금지)
- 한국어 주석으로 테스트 의도 설명
-45
View File
@@ -1,45 +0,0 @@
---
name: hook-policy-demo
label: Hook Policy Demo
description: hooks/hook_filters 런타임 정책 적용 예시 스킬입니다.
icon: \uE943
tabs: code
allowed-tools:
- file_read
- file_edit
- grep
- build_run
when_to_use: 코드 수정 후 특정 훅만 pre/post로 선택 적용하고 싶을 때
argument-hint: 대상 파일 경로 또는 변경 목적
context: fork
agent: worker
effort: medium
sample: true
hooks:
file_edit:
pre:
- lint-pre
post:
- verify-post
build_run:
post:
- verify-post
hook_filters: lint-pre@pre@file_edit, verify-post@post@file_edit, verify-post@post@build_run
---
이 스킬은 `hooks`/`hook_filters` 정책을 함께 사용하는 예시입니다.
## 목표
- 수정 전에는 `lint-pre` 훅만 실행
- 수정 후/빌드 후에는 `verify-post` 훅만 실행
## 실행 가이드
1. `grep` + `file_read`로 대상 코드와 관련 호출부를 확인합니다.
2. 필요한 최소 범위만 `file_edit`로 수정합니다.
3. 수정 후 `build_run`으로 빌드/테스트를 실행해 검증합니다.
4. 변경 내용/검증 결과/남은 리스크를 간단히 보고합니다.
## 제약
- `allowed-tools` 목록 외 도구 호출 금지
- 불필요한 전면 리팩토링 금지
- 동일 실패 재시도 전에 원인/대안 먼저 제시
-87
View File
@@ -1,87 +0,0 @@
---
name: image-processor
label: 이미지 처리
description: Python Pillow를 사용하여 이미지 리사이즈, 크롭, 워터마크, 포맷 변환을 수행합니다.
icon: \uEB9F
allowed-tools:
- folder_map
- file_read
- file_write
- process
- image_analyze
- format_convert
- file_manage
tabs: cowork
---
사용자의 요구에 맞게 이미지를 처리하세요.
## 실행 경로 선택 (Python 가능/불가)
- 먼저 `process``python --version`을 확인하세요.
- Python 가능: 기존 Pillow 경로를 사용하세요.
- Python 불가: `image_analyze`로 이미지 상태를 점검하고, `file_manage`/`format_convert`/`file_write`로 가능한 변환 및 수동 처리 지침을 제공하세요.
## 사전 준비
먼저 Pillow 패키지가 설치되어 있지 확인하고, 없으면 설하세요:
```
process: pip install Pillow
```
## 작업 절
1. **요구사항 파악**: 처리할 이미지 파일과 원하는 작업 확인
2. **Python 스크립트 작성**: file_write로 .py 파일 생성
3. **스크립트 실행**: `process`로 Python 스크립 실행
4. **결과 확인**: 처리된 이미지 파일 경로를 사용자에게 안내
## 지원 기능
### 리사이즈
```python
from PIL import Image
img = Image.open('input.png')
img_resized = img.resize((800, 600)) # 고정 크기
# 또는 비율 유지
img.thumbnail((800, 800))
img.save('output.png')
```
### 크롭
```python
img = Image.open('input.png')
cropped = img.crop((left, top, right, bottom))
cropped.save('output.png')
```
### 워터마크
```python
from PIL import Image, ImageDraw, ImageFont
img = Image.open('input.png')
draw = ImageDraw.Draw(img)
draw.text((10, 10), "Watermark", fill=(255, 255, 255, 128))
img.save('output.png')
```
### 포맷 변환
```python
img = Image.open('input.png')
img.save('output.jpg', 'JPEG', quality=85)
img.save('output.webp', 'WEBP', quality=80)
```
### 배치 처리
```python
import glob
for path in glob.glob('*.png'):
img = Image.open(path)
img.thumbnail((800, 800))
img.save(f'resized_{path}')
```
## 추가 기능
- 회전/뒤집기 (rotate, transpose)
- 밝기/대비/선명도 조절 (ImageEnhance)
- 필터 적용 (ImageFilter: BLUR, SHARPEN, CONTOUR)
- 이미지 합성 (Image.paste, Image.alpha_composite)
- EXIF 정보 읽기
한국어로 안내하세요. 작업 폴더에 Python 스크립트와 결과 파일을 저장하세요.
-67
View File
@@ -1,67 +0,0 @@
---
name: impact
label: 변경 영향 분석
description: 코드 변경 시 영향받는 파일, 함수, 테스트를 식별합니다.
icon: \uE946
allowed-tools:
- lsp_code_intel
- grep
- search_codebase
- git_tool
- file_read
- folder_map
tabs: code
---
코드 변경이 미치는 영향 범위를 분석하여 안전한 수정을 도와주세요.
## 워크플로우
1. **변경 대상 확인**: 어떤 파일/함수/클래스를 변경할 예정인지 파악
2. **직접 참조 분석**: lsp_code_intel의 find_references로 직접 호출처 확인
3. **간접 영향 분석**:
- grep으로 문자열 기반 참조 검색
- search_codebase로 의미적 유사 코드 탐색
- 상속/인터페이스 체인 추적
4. **테스트 영향**: 관련 테스트 파일 식별
5. **위험도 평가**: 변경 영향 범위와 위험도 매트릭스 생성
6. **보고서 작성**: 영향 분석 보고서 생성
## 분석 항목
### 직접 영향
- 해당 함수/클래스를 직접 호출하는 코드
- import/using 하는 파일
- 상속받는 클래스
### 간접 영향
- 인터페이스 구현체를 통한 호출
- 리플렉션/동적 호출
- 설정 파일 참조
- UI 바인딩
### 테스트 영향
- 직접 테스트하는 테스트 파일
- 관련 통합 테스트
- 테스트 더블(Mock) 대상 여부
## 출력 형식
### 영향 분석 보고서
| 영향 수준 | 파일 | 관련 함수 | 위험도 | 비고 |
|----------|------|----------|--------|------|
| 🔴 직접 | A.cs | MethodX() | 높음 | 시그니처 변경 시 컴파일 오류 |
| 🟡 간접 | B.cs | MethodY() | 중간 | 동적 호출, 런타임 오류 가능 |
| 🟢 테스트 | A.Tests.cs | Test1() | 낮음 | 테스트 수정 필요 |
### 변경 체크리스트
- [ ] 직접 참조 N개 확인 및 수정
- [ ] 테스트 M개 업데이트
- [ ] 관련 문서 갱신
## 규칙
- 코드를 직접 수정하지 않음 (분석만 수행)
- 가능하면 LSP 기반 정확한 참조 분석 우선
- LSP 불가 시 grep 기반 텍스트 검색으로 대체
- 위험도는 보수적으로 평가 (의심스러우면 높음)
-107
View File
@@ -1,107 +0,0 @@
---
name: json-schema
label: JSON/YAML 스키마 도구
description: JSON 또는 YAML 데이터에서 스키마를 생성하고, 데이터의 유효성을 검증합니다.
icon: \uE943
tabs: code
allowed-tools:
- json_tool
- file_read
- file_write
- clipboard_tool
---
JSON/YAML 샘플 데이터에서 스키마를 추출하거나, 기존 스키마로 데이터를 검증하세요.
## 작업 절차
1. **요청 유형 판별**:
- **스키마 생성**: 샘플 JSON/YAML → JSON Schema 추출
- **스키마 검증**: 데이터 + 스키마 → 유효성 검사
- **스키마 문서화**: 기존 스키마 → 사람이 읽을 수 있는 설명 생성
2. **입력 데이터 확인**: file_read 또는 사용자 입력으로 데이터 로드
3. **스키마 생성 또는 검증 수행**: json_tool로 처리
4. **결과 출력**: 생성된 스키마 또는 검증 결과를 표시
5. **파일 저장**: file_write로 결과를 저장하거나 clipboard_tool로 복사
## 스키마 생성 규칙
### 타입 추론
| JSON 값 | JSON Schema 타입 | 추가 속성 |
|---------|-----------------|----------|
| `"text"` | `string` | — |
| `123` | `integer` | — |
| `1.5` | `number` | — |
| `true` | `boolean` | — |
| `null` | `null` | nullable 처리 |
| `[]` | `array` | items 스키마 |
| `{}` | `object` | properties 스키마 |
### 추론 강화
- **패턴 감지**: 이메일, URL, 날짜, UUID 등은 `format` 속성 추가
- **열거형 감지**: 값의 종류가 적으면 `enum` 으로 제안
- **필수 필드**: 모든 샘플에 존재하는 필드는 `required`로 표시
- **배열 항목**: 배열 내 모든 항목을 분석하여 통합 스키마 생성
### 출력 형식 (JSON Schema Draft 7)
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "사용자 정보",
"description": "사용자 프로필 데이터 스키마",
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "사용자 이름"
},
"email": {
"type": "string",
"format": "email",
"description": "이메일 주소"
},
"age": {
"type": "integer",
"minimum": 0,
"maximum": 150,
"description": "나이"
}
},
"required": ["name", "email"],
"additionalProperties": false
}
```
## 검증 결과 형식
검증 결과는 다음과 같이 표시하세요:
```
검증 결과: ❌ 실패 (3건의 오류)
| # | 경로 | 오류 | 기대값 |
|---|------|------|--------|
| 1 | $.email | 필수 필드 누락 | string (required) |
| 2 | $.age | 타입 불일치 | integer (실제: string) |
| 3 | $.tags[2] | 열거형 불일치 | "A", "B", "C" 중 하나 |
```
## 스키마 문서화 형식
기존 스키마를 분석하여 사람이 읽기 쉬운 문서를 생성:
```
## 사용자 정보 스키마
| 필드 | 타입 | 필수 | 설명 | 제약 조건 |
|------|------|------|------|----------|
| name | string | ✅ | 사용자 이름 | — |
| email | string | ✅ | 이메일 주소 | format: email |
| age | integer | — | 나이 | 0~150 |
| tags | array | — | 태그 목록 | items: string |
```
## 규칙
- JSON Schema Draft 7 형식 사용
- 스키마 생성 시 description 필드를 한국어로 작성
- 복수 샘플이 제공되면 모든 샘플을 분석하여 통합 스키마 생성
- 중첩 객체는 재귀적으로 스키마 추출
- 결과는 파일 저장과 클립보드 복사 모두 제공
한국어로 안내하세요.
-69
View File
@@ -1,69 +0,0 @@
---
name: log-analyze
label: 로그 분석기
description: 로그 파일의 패턴을 분석하고 에러를 요약하며 타임라인을 시각화합니다.
icon: \uE9D9
allowed-tools:
- file_read
- grep
- chart_create
- html_create
- data_pivot
- text_summarize
tabs: cowork
---
로그 파일을 분석하여 패턴, 에러, 추세를 파악하고 보고서를 생성하세요.
## 워크플로우
1. **로그 파일 로드**: file_read로 로그 파일 읽기
2. **패턴 분석**:
- grep으로 에러/경고/예외 패턴 검색
- 시간대별 이벤트 빈도 파악
- 반복되는 에러 패턴 식별
3. **통계 생성**: data_pivot으로 집계
4. **시각화**: chart_create로 타임라인 차트 생성
5. **보고서**: html_create로 분석 보고서 생성
## 분석 항목
### 에러 분석
- ERROR, WARN, FATAL, Exception 키워드 추출
- 에러 유형별 발생 빈도
- 최초 발생 시점 및 최근 발생 시점
- 에러 메시지 클러스터링 (유사 에러 그룹화)
### 시간대 분석
- 시간대별 로그 발생 빈도
- 피크 시간대 식별
- 에러 집중 시간대
### 패턴 분석
- 반복 패턴 (주기적 에러)
- 연쇄 에러 (A 에러 후 B 에러 발생 패턴)
- 비정상 패턴 (평소와 다른 로그량)
## 출력 형식
```
## 로그 분석 보고서
- 분석 기간: [시작] ~ [끝]
- 총 로그: N줄
### 에러 요약 (상위 10건)
| 순위 | 에러 유형 | 발생 횟수 | 최근 발생 |
|------|----------|----------|----------|
### 타임라인 차트
[시간대별 이벤트 빈도 차트]
### 상세 분석
[에러별 상세 내용 및 권장 조치]
```
## 지원 로그 형식
- 일반 텍스트 로그 (타임스탬프 자동 감지)
- JSON 로그 (각 줄이 JSON 객체)
- CSV 로그 (헤더 포함)
- syslog 형식
-174
View File
@@ -1,174 +0,0 @@
---
name: markdown-to-doc
label: Markdown → 문서 변환
description: Markdown 파일을 서식이 적용된 Word(.docx) 또는 PDF 문서로 변환합니다.
icon: \uE8A5
allowed-tools:
- file_read
- file_write
- process
- format_convert
- document_assemble
tabs: cowork
---
Markdown 파일을 전문적인 Word 또는 PDF 문서로 변환하세요.
## 실행 경로 선택 (Python 가능/불가)
- 먼저 `process``python --version`을 확인하세요.
- Python 가능: 기존 python-docx 경로를 사용하세요.
- Python 불가: `format_convert`를 우선 사용해 Markdown을 docx/pdf로 변환하고, 변환 제한 시 `file_write`로 보정 가이드를 생성하세요.
## 사전 준비
먼저 필요한 패키지가 설치되어 있는지 확인하고, 없으면 설치하세요:
```
process: pip install python-docx markdown
```
## 작업 절차
1. **Markdown 파일 확인**: file_read로 변환할 Markdown 파일의 내용과 구조를 파악
2. **변환 옵션 확인**: 사용자에게 다음 옵션을 확인
- 출력 형식: Word(.docx) 또는 PDF
- 폰트: 맑은 고딕 (기본) / 사용자 지정
- 여백, 페이지 크기 설정
- 머리글/바닥글 포함 여부
3. **Python 스크립트 작성**: file_write로 변환 스크립트 생성
4. **스크립트 실행**: `process`로 Python 스크립트 실행
5. **결과 확인**: 생성된 문서 파일 경로와 페이지 수를 안내
## 스타일 매핑
| Markdown | Word 스타일 | 설명 |
|----------|------------|------|
| `# 제목` | Heading 1 | 16pt, 굵게 |
| `## 소제목` | Heading 2 | 14pt, 굵게 |
| `### 항목` | Heading 3 | 12pt, 굵게 |
| 본문 텍스트 | Normal | 11pt |
| `**굵게**` | Bold run | 굵게 |
| `*기울임*` | Italic run | 기울임 |
| `` `코드` `` | 코드 스타일 | Consolas, 배경색 |
| `> 인용` | Quote | 들여쓰기 + 왼쪽 테두리 |
| `- 목록` | List Bullet | 글머리 기호 |
| `1. 번호` | List Number | 번호 목록 |
| 표 | Table Grid | 테두리 표 |
| `---` | 페이지 구분 | 가로선 → 페이지 나누기 |
| 코드 블록 | 코드 단락 | Consolas, 회색 배경 |
## Python 스크립트 템플릿
```python
import re
from docx import Document
from docx.shared import Inches, Pt, Cm, RGBColor
from docx.enum.text import WD_ALIGN_PARAGRAPH
from docx.oxml.ns import qn
import markdown
# Markdown 파일 읽기
with open('input.md', 'r', encoding='utf-8') as f:
md_content = f.read()
doc = Document()
# 기본 스타일 설정
style = doc.styles['Normal']
font = style.font
font.name = '맑은 고딕'
font.size = Pt(11)
style.element.rPr.rFonts.set(qn('w:eastAsia'), '맑은 고딕')
# 여백 설정
for section in doc.sections:
section.top_margin = Cm(2.54)
section.bottom_margin = Cm(2.54)
section.left_margin = Cm(3.17)
section.right_margin = Cm(3.17)
# Markdown 파싱 및 변환
lines = md_content.split('\n')
i = 0
while i < len(lines):
line = lines[i]
# 제목 (Heading)
if line.startswith('#'):
level = len(line) - len(line.lstrip('#'))
text = line.lstrip('#').strip()
doc.add_heading(text, level=min(level, 4))
# 코드 블록
elif line.startswith('```'):
code_lines = []
i += 1
while i < len(lines) and not lines[i].startswith('```'):
code_lines.append(lines[i])
i += 1
p = doc.add_paragraph()
run = p.add_run('\n'.join(code_lines))
run.font.name = 'Consolas'
run.font.size = Pt(9)
# 인용
elif line.startswith('>'):
text = line.lstrip('>').strip()
p = doc.add_paragraph(text)
p.paragraph_format.left_indent = Cm(1.27)
# 글머리 기호
elif line.startswith('- ') or line.startswith('* '):
text = line[2:].strip()
doc.add_paragraph(text, style='List Bullet')
# 번호 목록
elif re.match(r'^\d+\.\s', line):
text = re.sub(r'^\d+\.\s', '', line).strip()
doc.add_paragraph(text, style='List Number')
# 가로선 → 페이지 나누기
elif line.strip() in ('---', '***', '___'):
doc.add_page_break()
# 빈 줄
elif line.strip() == '':
pass
# 일반 텍스트
else:
p = doc.add_paragraph()
# 굵게, 기울임 처리
parts = re.split(r'(\*\*.*?\*\*|\*.*?\*|`.*?`)', line)
for part in parts:
if part.startswith('**') and part.endswith('**'):
run = p.add_run(part[2:-2])
run.bold = True
elif part.startswith('*') and part.endswith('*'):
run = p.add_run(part[1:-1])
run.italic = True
elif part.startswith('`') and part.endswith('`'):
run = p.add_run(part[1:-1])
run.font.name = 'Consolas'
run.font.size = Pt(10)
else:
p.add_run(part)
i += 1
# 저장
doc.save('output.docx')
print(f'변환 완료: output.docx ({len(doc.paragraphs)}개 단락)')
```
## 표 변환
Markdown 표가 있으면 Word 표로 변환합니다:
- 헤더 행: 굵게, 배경색 적용
- 셀 정렬: Markdown의 `:---`, `:---:`, `---:` 구문 반영
- 테두리: 전체 셀에 얇은 테두리
## 규칙
- 원본 Markdown 파일은 수정하지 않음
- 인코딩: UTF-8 기본
- 이미지 링크(`![](path)`)는 로컬 파일이면 삽입, URL이면 경로만 표시
- 복잡한 Markdown(수식, 다이어그램)은 지원 범위와 한계를 안내
- 출력 파일명: 원본 파일명 기준 (.md → .docx)
한국어로 안내하세요. 작업 폴더에 Python 스크립트와 결과 파일을 저장하세요.
-45
View File
@@ -1,45 +0,0 @@
---
name: meeting-minutes
label: 회의록 정리
description: 회의 내용을 체계적으로 정리하여 회의록을 생성합니다.
icon: \uE771
allowed-tools:
- file_read
- file_write
- text_summarize
- template_render
tabs: cowork
---
사용자가 제공한 회의 내용(텍스트, 메모, 파일)을 정리하여 체계적인 회의록을 작성하세요.
다음 도구를 사용하세요:
1. file_read — 회의 관련 파일 읽기 (필요 시)
2. file_write — 회의록 파일 생성
회의록 형식:
## 회의 정보
- 일시:
- 참석자:
- 장소/방법:
- 주제:
## 안건 및 논의 내용
각 안건별로:
- **안건**: 주제
- **논의 내용**: 주요 발언 및 의견 정리
- **결정 사항**: 합의된 내용
## 액션 아이템
| 번호 | 담당자 | 내용 | 기한 | 비고 |
|------|--------|------|------|------|
| 1 | - | - | - | - |
## 다음 회의
- 예정일:
- 주요 안건:
규칙:
- 핵심 내용 위주로 간결하게 정리
- 결정 사항과 액션 아이템은 명확하게 기술
- 한국어로 작성
-118
View File
@@ -1,118 +0,0 @@
---
name: ocr-extract
label: OCR 텍스트 추출
description: Python pytesseract를 사용하여 이미지/스캔 문서에서 텍스트를 추출합니다.
icon: \uE8D4
allowed-tools:
- file_read
- file_write
- process
- image_analyze
- text_summarize
tabs: cowork
---
이미지 또는 스캔된 문서에서 텍스트를 추출하세요.
## 실행 경로 선택 (Python 가능/불가)
- 먼저 `process``python --version`을 확인하세요.
- Python 가능: 기존 pytesseract 경로를 사용하세요.
- Python 불가: `image_analyze`로 텍스트 후보를 추출하고 `text_summarize` + `file_write`로 정제본을 제공하세요.
## 사전 준비
1. Tesseract OCR 엔진이 시스템에 설치되어 있어야 합니다.
- Windows: https://github.com/UB-Mannheim/tesseract/wiki 에서 설치
- 한국어 지원: 설치 시 "Korean" 언어 데이터 선택
2. Python 패키지 설치:
```
process: pip install pytesseract Pillow
```
## 작업 절차
1. **이미지 확인**: 사용자가 제공한 이미지 파일 확인
2. **전처리 (선택)**: 이미지 품질이 낮으면 전처리 스크립트 적용
3. **OCR 실행**: pytesseract로 텍스트 추출
4. **결과 저장**: 추출된 텍스트를 파일로 저장하고 사용자에게 안내
## OCR 스크립트 템플릿
### 기본 텍스트 추출
```python
import pytesseract
from PIL import Image
# Windows에서 Tesseract 경로 지정 (필요 시)
# pytesseract.pytesseract.tesseract_cmd = r'C:\Program Files\Tesseract-OCR\tesseract.exe'
img = Image.open('scan.png')
text = pytesseract.image_to_string(img, lang='kor+eng')
print(text)
with open('extracted.txt', 'w', encoding='utf-8') as f:
f.write(text)
```
### 이미지 전처리 (품질 개선)
```python
from PIL import Image, ImageFilter, ImageEnhance
img = Image.open('scan.png')
# 그레이스케일 변환
img = img.convert('L')
# 대비 향상
enhancer = ImageEnhance.Contrast(img)
img = enhancer.enhance(2.0)
# 선명도 향상
img = img.filter(ImageFilter.SHARPEN)
# 이진화 (흑백)
threshold = 128
img = img.point(lambda x: 255 if x > threshold else 0)
img.save('preprocessed.png')
text = pytesseract.image_to_string(img, lang='kor+eng')
```
### 배치 OCR (여러 이미지)
```python
import glob
import pytesseract
from PIL import Image
results = []
for path in sorted(glob.glob('*.png')):
img = Image.open(path)
text = pytesseract.image_to_string(img, lang='kor+eng')
results.append(f'--- {path} ---\n{text}\n')
with open('all_extracted.txt', 'w', encoding='utf-8') as f:
f.write('\n'.join(results))
```
### 표 영역 추출
```python
import pytesseract
from PIL import Image
img = Image.open('table.png')
# TSV 형식으로 추출 (표 구조 보존)
tsv_data = pytesseract.image_to_data(img, lang='kor+eng', output_type=pytesseract.Output.DATAFRAME)
print(tsv_data)
```
## 지원 언어
- `kor` — 한국어
- `eng` — 영어
- `kor+eng` — 한국어+영어 혼합 (권장)
- `jpn` — 일본어
- `chi_sim` — 중국어 간체
## 팁
- 스캔 해상도 300dpi 이상이면 인식률이 높습니다
- 기울어진 이미지는 `img.rotate()` 로 보정 후 추출하세요
- 손글씨는 인식률이 낮을 수 있습니다
한국어로 안내하세요. 작업 폴더에 Python 스크립트와 결과 파일을 저장하세요.
-59
View File
@@ -1,59 +0,0 @@
---
name: paper-review
label: 논문 분석
description: 논문 또는 기술 문서를 체계적으로 분석하고 핵심 내용을 정리합니다.
icon: \uE736
allowed-tools:
- document_read
- file_read
- file_write
- text_summarize
tabs: cowork
---
사용자가 제공한 논문 또는 기술 문서를 체계적으로 분석하세요.
## 파일 읽기 전략
- **PDF 파일**: 반드시 `document_read` 도구를 사용하세요. 페이지 범위 지정이 가능합니다.
- 먼저 전체 페이지 수를 확인한 후, 초록(1-2페이지)→본문→참고문헌 순으로 읽으세요.
- `file_read`로 PDF를 읽으면 텍스트가 깨질 수 있으므로 사용하지 마세요.
- **텍스트 파일** (.txt, .md, .html 등): `file_read` 도구를 사용하세요.
## 사용 도구
1. document_read — PDF 논문 파일 읽기 (페이지 범위 지정, 초록/참고문헌 추출)
2. file_read — 텍스트 기반 문서 파일 읽기
3. file_write — 분석 보고서 생성
## 분석 항목
## 논문 개요
- 제목, 저자, 발표 연도/학회
- 연구 분야 및 키워드
## 연구 목적 및 배경
- 연구 문제 정의
- 기존 연구의 한계점
## 방법론
- 제안 방법의 핵심 아이디어
- 실험 설계 및 데이터셋
## 주요 결과
- 핵심 실험 결과 (표/수치 인용)
- 기존 방법 대비 개선점
## 한계점 및 향후 연구
- 저자가 인정한 한계
- 발전 가능성
## 실무 적용 가능성
- 우리 업무에 적용할 수 있는 포인트
- 기술 도입 시 고려사항
## 작업 절차
1. 사용자가 파일명을 언급하면 작업 폴더에서 해당 파일을 찾아 읽기
2. PDF인 경우 `document_read`로 초록(1-2p) 먼저 읽어 전체 구조 파악
3. 본문을 페이지 범위별로 나누어 순차 읽기
4. 위 분석 항목에 따라 체계적으로 정리
5. `file_write`로 분석 보고서를 마크다운 파일로 저장
한국어로 작성하고, 전문 용어는 원문과 함께 표기하세요.
-74
View File
@@ -1,74 +0,0 @@
---
name: pdf-processor
label: PDF 처리
description: Python을 사용하여 PDF에서 텍스트/표를 추출하거나 PDF를 생성합니다.
icon: \uE9F9
allowed-tools:
- folder_map
- document_read
- file_read
- file_write
- process
- format_convert
tabs: cowork
---
PDF 파일을 읽거나 새 PDF를 생성하세요.
## 실행 경로 선택 (Python 가능/불가)
- 먼저 `process``python --version`을 확인하세요.
- Python 가능: 기존 pypdf/pdfplumber/reportlab 경로를 사용하세요.
- Python 불가: `document_read`로 텍스트/구조를 추출하고, 생성 작업은 `format_convert` + `file_write` 기반으로 대체하세요.
## 사전 준비
필요한 패키지를 확인하고 설치하세요:
```
process: pip install pypdf pdfplumber reportlab
```
## 작업 절차
### PDF 텍스트 추출
1. **파일 확인**: folder_map으로 PDF 파일 위치 확인
2. **추출 스크립트 작성**: file_write로 Python 스크립트 생성
3. **실행**: `process`로 실행
4. **결과 전달**: 추출된 텍스트를 사용자에게 전달
### PDF 생성
1. **내용 파악**: 사용자가 원하는 문서 내용 확인
2. **생성 스크립트 작성**: file_write로 Python 스크립트 생성
3. **실행 및 확인**: `process`로 실행
## 텍스트 추출 템플릿
```python
import pdfplumber
import json
results = []
with pdfplumber.open('input.pdf') as pdf:
for i, page in enumerate(pdf.pages):
text = page.extract_text() or ''
tables = page.extract_tables() or []
results.append({
'page': i + 1,
'text': text,
'tables': tables,
})
with open('pdf_extracted.json', 'w', encoding='utf-8') as f:
json.dump(results, f, ensure_ascii=False, indent=2)
for r in results:
print(f"--- 페이지 {r['page']} ---")
print(r['text'][:500])
```
## 지원 기능
- 텍스트 추출 (페이지별)
- 표 추출 (구조 보존)
- PDF 병합 / 분할
- PDF 생성 (reportlab)
- 페이지 회전
- 메타데이터 읽기
한국어로 안내하세요. 원본 PDF는 수정하지 마세요.
-94
View File
@@ -1,94 +0,0 @@
---
name: perf-audit
label: 성능 감사
description: 코드 복잡도, 성능 병목, 메모리 이슈를 분석하고 최적화 방안을 제시합니다.
icon: \uE9D9
allowed-tools:
- file_read
- grep
- folder_map
- search_codebase
- glob
- html_create
tabs: code
---
코드베이스의 성능 관련 이슈를 분석하고 최적화 보고서를 생성하세요.
## 워크플로우
1. **프로젝트 분석**: folder_map으로 구조 파악, 언어/프레임워크 식별
2. **복잡도 분석**: 파일별 줄 수, 메서드 크기, 중첩 깊이 측정
3. **성능 안티패턴 탐지**: grep으로 알려진 성능 이슈 패턴 검색
4. **메모리 이슈 탐지**: 리소스 해제 누락, 대용량 할당 패턴
5. **보고서 생성**: html_create로 성능 감사 보고서
## 분석 항목
### 코드 복잡도 지표
- **파일 크기**: 500줄 이상 파일 식별
- **메서드 크기**: 50줄 이상 메서드 식별
- **중첩 깊이**: 4단계 이상 들여쓰기
- **매개변수 수**: 5개 이상 파라미터 메서드
### 성능 안티패턴
#### 데이터베이스
- N+1 쿼리 패턴 (루프 내 DB 호출)
- SELECT * 사용 (불필요한 컬럼 로드)
- 인덱스 미사용 쿼리 힌트
#### 메모리
- IDisposable 미해제 (using 미사용)
- 대용량 문자열 결합 (StringBuilder 미사용)
- 정적 컬렉션 무한 증가
- 이벤트 핸들러 미해제 (메모리 누수)
#### I/O
- 동기 파일 I/O (async 미사용)
- 동기 네트워크 호출
- 불필요한 직렬화/역직렬화
#### 알고리즘
- O(n²) 이상 루프 (중첩 foreach/for)
- 반복 계산 (캐싱 미적용)
- LINQ 체인의 불필요한 ToList()
#### 프론트엔드
- 불필요한 리렌더링 패턴
- 대용량 번들 임포트
- 이미지 최적화 미적용
### .NET 전용 패턴
```
탐지 대상:
- Task.Result / .Wait() (데드락 위험)
- lock 내부 async 호출
- GC.Collect() 직접 호출
- Reflection 반복 사용
- string + string 반복 (루프 내)
```
## 출력 형식
### 성능 감사 보고서
**요약 대시보드**
| 지표 | 값 | 상태 |
|------|-----|------|
| 총 파일 수 | ... | — |
| 대형 파일 (500줄+) | ... | ⚠ |
| 대형 메서드 (50줄+) | ... | ⚠ |
| 성능 안티패턴 | ... | 🔴 |
| 메모리 이슈 | ... | 🟡 |
**상세 이슈 목록**
| 우선순위 | 파일 | 라인 | 이슈 | 영향 | 권장 조치 |
|---------|------|------|------|------|----------|
| 🔴 높음 | ... | ... | N+1 쿼리 | 응답 지연 | 일괄 로드 |
## 규칙
- 코드를 수정하지 않음 (분석 + 보고서만)
- 성능 이슈는 영향도와 수정 난이도를 함께 평가
- 추측보다 패턴 기반 탐지 우선
- 한국어로 작성
-123
View File
@@ -1,123 +0,0 @@
---
name: pptx-creator
label: PPT 프레젠테이션 생성
description: Python을 사용하여 전문적인 PowerPoint 프레젠테이션을 생성합니다. 작업 폴더의 양식 파일을 자동 활용합니다.
icon: \uE7BE
allowed-tools:
- folder_map
- document_read
- file_read
- file_write
- process
- pptx_create
- template_render
tabs: cowork
---
사용자의 요구에 맞는 PowerPoint 프레젠테이션을 Python으로 생성하세요.
## 실행 경로 선택 (Python 가능/불가)
- 먼저 `process``python --version`을 확인하세요.
- Python 가능: 기존 python-pptx 경로를 사용하세요.
- Python 불가: `pptx_create`로 슬라이드 초안을 생성하고 `template_render` + `file_write`로 발표자료 구조를 보강하세요.
## 사전 준비
필요한 패키지를 확인하고 설치하세요:
```
process: pip install python-pptx
```
## 양식 활용 (마스터 슬라이드 템플릿)
작업 폴더에 PPT 양식이 있으면 **반드시** 활용하세요:
1. **양식 탐색**: `folder_map`으로 작업 폴더를 스캔하여 `.pptx` 파일 확인
2. **양식 후보 판별**:
- 파일명에 "양식", "template", "서식", "표준", "기본" 포함
- 또는 사용자가 명시적으로 "XX 양식으로 작성해줘" 요청
- 또는 사용자가 특정 .pptx 파일명을 언급
3. **양식 구조 파악**: `document_read`로 양식의 슬라이드 레이아웃 목록 확인
4. **양식 기반 생성**:
```python
prs = Presentation('양식_발표.pptx')
# 마스터 슬라이드의 배경, 로고, 색 테마, 폰트가 자동 상속
# 기존 슬라이드 제거 후 새 내용 추가
while len(prs.slides) > 0:
rId = prs.slides._sldIdLst[0].rId
prs.part.drop_rel(rId)
del prs.slides._sldIdLst[0]
```
5. **레이아웃 확인** (양식마다 다를 수 있음):
```python
for i, layout in enumerate(prs.slide_layouts):
print(f'{i}: {layout.name}')
```
6. **양식이 없으면**: 아래 기본 템플릿으로 새 프레젠테이션 생성
## 작업 절차
1. **요구사항 파악**: 발표 주제, 슬라이드 수, 스타일 확인
2. **양식 확인**: folder_map으로 작업 폴더에 양식 .pptx 파일이 있는지 확인
3. **스크립트 작성**: file_write로 Python 스크립트 생성
4. **실행**: `process`로 스크립트 실행
5. **결과 안내**: 생성된 .pptx 파일 경로를 사용자에게 전달
## 스크립트 템플릿
```python
from pptx import Presentation
from pptx.util import Inches, Pt, Emu
from pptx.dml.color import RGBColor
from pptx.enum.text import PP_ALIGN
import os
# 양식 파일 자동 감지
template_keywords = ['양식', 'template', '서식', '표준', '기본']
template_file = None
for f in os.listdir('.'):
if f.endswith('.pptx') and any(kw in f.lower() for kw in template_keywords):
template_file = f
break
# 양식이 있으면 활용, 없으면 새 프레젠테이션
if template_file:
prs = Presentation(template_file)
# 기존 슬라이드 제거 (마스터/레이아웃은 유지)
while len(prs.slides) > 0:
rId = prs.slides._sldIdLst[0].rId
prs.part.drop_rel(rId)
del prs.slides._sldIdLst[0]
print(f'양식 활용: {template_file}')
print(f'사용 가능한 레이아웃: {[l.name for l in prs.slide_layouts]}')
else:
prs = Presentation()
prs.slide_width = Inches(13.333)
prs.slide_height = Inches(7.5)
# 제목 슬라이드
slide = prs.slides.add_slide(prs.slide_layouts[0])
slide.shapes.title.text = '프레젠테이션 제목'
if len(slide.placeholders) > 1:
slide.placeholders[1].text = '부제목'
# 내용 슬라이드
slide = prs.slides.add_slide(prs.slide_layouts[1])
slide.shapes.title.text = '섹션 제목'
body = slide.placeholders[1]
body.text = '첫 번째 포인트'
p = body.text_frame.add_paragraph()
p.text = '두 번째 포인트'
prs.save('presentation.pptx')
print('프레젠테이션 생성 완료: presentation.pptx')
```
## 지원 기능
- 제목/내용/빈 슬라이드 레이아웃
- 텍스트 서식 (글꼴, 크기, 색상, 정렬)
- 표 삽입
- 이미지 삽입
- 도형 (사각형, 원, 화살표)
- 차트 (막대, 선, 원형)
- 슬라이드 번호
- 마스터 슬라이드 커스터마이징
- **양식 파일 기반 마스터/레이아웃 상속** (배경, 로고, 색 테마, 폰트 자동 유지)
한국어로 안내하세요. 작업 폴더에 결과 파일을 저장하세요.
-100
View File
@@ -1,100 +0,0 @@
---
name: prd-generator
label: 요구사항 정의서 (PRD)
description: 제품 요구사항 정의서, 유저 스토리, 수용 기준을 체계적으로 생성합니다.
icon: \uE8A5
allowed-tools:
- file_read
- file_write
- html_create
- docx_create
- document_plan
- document_assemble
tabs: cowork
---
제품/기능의 요구사항 정의서(PRD)를 체계적으로 작성하세요.
## 워크플로우
1. **요구사항 수집**: 사용자에게 다음을 확인
- 제품/기능 이름
- 목적과 배경
- 대상 사용자
- 핵심 기능 목록
- 제약 조건 (기술, 일정, 예산)
2. **구조화**: document_plan으로 PRD 개요 설계
3. **상세 작성**: 섹션별 상세 내용 작성
- 유저 스토리 (As a... I want... So that...)
- 수용 기준 (Given... When... Then...)
- 기능 우선순위 (MoSCoW)
4. **문서 생성**: document_assemble 또는 html_create로 최종 문서
## PRD 구조
### 1. 개요
- 제품/기능 이름
- 버전 / 작성일 / 작성자
- 문서 목적
### 2. 배경 및 목적
- 비즈니스 배경
- 해결하려는 문제
- 기대 효과 (정량적 KPI)
### 3. 대상 사용자
- 사용자 페르소나
- 사용 시나리오
- 사용자 여정 맵
### 4. 기능 요구사항
#### 유저 스토리 형식
```
US-001: [기능명]
As a [역할],
I want [기능],
So that [가치].
수용 기준:
- Given [사전 조건], When [행동], Then [기대 결과]
- Given ..., When ..., Then ...
우선순위: Must Have / Should Have / Could Have / Won't Have
```
### 5. 비기능 요구사항
- 성능 (응답 시간, 처리량)
- 보안 (인증, 권한, 암호화)
- 접근성 (WCAG 수준)
- 호환성 (브라우저, OS, 디바이스)
### 6. 기술 제약
- 기술 스택 제한
- 연동 시스템
- 데이터 마이그레이션
### 7. 일정 및 마일스톤
| 마일스톤 | 예정일 | 산출물 |
|---------|--------|--------|
| 설계 완료 | ... | 상세 설계서 |
| 개발 완료 | ... | 릴리즈 빌드 |
| QA 완료 | ... | 테스트 보고서 |
### 8. 성공 지표
- 핵심 KPI 및 측정 방법
- 목표 수치
### 9. 리스크 및 대안
| 리스크 | 영향 | 대안 |
|--------|------|------|
| ... | 높음 | ... |
## 규칙
- 사용자 관점에서 작성 (기술 용어 최소화)
- 유저 스토리는 INVEST 원칙 준수 (Independent, Negotiable, Valuable, Estimable, Small, Testable)
- 수용 기준은 테스트 가능하도록 구체적으로
- 한국어로 작성 (영어 용어 병기 가능)
-72
View File
@@ -1,72 +0,0 @@
---
name: refactor
label: 리팩토링 가이드
description: 코드베이스를 분석하여 리팩토링 포인트를 식별하고 실행 계획을 생성합니다.
icon: \uE90F
allowed-tools:
- search_codebase
- grep
- file_read
- code_review
- folder_map
- lsp_code_intel
tabs: code
# hooks / hook_filters 예시 (옵션):
# hooks:
# file_edit:
# pre:
# - lint-pre
# post: verify-post
# hook_filters: lint-pre@pre@file_edit, verify-post@post@*
---
코드베이스를 분석하여 리팩토링이 필요한 부분을 식별하고 개선 계획을 수립하세요.
## 워크플로우
1. **구조 파악**: folder_map + grep으로 프로젝트 전체 구조 분석
2. **코드 스멜 탐지**:
- 중복 코드 (grep으로 유사 패턴 검색)
- 긴 메서드/클래스 (file_read로 크기 확인)
- 복잡한 조건문 (중첩 if/switch)
- 미사용 코드 (lsp_code_intel로 참조 확인)
3. **의존성 분석**: lsp_code_intel로 참조 관계 파악
4. **우선순위 결정**: 영향도 × 난이도 매트릭스
5. **리팩토링 계획 생성**: 단계별 실행 계획
## 분석 항목
### 코드 스멜 (Code Smells)
- **중복 코드**: 3곳 이상 반복되는 유사 코드
- **거대 클래스**: 500줄 이상의 클래스
- **긴 메서드**: 50줄 이상의 메서드
- **매개변수 과다**: 5개 이상 파라미터
- **의존성 순환**: 상호 참조 관계
- **매직 넘버**: 하드코딩된 숫자/문자열
- **깊은 중첩**: 4단계 이상 들여쓰기
### 리팩토링 기법 (제안)
- Extract Method / Extract Class
- Rename (변수, 메서드, 클래스)
- Move Method / Move Field
- Replace Conditional with Polymorphism
- Introduce Parameter Object
- Remove Dead Code
## 출력 형식
### 리팩토링 보고서
| 우선순위 | 파일 | 이슈 | 제안 | 영향도 | 난이도 |
|---------|------|------|------|--------|--------|
| 🔴 높음 | ... | ... | ... | ★★★ | ★☆☆ |
| 🟡 중간 | ... | ... | ... | ★★☆ | ★★☆ |
### 실행 계획
1. [안전한 변경부터] ...
2. [테스트 추가 후] ...
3. [구조 변경] ...
## 규칙
- 코드를 직접 수정하지 않음 (분석 + 계획만)
- 기존 테스트가 있으면 테스트 커버리지 확인
- 팀 컨벤션/스타일 가이드 존중
-86
View File
@@ -1,86 +0,0 @@
---
name: regex-helper
label: 정규식 도우미
description: 정규식 패턴을 생성하고, 기존 패턴을 해석하며, 테스트 케이스로 검증합니다.
icon: \uE8FD
tabs: code
allowed-tools:
- regex_tool
- clipboard_tool
- file_read
---
사용자의 요구에 맞는 정규식 패턴을 작성하거나, 기존 패턴을 해석하고 테스트하세요.
## 작업 절차
1. **요구사항 파악**: 사용자의 요청 유형을 판별
- **패턴 생성**: "이메일 주소를 찾는 정규식 만들어줘"
- **패턴 해석**: "이 정규식이 무슨 뜻이야? `^[\w.-]+@[\w.-]+\.\w+$`"
- **패턴 테스트**: "이 패턴이 이 문자열에 매칭되는지 확인해줘"
2. **패턴 작성 또는 분석**:
- 생성: 요구사항을 분석하여 정규식 패턴 작성
- 해석: 패턴을 구성 요소별로 분해하여 설명
3. **테스트 수행**: regex_tool로 패턴을 테스트 케이스에 적용
- 매칭되어야 할 문자열 (positive cases)
- 매칭되지 않아야 할 문자열 (negative cases)
4. **결과 설명**: 매칭 결과와 캡처 그룹을 상세히 설명
5. **최적화 제안**: 성능 또는 가독성 개선이 가능하면 대안 제시
## 패턴 생성 가이드
### 자주 사용되는 패턴
| 용도 | 패턴 | 설명 |
|------|------|------|
| 이메일 | `[\w.-]+@[\w.-]+\.\w{2,}` | 기본 이메일 형식 |
| 전화번호 (한국) | `0\d{1,2}-\d{3,4}-\d{4}` | 010-1234-5678 형식 |
| 날짜 (YYYY-MM-DD) | `\d{4}-(?:0[1-9]\|1[0-2])-(?:0[1-9]\|[12]\d\|3[01])` | ISO 날짜 형식 |
| IP 주소 | `(?:\d{1,3}\.){3}\d{1,3}` | IPv4 기본 |
| URL | `https?://[\w.-]+(?:/[\w./?#&=-]*)?` | HTTP/HTTPS URL |
| 한글만 | `[가-힣]+` | 한글 문자 |
| 사업자등록번호 | `\d{3}-\d{2}-\d{5}` | 123-45-67890 형식 |
### 패턴 해석 형식
패턴을 해석할 때는 다음 구조로 설명하세요:
```
패턴: ^(\d{3})-(\d{2})-(\d{5})$
해석:
^ → 문자열 시작
(\d{3}) → 캡처 그룹 1: 숫자 3자리
- → 하이픈 (리터럴)
(\d{2}) → 캡처 그룹 2: 숫자 2자리
- → 하이픈 (리터럴)
(\d{5}) → 캡처 그룹 3: 숫자 5자리
$ → 문자열 끝
```
## 테스트 형식
테스트 결과는 다음 형식으로 표시하세요:
```
패턴: \d{3}-\d{2}-\d{5}
✅ 매칭 성공:
"123-45-67890" → 전체 매칭: "123-45-67890"
"사업자번호: 123-45-67890입니다" → 부분 매칭: "123-45-67890"
❌ 매칭 실패:
"12-345-67890" → 형식 불일치
"abc-de-fghij" → 숫자가 아닌 문자
```
## 플래그 안내
| 플래그 | 설명 |
|--------|------|
| `i` | 대소문자 무시 |
| `m` | 멀티라인 (^$가 각 줄에 적용) |
| `s` | 점(.)이 줄바꿈도 매칭 |
| `g` | 전역 검색 (모든 매칭) |
## 규칙
- 패턴 생성 시 positive/negative 테스트 케이스를 반드시 포함
- 복잡한 패턴은 주석이 포함된 확장 모드(`x`)로 설명
- 캡처 그룹이 있으면 각 그룹의 의미를 설명
- 성능에 민감한 경우 탐욕적/게으른 수량자 선택 이유를 설명
- 최종 패턴은 clipboard_tool로 클립보드에 복사
한국어로 안내하세요.
-96
View File
@@ -1,96 +0,0 @@
---
name: release-note
label: 릴리즈 노트 생성
description: 비개발자 대상의 사용자 친화적인 릴리즈 노트를 자동 생성합니다.
icon: \uE70B
tabs: code
allowed-tools:
- file_read
- file_write
- clipboard_tool
- html_create
---
사용자(비개발자) 대상의 읽기 쉬운 릴리즈 노트를 작성하세요.
## 작업 절차
1. **변경 정보 수집**: 사용자에게 다음 정보를 확인
- 버전 번호 (예: v1.6.0)
- 릴리즈 날짜
- 주요 변경 내용 (기능 추가, 개선, 버그 수정)
- 참고 자료: CHANGELOG, 커밋 이력, 개발 문서 등
2. **내용 분류**: 수집된 변경 사항을 사용자 관점으로 분류
- 기술 용어 → 사용자 언어로 변환
- 내부 리팩토링 등 사용자에게 무관한 항목은 제외
3. **릴리즈 노트 초안 작성**: 아래 템플릿 기반으로 작성
4. **사용자 검토**: 초안을 보여주고 수정 요청 반영
5. **최종 출력**: Markdown, HTML, 또는 텍스트 형식으로 저장
## 릴리즈 노트 템플릿
```markdown
# [제품명] v[버전] 업데이트 안내
안녕하세요. [제품명] v[버전] 업데이트 소식을 안내드립니다.
## 🎉 새로운 기능
- **[기능 이름]**: [사용자가 얻는 혜택 중심으로 1~2문장 설명]
- **[기능 이름]**: [사용자가 얻는 혜택 중심으로 1~2문장 설명]
## ✨ 개선 사항
- **[개선 영역]**: [무엇이 어떻게 좋아졌는지 설명]
- **[개선 영역]**: [무엇이 어떻게 좋아졌는지 설명]
## 🐛 문제 해결
- [사용자가 겪었던 문제]를 해결했습니다.
- [사용자가 겪었던 문제]를 해결했습니다.
## ⚠️ 알려진 이슈
- [현재 알려진 제한 사항이나 이슈]
- [해결 예정 시기 또는 임시 해결 방법]
---
문의 사항이 있으시면 [연락처/채널]로 알려주세요.
감사합니다.
```
## 작성 원칙
### 사용자 언어로 변환
| 개발 용어 (사용 금지) | 사용자 표현 (사용) |
|---------------------|-------------------|
| API 엔드포인트 추가 | 새로운 연동 기능 추가 |
| 메모리 누수 수정 | 장시간 사용 시 느려지는 문제 해결 |
| UI 리팩토링 | 화면 디자인 개선 |
| 캐시 최적화 | 실행 속도 개선 |
| null 참조 오류 수정 | 예기치 않은 오류로 종료되는 문제 해결 |
| 인코딩 이슈 수정 | 한글이 깨져 보이는 문제 해결 |
| 동시성 버그 수정 | 여러 작업 동시 실행 시 오류 발생 문제 해결 |
### 작성 규칙
- **혜택 중심**: "무엇을 했다"가 아닌 "사용자에게 어떤 도움이 되는지" 설명
- **간결**: 한 항목당 1~2문장 이내
- **구체적**: "성능 개선" → "파일 열기 속도가 약 2배 빨라졌습니다"
- **긍정적 톤**: 문제를 "해결했습니다", 기능을 "추가했습니다"
- **이모지 활용**: 섹션별 시각적 구분 (새 기능: 🎉, 개선: ✨, 수정: 🐛, 주의: ⚠️)
## 출력 형식
- **Markdown**: 기본 출력 형식 (.md)
- **HTML**: html_create로 스타일이 적용된 웹 페이지 생성
- **텍스트**: 이메일 본문용 서식 없는 텍스트
- 사용자가 원하는 형식으로 제공
## 규칙
- 내부 기술 구현 세부사항은 노출하지 않음
- 사용자에게 무관한 변경(코드 리팩토링, 테스트 추가 등)은 제외
- Breaking Change가 있으면 "이전 버전과 달라진 점" 섹션 추가
- 업데이트 방법 안내를 포함 (인스톨러 경로, 주의사항)
- 최종 결과는 파일 저장과 클립보드 복사 모두 제공
한국어로 안내하세요.
-41
View File
@@ -1,41 +0,0 @@
---
name: report-writer
label: 보고서 작성
description: 작업 폴더의 데이터를 분석하여 체계적인 업무 보고서를 생성합다.
icon: \uE9F9
allowed-tools:
- folder_map
- file_read
- file_write
- data_pivot
- chart_create
- template_render
- text_summarize
tabs: cowork
---
작업 폴더의 파일과 데이터를 분석하여 업무 보고서를 작성하세요.
다음 도구를 사용하세요:
1. folder_map — 작업 폴더의 파일 구조 파악
2. file_read — 관련 데이터 파일 읽기 (CSV, Excel, 텍스트)
3. file_write — 보고서 파일 생성 (HTML 또는 Markdown)
보고서 구성:
## 제목
- 작성 일시, 작성자 (요청 시)
## 요약 (Executive Summary)
- 핵심 내용을 3줄 이내로 요약
## 본문
- 데이터 기반 분석 결과
- 표/차트를 활용한 시각적 정리
- 주요 발견 사항
## 결론 및 제안
- 결론 요약
- 향후 조치 사항
HTML 보고서 생성 시 현재 적용된 디자인 무드를 반영하세요.
한국어로 작성하세요.
-93
View File
@@ -1,93 +0,0 @@
---
name: security-audit
label: 보안 코드 감사
description: 코드베이스의 보안 취약점을 점검하고 OWASP 기반 보안 보고서를 생성합니다.
icon: \uE72E
allowed-tools:
- file_read
- grep
- folder_map
- search_codebase
- html_create
- glob
tabs: code
---
코드베이스의 보안 취약점을 체계적으로 점검하고 보고서를 생성하세요.
## 워크플로우
1. **스캔 범위 확인**: folder_map으로 프로젝트 구조 파악, 언어/프레임워크 식별
2. **취약점 패턴 탐지**: grep으로 위험 패턴 검색
3. **의존성 분석**: 패키지 파일(package.json, *.csproj, requirements.txt) 읽기
4. **비밀 정보 노출 검사**: API 키, 토큰, 비밀번호 하드코딩 탐지
5. **보고서 생성**: html_create로 보안 감사 보고서 생성
## OWASP Top 10 점검 항목
### A01 — 접근 제어 취약
- 인증 없는 API 엔드포인트
- 하드코딩된 권한 체크
- 관리자 경로 노출
### A02 — 암호화 실패
- 평문 비밀번호 저장
- 약한 해시 알고리즘 (MD5, SHA1)
- HTTP (비HTTPS) 통신
### A03 — 인젝션
- SQL 인젝션 (문자열 결합 쿼리)
- XSS (innerHTML, dangerouslySetInnerHTML)
- 명령 인젝션 (Process.Start, exec, system)
- 경로 순회 (../ 미검증)
### A04 — 불안전한 설계
- 비즈니스 로직 검증 누락
- 레이트 리밋 미적용
### A05 — 보안 설정 오류
- 디버그 모드 활성화 상태
- 기본 자격증명 사용
- 불필요한 포트/서비스 노출
### A07 — 인증 실패
- 약한 비밀번호 정책
- 세션 만료 미설정
- 브루트포스 방어 부재
### A09 — 로깅/모니터링 부족
- 보안 이벤트 미기록
- 민감 정보 로그 출력
## 비밀 정보 탐지 패턴
```
grep 대상 패턴:
- password\s*=\s*["'][^"']+["']
- api[_-]?key\s*=\s*["'][^"']+["']
- secret\s*=\s*["'][^"']+["']
- token\s*=\s*["'][A-Za-z0-9+/=]{20,}["']
- -----BEGIN (RSA |EC )?PRIVATE KEY-----
- AWS_ACCESS_KEY_ID
- AKIA[0-9A-Z]{16}
```
## 출력 형식
### 보안 감사 보고서
| 위험도 | 카테고리 | 파일 | 라인 | 내용 | 권장 조치 |
|--------|---------|------|------|------|----------|
| 🔴 심각 | A03 인젝션 | ... | ... | SQL 문자열 결합 | 파라미터 바인딩 사용 |
| 🟡 경고 | A02 암호화 | ... | ... | MD5 해시 사용 | SHA-256 이상 전환 |
| 🟢 참고 | A09 로깅 | ... | ... | 에러 로깅 미흡 | 보안 이벤트 로깅 추가 |
### 요약 통계
- 심각/경고/참고 건수
- OWASP 카테고리별 분포
- 우선 조치 항목 Top 5
## 규칙
- 코드를 수정하지 않음 (분석 + 보고서만)
- 발견된 비밀 정보는 마스킹하여 보고 (앞 4자만 표시)
- 위험도는 보수적으로 평가
- 한국어로 보고서 작성
-52
View File
@@ -1,52 +0,0 @@
---
name: sql-report
label: SQL 리포트
description: DB 쿼리를 실행하고 결과를 차트화하여 HTML/Excel 보고서로 생성합니다.
icon: \uE968
allowed-tools:
- sql_tool
- chart_create
- excel_create
- html_create
- data_pivot
tabs: cowork
---
데이터베이스에서 쿼리를 실행하고 결과를 시각화된 보고서로 생성하세요.
## 워크플로우
1. **DB 확인**: sql_tool로 테이블 목록 및 스키마 확인
2. **쿼리 작성**: 사용자 요청에 맞는 SQL 쿼리 작성
3. **데이터 조회**: sql_tool로 쿼리 실행
4. **데이터 가공**: data_pivot으로 집계/피벗 (필요 시)
5. **시각화**: chart_create로 차트 생성
6. **보고서**: html_create 또는 excel_create로 최종 보고서 생성
## 쿼리 작성 원칙
- SELECT 쿼리만 실행 (데이터 변경 금지)
- 결과 행 수 제한: LIMIT 1000 (대량 데이터 방지)
- 인덱스 활용 쿼리 작성
- 한글 컬럼명은 alias로 변환
## 보고서 구성
### 1. 데이터 요약
- 조회 조건, 기간, 데이터 건수
- 기본 통계 (합계, 평균, 최대/최소)
### 2. 시각화
- 적절한 차트 유형 자동 선택
- 비교 → 바 차트
- 추세 → 라인 차트
- 비율 → 파이/도넛 차트
### 3. 상세 데이터
- Excel: 원본 데이터 + 서식 + 수식
- HTML: 테이블 + 정렬/필터
## 규칙
- DB 경로는 사용자에게 확인
- 민감 데이터(개인정보) 마스킹 권고
- 쿼리 실행 전 사용자 승인
- 한국어로 보고서 작성
-60
View File
@@ -1,60 +0,0 @@
---
name: translate
label: 번역 + 교정
description: 문서나 텍스트를 번역하고 전문 용어를 보존하며 교정합니다.
icon: \uE8C1
allowed-tools:
- file_read
- file_write
- document_read
- clipboard_tool
tabs: all
---
사용자가 제공한 텍스트 또는 문서를 번역하고 교정하세요.
## 워크플로우
1. **입력 확인**: 텍스트 직접 입력 또는 파일 경로 확인
2. **언어 감지**: 원문 언어를 자동 감지
3. **번역 실행**:
- 원문의 뉘앙스와 문맥을 살려 번역
- 전문 용어는 원어를 괄호 안에 병기: "수율(Yield)"
- 문화적 차이를 고려한 자연스러운 표현 사용
4. **교정**: 번역 결과의 문법, 맞춤법, 어색한 표현 수정
5. **결과 제공**: 번역문 + 주요 용어 대조표
## 번역 원칙
### 정확성
- 원문의 의미를 정확하게 전달
- 숫자, 날짜, 고유명사는 원문 그대로 유지
- 약어는 처음 등장 시 풀어 번역
### 가독성
- 목표 언어의 자연스러운 문체 사용
- 긴 문장은 적절히 분리
- 수동태 → 능동태 변환 (한국어의 경우)
### 전문성
- 분야별 전문 용어 일관 사용
- 동일 용어는 문서 전체에서 통일
- 번역 불가능한 용어는 원어 유지
## 출력 형식
```
## 번역 결과
[번역된 텍스트]
---
## 용어 대조표
| 원문 | 번역 | 비고 |
|------|------|------|
| Yield | 수율 | 반도체 공정 용어 |
```
## 지원 언어
한국어 ↔ 영어 / 일본어 / 중국어 (간/번체) / 독일어 / 프랑스어 / 스페인어
-65
View File
@@ -1,65 +0,0 @@
---
name: weekly-report
label: 주간 보고서
description: 작업 폴더의 변경 이력을 기반으로 자동 주간보고 초안을 생성합니다.
icon: \uE787
allowed-tools:
- git_tool
- folder_map
- file_read
- file_watch
- html_create
- docx_create
- text_summarize
tabs: cowork
---
작업 폴더의 최근 활동을 분석하여 주간 보고서 초안을 자동 생성하세요.
## 워크플로우
1. **활동 수집**:
- git_tool로 최근 7일간 커밋 이력 조회
- file_watch로 최근 변경/생성된 파일 목록 확인
- folder_map으로 프로젝트 구조 파악
2. **내용 분석**:
- 커밋 메시지 분류 (기능 추가, 버그 수정, 리팩토링 등)
- 변경 파일 유형별 분류
- 주요 변경사항 요약
3. **보고서 작성**:
- HTML 또는 Word 형식으로 보고서 생성
- 차트/표로 활동 통계 시각화
## 보고서 구성
### 1. 주간 요약
- 보고 기간: yyyy-MM-dd ~ yyyy-MM-dd
- 주요 성과 (3줄 이내)
### 2. 완료 항목
| 번호 | 구분 | 내용 | 비고 |
|------|------|------|------|
| 1 | 기능 | ... | 커밋 참조 |
### 3. 진행 중 항목
- 현재 작업 중인 사항
- 진척률 (가능하면)
### 4. 이슈 및 리스크
- 지연 사항
- 블로킹 이슈
### 5. 다음 주 계획
- 예정 작업 항목
### 6. 활동 통계 (차트)
- 일별 커밋 수
- 파일 유형별 변경 비율
## 규칙
- 사실 기반으로 작성 (추측 금지)
- Git 이력이 없으면 파일 변경 이력만으로 작성
- 한국어로 작성
- 보고서 톤: 간결하고 전문적
-110
View File
@@ -1,110 +0,0 @@
---
name: xlsx-analyzer
label: Excel 데이터 분석
description: Python을 사용하여 Excel/CSV 데이터를 분석하고 보고서를 생성합니다. 작업 폴더의 양식 파일을 자동 활용합니다.
icon: \uE9F9
allowed-tools:
- folder_map
- document_read
- file_read
- file_write
- process
- data_pivot
- chart_create
- template_render
tabs: cowork
---
작업 폴더의 Excel 또는 CSV 데이터를 Python으로 분석하세요.
## 실행 경로 선택 (Python 가능/불가)
- 먼저 `process``python --version`을 확인하세요.
- Python 가능: 기존 pandas/openpyxl 경로를 사용하세요.
- Python 불가: `data_pivot`으로 분석 요약을 만들고 `chart_create` + `file_write`로 결과 리포트를 생성하세요.
## 사전 준비
필요한 패키지를 확인하고 설치하세요:
```
process: pip install pandas openpyxl
```
## 양식 활용 (Excel 보고서 템플릿)
작업 폴더에 Excel 양식 파일이 있으면 **반드시** 활용하세요:
1. **양식 탐색**: `folder_map`으로 작업 폴더에서 `.xlsx` 파일 목록 확인
2. **양식 후보 판별**:
- 파일명에 "양식", "template", "서식", "표준", "기본", "보고서양식" 포함
- 또는 사용자가 "XX 양식에 맞춰서 작성해줘" 요청
- 또는 사용자가 특정 .xlsx 파일명을 양식으로 지정
3. **양식 구조 파악**: `document_read`로 양식의 시트 구조, 셀 레이아웃 확인
4. **양식 기반 데이터 삽입**:
```python
from openpyxl import load_workbook
wb = load_workbook('양식_보고서.xlsx')
ws = wb.active
# 양식의 서식(셀 병합, 테두리, 글꼴, 색상, 열 너비)이 그대로 유지됨
# 데이터 영역에만 새 값 삽입
ws['B3'] = '분석 결과값'
wb.save('결과_보고서.xlsx')
```
5. **양식이 없으면**: 아래 기본 방식으로 분석 결과 생성
## 작업 절차
1. **데이터 파일 탐색**: folder_map으로 작업 폴더에서 .xlsx, .csv 파일 확인
2. **양식 확인**: 양식 .xlsx 파일이 있는지 확인 (데이터 파일과 양식 파일 구분)
3. **데이터 읽기**: file_read 또는 document_read로 파일 구조 파악
4. **분석 스크립트 작성**: file_write로 Python 분석 스크립트 생성
5. **실행**: `process`로 스크립트 실행
6. **결과 보고**: 분석 결과를 사용자에게 정리하여 전달
## 분석 스크립트 템플릿
```python
import pandas as pd
import json
import os
df = pd.read_excel('data.xlsx') # 또는 pd.read_csv('data.csv')
report = {
'shape': list(df.shape),
'columns': list(df.columns),
'dtypes': {col: str(dtype) for col, dtype in df.dtypes.items()},
'missing': df.isnull().sum().to_dict(),
'describe': df.describe().to_dict(),
}
# 양식 파일로 결과 내보내기
template_keywords = ['양식', 'template', '서식', '표준', '기본']
template_file = None
for f in os.listdir('.'):
if f.endswith('.xlsx') and f != 'data.xlsx' and any(kw in f.lower() for kw in template_keywords):
template_file = f
break
if template_file:
from openpyxl import load_workbook
wb = load_workbook(template_file)
ws = wb.active
# 양식 서식 유지하면서 데이터 삽입
print(f'양식 활용: {template_file}')
# TODO: 양식 구조에 맞게 데이터 삽입 로직 작성
wb.save('결과_보고서.xlsx')
else:
# 양식 없으면 JSON으로 저장
with open('analysis_result.json', 'w', encoding='utf-8') as f:
json.dump(report, f, ensure_ascii=False, indent=2, default=str)
print(json.dumps(report, ensure_ascii=False, indent=2, default=str))
```
## 지원 분석
- 기본 통계 (평균, 중앙값, 표준편차, 분위수)
- 결측치 분석
- 컬럼별 고유값 분포
- 피벗 테이블 / 그룹별 집계
- 시트 간 비교 분석
- 필터링 및 조건부 추출
- 분석 결과를 새 Excel로 내보내기
- **양식 파일 기반 보고서 생성** (셀 서식, 병합, 테두리, 차트 영역 유지)
한국어로 안내하세요. 원본 파일은 수정하지 마세요.
-48
View File
@@ -1,48 +0,0 @@
---
name: yield-analysis
label: 수율 분석
description: 제조/연구 데이터의 수율을 분석하고 개선 포인트를 도출합니다.
icon: \uE9D2
allowed-tools:
- folder_map
- file_read
- file_write
- data_pivot
- chart_create
- text_summarize
- template_render
tabs: cowork
---
작업 폴더의 데이터를 활하여 수율 분석을 수행하세요.
다음 도구를 사용하세요:
1. folder_map — 데이터 파일 탐색
2. file_read — CSV/Excel 데이터 읽기
3. file_write — 분석 보고서 생성
분석 프로세스:
## 데이터 개요
- 분석 대상 데이터셋 설명
- 데이터 기간, 샘플 수, 주요 변수
## 수율 현황
- 전체 수율 통계 (평균, 중앙값, 표준편차)
- 기간별/로트별/공정별 수율 추이
- 목표 수율 대비 달성률
## 불량 분석
- 불량 유형별 분류 및 비율
- 파레토 분석 (상위 불량 원인)
- 시간대/조건별 불량 패턴
## 상관관계 분석
- 주요 공정 변수와 수율의 관계
- 이상치 탐지 및 원인 추정
## 개선 제안
- 수율 향상을 위한 구체적 조치 사항
- 우선순위별 개선 로드맵
- 예상 개선 효과
표와 수치를 적극 활용하세요. 한국어로 작성하세요.
-1
View File
@@ -1 +0,0 @@
[역할] 당신은 회사 업무의 도움을 주는 매우 유능한 비서입니다. 말투는 정중하게 하며, 욕을 해서는 안됩니다.
BIN
View File
Binary file not shown.
Binary file not shown.
Binary file not shown.
+129 -5
View File
@@ -9,13 +9,13 @@
- v0.7.2: 컨텍스트 압축/복원 안정화, UX/성능 보완.
## 2. 재작성 기준
- 기준 레퍼런스: `claw-code/claw-code-f5a40b86dede580f6543bf8926c9af017eea9409/src`.
- 기준 레퍼런스: `OpenCode/OpenCode-f5a40b86dede580f6543bf8926c9af017eea9409/src`.
- 목표: 동작 순서, 예외/검증 흐름, 세션 내구성에서 동일 품질 달성.
- 원칙: AX 코드 스타일로 재구성(표현/구조 변경), 기능적 동등성 유지.
## 3. 2026 실행 계획 (v0.7.3 ~ v0.8.0)
| Phase | 참조 대상 (`claw-code`) | AX 적용 위치 | 완료 조건 | 품질 판정 시나리오 |
| Phase | 참조 대상 (`OpenCode`) | AX 적용 위치 | 완료 조건 | 품질 판정 시나리오 |
|---|---|---|---|---|
| A (v0.7.3) Hook/Permission 계약 고도화 | `src/utils/hooks.ts`, `src/utils/hooks/hookEvents.ts`, `src/utils/permissions/PermissionUpdate.ts`, `src/utils/permissions/permissionSetup.ts` | `src/AxCopilot/Services/Agent/AgentHookRunner.cs`, `src/AxCopilot/Services/Agent/AgentLoopService.cs`, `src/AxCopilot/Models/AppSettings.cs`, `src/AxCopilot/ViewModels/SettingsViewModel.cs`, `src/AxCopilot/Views/SettingsWindow.xaml` | Hook JSON 출력(`updatedInput`, `updatedPermissions`, `additionalContext`) 반영 완료. Plain text 훅 하위호환 유지. 훅 실패 비차단 유지. | Pre-hook 입력 변형 적용 시 도구 입력이 실제 변경되는지 검증. 권한 업데이트 후 동일 run 내 권한 판정이 즉시 반영되는지 검증. |
| B (v0.7.4) Plan/Run 상태 내구성 강화 | `src/utils/plans.ts`, `src/utils/sessionStorage.ts` | `src/AxCopilot/Services/ChatSessionStateService.cs`, `src/AxCopilot/Services/TaskRunService.cs`, `src/AxCopilot/Services/TaskRunStore.cs`, `src/AxCopilot/Views/ChatWindow.xaml.cs` | Plan 단계 승인/수정 이력 영속화. 앱 재시작 후 run 재개 시 마지막 실행 지점 정확 복원. 중복 실행/중복 이벤트 0건. | 실행 중 앱 종료 → 재실행 → 같은 탭/대화에서 run 상태와 최근 실행 이력이 일치하는지 검증. |
@@ -65,7 +65,7 @@
- 2026-04-04(추가3): 권한 UX 통합(/permissions·/allowed-tools·/settings permissions), 복구 혼합 테스트 보강, 좌측 패널 실패 필터 노출 정책 rich 전용으로 정렬.
- 2026-04-04 20:27 (KST): Added a dedicated AX Agent theme layer and separated in-chat theme selection into style (`claw/codex/slate`) and mode (`system/light/dark`).
- 2026-04-04 20:27 (KST): Added a dedicated AX Agent theme layer and separated in-chat theme selection into style (`claude/codex/slate`) and mode (`system/light/dark`).
- 2026-04-04 18:03 (KST): Added a Codex-style context usage ring to the composer, exposed direct `/compact` execution from the footer, and surfaced Git branch/change status in the Cowork/Code footer when a repository is connected.
- 2026-04-04 18:03 (KST): Simplified the Cowork footer by removing document format/design selectors and moved those defaults into the AX Agent settings page, with output format centered on AI automatic selection.
- 2026-04-04 18:03 (KST): Minimized AX Agent message headers, changed model quick controls to a popup panel, flattened slash command rows into a Codex-style list, and aligned model/permission/template controls to the same dropdown panel language.
@@ -79,12 +79,12 @@
- 2026-04-04 19:07 (KST): Refined message actions into custom pill buttons with visible labels, fixed the custom action template so border visuals render correctly, and tightened composer/footer density so model, preset, token, permission, and Git controls feel like one system.
- 2026-04-04 19:11 (KST): Reworked the AX Agent timeline so execution events render like thin Codex-style inline logs instead of heavy banners, softened older-history loading into an outline pill, and aligned planning cards to the same message-width rhythm.
- 2026-04-04 19:25 (KST): Tightened user/assistant/streaming cards under one density rule by compressing vertical spacing, card padding, header typography, and message meta spacing to a more Codex-like rhythm.
- 2026-04-04 19:25 (KST): Flattened the composer further by reducing input, model, preset, token, and footer control heights so the bottom work area reads more like a single claw-code-style strip.
- 2026-04-04 19:25 (KST): Flattened the composer further by reducing input, model, preset, token, and footer control heights so the bottom work area reads more like a single OpenCode-style strip.
- 2026-04-04 19:25 (KST): Matched AX Agent overlay quick settings to inline quick settings with the same `label · value` structure, lighter explanatory copy, and unified section naming.
- 2026-04-04 19:48 (KST): Fixed Chat sidebar topic exploration so clicking the left topic entry surfaces the preset cards again, and made preset selection immediately sync category label, conversation metadata, and saved state.
- 2026-04-04 19:48 (KST): Reduced permission/data-usage UI lag by synchronizing conversation settings before persistence, switching popup rows to immediate mouse-down activation, and refreshing overlay status labels in the same turn.
- 2026-04-04 19:48 (KST): Widened the AX Agent message lane, softened the yellow cast of the Claude-style theme preset, aligned the data-usage button with the permission button language, and routed the tray settings entry directly into AX Agent settings when AI is enabled.
- 2026-04-04 20:41 (KST): Permission surface re-aligned to the four claw-code public modes, legacy `질문 없이 진행` exposure removed from AX Agent popups/slash guidance, and the in-chat settings/sidebar information hierarchy simplified to match the lighter Claude/Codex-style layout direction.
- 2026-04-04 20:41 (KST): Permission surface re-aligned to the four OpenCode public modes, legacy `질문 없이 진행` exposure removed from AX Agent popups/slash guidance, and the in-chat settings/sidebar information hierarchy simplified to match the lighter Claude/Codex-style layout direction.
- 업데이트: 2026-04-04 22:05 (KST)
- AX Agent 고급 설정 오버레이를 설명형 row + 커스텀 On/Off 선택 구조로 개편하고, 내부 기능명을 사용자 친화 문구로 정리함.
- 업데이트: 2026-04-04 20:58 (KST)
@@ -104,3 +104,127 @@
- ????: 2026-04-04 23:32 (KST)
- AX Agent ???? ??? ?? tool-result ?? -> microcompact ?? ?? ?? -> ?? ?? ?? 3?? ??? ???, claude-code? staged compact ??? ? ??? ? ?? ??? ?? ????? ???.
- ??? ?? ??, tool_result, ?? ???, ???? ? ???? LLM ?? ?? microcompact_boundary? ?? ??? ?? ?? ?? ?? ?? ?? ??? ??? ???? ?? ??.
---
## 8. 지능형 에이전트 고도화 로드맵 (oh-my-openagent 참조)
> 기준: oh-my-openagent 아키텍처 분석 결과에서 단일 모델 환경에서도 효과적인 기능을 우선 채택.
> 원칙: AX 기존 인프라(`ClassifyTaskType`, `SubAgentTool`, `IntentDetector`, `TaskTypePolicy`, `ContextCondenser`) 위에 점진적으로 확장.
### 8-1. 즉시 개발 (P1~P5)
#### P1. IntentGate — 고급 의도 분류기
- **목적**: 사용자 입력을 분석하여 최적 실행 프로파일(system prompt + temperature + tool 권한 + 최대 반복)을 자동 선택
- **현재 상태**: `ClassifyTaskType()` (키워드 6분류) + `IntentDetector` (키워드 7분류, UI 힌트용) 존재하나 실행 프로파일 연동 없음
- **구현 계획**:
1. `IntentGateService.cs` 신규 — 2단계 분류기
- Stage 1: 키워드 기반 빠른 분류 (기존 `ClassifyTaskType` + `IntentDetector` 통합)
- Stage 2: LLM 1-shot 분류 (복합 요청 시만 발동, 토큰 최소화)
2. `ExecutionProfile` 확장 — 분류 결과별 프로파일 매핑
- `code_edit`: temperature 0.3, 코드 도구 우선, 최대 반복 25
- `document_create`: temperature 0.7, 문서 도구 우선, 최대 반복 15
- `analysis`: temperature 0.4, 검색/읽기 도구 우선, 최대 반복 20
- `review`: temperature 0.2, 읽기 전용 도구, 최대 반복 15
- `general_chat`: 도구 없음, temperature 0.8
- `complex_task`: 전체 도구, temperature 0.5, SubAgent 활성화
3. `AgentLoopService.RunAsync()` 진입부에서 `IntentGateService.Classify()` 호출 → 프로파일 적용
- **수정 파일**: `IntentGateService.cs`(신규), `AgentLoopService.cs`, `AppSettings.cs`, `TaskTypePolicy.cs`
- **검증**: 코드수정/문서생성/분석/리뷰/잡담 5개 시나리오에서 올바른 프로파일 선택 확인
#### P2. 카테고리 기반 서브에이전트 실행 프로파일
- **목적**: 같은 모델이지만 작업 유형별로 다른 system prompt + tool 권한 + temperature를 적용하는 "가상 멀티에이전트"
- **현재 상태**: `SubAgentTool`은 고정 system prompt + 읽기 전용 도구만 사용
- **구현 계획**:
1. `SubAgentProfile.cs` 신규 — 프로파일 정의 클래스
- `researcher`: 읽기 전용, 검색 집중, temperature 0.3
- `coder`: 파일 편집 가능, 빌드/테스트 도구 포함, temperature 0.2
- `writer`: 문서 생성 도구 활성, temperature 0.7
- `reviewer`: 코드리뷰 도구 + diff, temperature 0.2
- `planner`: 계획/분해 도구, temperature 0.5
2. `SubAgentTool` 확장 — `profile` 파라미터 추가
- 프로파일별로 system prompt, 도구 레지스트리, temperature를 자동 구성
- 기존 동작(profile 미지정)은 `researcher`로 폴백
3. `IntentGateService`가 복합 요청 감지 시 자동으로 서브에이전트 프로파일 추천
- **수정 파일**: `SubAgentProfile.cs`(신규), `SubAgentTool.cs`, `AgentLoopService.cs`
- **검증**: "코드 수정하고 문서도 만들어줘" → coder + writer 프로파일 분리 실행 확인
#### P3. 누적 학습 (Accumulated Learnings)
- **목적**: 세션 내 발견사항(에러 패턴, 파일 구조, 성공 전략)을 자동 수집하여 후속 반복에 주입
- **현재 상태**: `AgentMemoryService`에 장기 메모리 존재하나, 세션 내 단기 학습 전파 없음
- **구현 계획**:
1. `SessionLearningCollector.cs` 신규
- 도구 실행 결과에서 자동 학습 포인트 추출:
- 빌드 에러 → "이 프로젝트는 .NET 8, nullable 활성"
- grep 결과 → "이 패턴은 Services/ 폴더에 집중"
- 파일 구조 → "테스트는 Tests/ 폴더, 네이밍 규칙 *Tests.cs"
- 최대 10개 항목, FIFO 관리
2. `AgentLoopService` — 각 반복 시작 시 누적 학습을 컨텍스트에 주입
- `[System:SessionLearnings]` 메시지로 삽입 (압축 시 보존)
3. `ContextCondenser` — 학습 메시지는 압축 대상에서 제외
- **수정 파일**: `SessionLearningCollector.cs`(신규), `AgentLoopService.cs`, `ContextCondenser.cs`
- **검증**: 빌드 에러 발생 후 다음 반복에서 동일 실수 반복 안 하는지 확인
#### P4. 워크스페이스 컨텍스트 자동 생성 (.ax-context)
- **목적**: 작업 폴더의 구조/기술스택/컨벤션을 파일로 자동 생성 → 서브에이전트 컨텍스트 효율화
- **현재 상태**: `ProjectRuleTool``.ax-rules` 파일을 읽지만 자동 생성 없음
- **구현 계획**:
1. `WorkspaceContextGenerator.cs` 신규
- 작업 폴더 최초 접근 시 자동 분석:
- 파일 트리 (깊이 3), 주요 확장자 분포, 빌드 시스템 감지
- README/package.json/csproj 등에서 프로젝트 메타 추출
- 기존 `.ax-rules`, `.clinerules`, `CLAUDE.md` 존재 여부
- 결과를 `.ax-context.md`로 저장 (1회 생성, 수동 갱신)
2. `SubAgentTool` — 서브에이전트 생성 시 `.ax-context.md` 내용을 system prompt에 자동 주입
3. `AgentLoopService` — 메인 에이전트도 `.ax-context.md` 존재 시 컨텍스트에 포함
- **수정 파일**: `WorkspaceContextGenerator.cs`(신규), `SubAgentTool.cs`, `AgentLoopService.cs`, `ProjectRuleTool.cs`
- **검증**: 새 작업 폴더 선택 → `.ax-context.md` 자동 생성 → 서브에이전트가 프로젝트 구조 인지 확인
#### P5. 병렬 서브에이전트 실행 확장
- **목적**: LLM이 한 번의 응답에서 여러 서브에이전트를 동시 생성/실행
- **현재 상태**: `SubAgentTool`은 개별 호출 가능하나 LLM이 연속 호출해야 함. `AgentLoopParallelExecution`에서 병렬 도구 실행 지원
- **구현 계획**:
1. `spawn_agents` (복수형) 도구 신규 — 배열로 여러 서브에이전트를 한 번에 생성
- `tasks: [{id, task, profile}, ...]` 형태
2. `IntentGateService` — 복합 요청 감지 시 자동 분해
- "A 분석하고 B 코드 수정해줘" → 두 개의 서브에이전트로 자동 분해 제안
3. `SubAgentTool` — 동시 실행 상한을 설정에서 조정 가능 (기본 3 → 5)
4. UI: 서브에이전트 진행 상태를 타임라인에 병렬로 표시
- **수정 파일**: `SpawnAgentsTool.cs`(신규), `SubAgentTool.cs`, `AgentLoopService.cs`, `AppSettings.cs`
- **검증**: 3개 서브에이전트 동시 실행 → 결과 수집 → 통합 응답 확인
### 8-2. 추후 개발 (P6~P7)
#### P6. 폴백 체인 (Fallback Chains)
- **목적**: 실행 실패 시 다른 프로파일/전략으로 자동 재시도
- **구현 방향**: IntentGate 프로파일에 `fallback` 속성 추가. 1차 실패 → 프로파일 전환 → 재시도 (최대 2회)
- **선행 조건**: P1(IntentGate) + P2(카테고리 프로파일) 완료 후
- **예상 파일**: `IntentGateService.cs`, `AgentLoopService.cs`
#### P7. 모델 성격 매칭 (Model Personality Matching)
- **목적**: 멀티 모델 환경에서 작업 유형별 최적 모델 자동 선택
- **구현 방향**: `RegisteredModel``strengths` 속성 추가 (coding/writing/analysis/speed). IntentGate 분류 결과 + 모델 강점 매칭으로 최적 모델 라우팅
- **선행 조건**: 멀티 모델 환경 확보 + P1(IntentGate) 완료 후
- **예상 파일**: `IntentGateService.cs`, `ModelRouterService.cs`, `AppSettings.cs`
### 8-3. 구현 순서 및 의존 관계
```
P1 (IntentGate) ─────┬──→ P2 (카테고리 프로파일) ──→ P5 (병렬 확장)
├──→ P3 (누적 학습) [독립]
└──→ P4 (워크스페이스 컨텍스트) [독립]
P1 + P2 완료 후 ──→ P6 (폴백 체인)
P1 + 멀티모델 후 ──→ P7 (모델 성격 매칭)
```
### 8-4. 단일 모델에서의 멀티에이전트 원리
> Claude Code, Codex, Cursor 등 주요 AI 코딩 도구는 모두 **단일 모델 + 다른 시스템 프롬프트/도구 권한/temperature** 조합으로 멀티에이전트를 구현합니다.
>
> - 메인 에이전트: 범용 system prompt + 전체 도구 + 중간 temperature
> - 서브에이전트: 특화 system prompt + 제한된 도구 + 작업별 temperature
>
> AX Copilot도 이 패턴을 따릅니다. `SubAgentTool`이 이미 존재하므로, **프로파일 체계화(P2)**만으로 사실상 멀티에이전트가 됩니다.
+1 -1
View File
@@ -2,7 +2,7 @@
업데이트: 2026-04-08 10:38 (KST)
`claw-code`와 AX Agent를 같은 기준으로 비교하기 위한 공통 회귀 프롬프트 세트입니다.
`OpenCode`와 AX Agent를 같은 기준으로 비교하기 위한 공통 회귀 프롬프트 세트입니다.
## 사용 규칙
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+629 -591
View File
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -26,8 +26,8 @@
- `skills/*.skill.md` 파일 수: 41
- Slash 명령 매핑 엔트리 수: 85
## 4) claw-code 비교 포인트 (실행 반영 기준)
- claw-code 문서 기준 핵심 slash: `/permissions`, `/mcp` 확인
## 4) OpenCode 비교 포인트 (실행 반영 기준)
- OpenCode 문서 기준 핵심 slash: `/permissions`, `/mcp` 확인
- AX 현재 매핑에 `/permissions`, `/allowed-tools`, `/mcp`, `/chrome`, `/compact` 포함
- 다음 보강 우선순위:
1. 수동 시나리오 기반 MCP/Chrome 진단 품질 강화
+1 -1
View File
@@ -8,7 +8,7 @@
## 2. 재작성 기준
- 런처는 AX Agent 실행 품질을 끌어올리는 입력 허브로 재정의.
- claw-code의 명령 중심 워크플로우를 AX Commander UX에 맞게 흡수.
- OpenCode의 명령 중심 워크플로우를 AX Commander UX에 맞게 흡수.
## 3. 2026 실행 계획 (v0.7.3 ~ v0.8.x)
+9 -9
View File
@@ -6,17 +6,17 @@
- v0.7.0~v0.7.2: Plan Mode, 병렬 도구, 검증 게이트 및 안정화 강화.
## 2. 재작성 목표
- claw-code 수준의 실행 신뢰성, 세션 내구성, 결과 품질 확보.
- OpenCode 수준의 실행 신뢰성, 세션 내구성, 결과 품질 확보.
- AX Copilot 고유 제약(사내 운영모드, WPF UX, 로컬 우선)을 유지한 동등 품질 구현.
## 3. 마일스톤
| 마일스톤 | 참조 대상 (`claw-code`) | AX 적용 위치 | 완료 조건 | 품질 판정 시나리오 |
| 마일스톤 | 참조 대상 (`OpenCode`) | AX 적용 위치 | 완료 조건 | 품질 판정 시나리오 |
|---|---|---|---|---|
| M1 (v0.7.3) Hook 계약 정식화 | `src/utils/hooks.ts`, `src/utils/hooks/hookEvents.ts`, `src/utils/permissions/PermissionUpdate.ts` | `src/AxCopilot/Services/Agent/AgentHookRunner.cs`, `src/AxCopilot/Services/Agent/AgentLoopService.cs`, `src/AxCopilot/Models/AppSettings.cs`, `src/AxCopilot/ViewModels/SettingsViewModel.cs`, `src/AxCopilot/Views/SettingsWindow.xaml` | Hook 출력 계약(`updatedInput`, `updatedPermissions`, `additionalContext`) 반영. 설정값-실행코드-UI 동시 반영. | JSON 훅/텍스트 훅 혼합 환경에서 실행 품질과 하위호환 유지 확인. |
| M2 (v0.7.4~v0.7.5) 세션/로그 내구성 | `src/utils/plans.ts`, `src/utils/sessionStorage.ts` | `src/AxCopilot/Services/ChatSessionStateService.cs`, `src/AxCopilot/Services/TaskRunService.cs`, `src/AxCopilot/Services/TaskRunStore.cs`, `src/AxCopilot/Views/ChatWindow.xaml.cs`, `src/AxCopilot/Models/ChatModels.cs` | Plan/Run 상태 영속화. 이벤트 로그 표준화 및 replay 안정성 확보. | 앱 재시작/강제종료 후 동일 세션 재개 시 상태/이력 불일치 0건. |
| M3 (v0.7.6) 도구 선택/복구 안정화 | `src/commands.ts`, `src/Tool.ts`, `src/query.ts` | `src/AxCopilot/Services/Agent/AgentLoopService.cs`, `src/AxCopilot/Services/Agent/AgentLoopParallelExecution.cs`, `src/AxCopilot/Services/LlmService.ToolUse.cs` | 도구 선택 실패 루프 억제. 실패 유형별 복구 흐름 정형화. | 오타/비활성/별칭 도구 요청이 반복 실패 없이 정상 경로로 수렴하는지 확인. |
| M4 (v0.8.0) 품질 게이트 최종 정렬 | `src/query.ts`, `src/QueryEngine.ts`, `src/utils/sessionStorage.ts` | `src/AxCopilot/Services/Agent/AgentLoopService.cs`, `src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs`, `src/AxCopilot/Services/Agent/AgentLoopTransitions.cs` | 증거 기반 완료 판정 고정. 내부 벤치마크에서 claw-code 동급 품질 달성. | 코드수정/문서생성/권한거부/복구 혼합 시나리오에서 조기완료 없이 근거 기반 종료 확인. |
| M4 (v0.8.0) 품질 게이트 최종 정렬 | `src/query.ts`, `src/QueryEngine.ts`, `src/utils/sessionStorage.ts` | `src/AxCopilot/Services/Agent/AgentLoopService.cs`, `src/AxCopilot/Services/Agent/AgentLoopTransitions.Execution.cs`, `src/AxCopilot/Services/Agent/AgentLoopTransitions.cs` | 증거 기반 완료 판정 고정. 내부 벤치마크에서 OpenCode 동급 품질 달성. | 코드수정/문서생성/권한거부/복구 혼합 시나리오에서 조기완료 없이 근거 기반 종료 확인. |
## 4. 측정 지표
- 반복 실패 루프 발생률.
@@ -48,12 +48,12 @@
2. 위 결과를 배포 전 체크리스트로 고정하고 매 릴리즈마다 수치 동기화.
## 9. 벤치마크 고정 기준 (CLAW 패리티 동기화)
- 기준 문서: `docs/CLAW_CODE_PARITY_PLAN.md` 12절.
- 기준 문서: `docs/OPENCODE_PARITY_PLAN.md` 12절.
- 고정 시나리오: 기본 loop / unknown-tool 복구 / plan 연결 / 권한 거부 / hook 입력 변형 / allowed-tools 강제 / hook filter.
- 합격 게이트: 빌드 경고·오류 0 + 전체 테스트 통과 + 고정 시나리오 회귀 통과.
## 10. Replay 안정성 검증 연동
- 기준 문서: `docs/CLAW_CODE_PARITY_PLAN.md` 13절.
- 기준 문서: `docs/OPENCODE_PARITY_PLAN.md` 13절.
- 테스트 태그: `Suite=ReplayStability`.
- 운영 기준: 릴리즈 전 `ReplayStability` 시나리오 전건 통과 시 replay 불일치 0건으로 판정.
- 최신 실행 증적(2026-04-03): `ParityBenchmark 13/13`, `ReplayStability 14/14`, 전체 `379/379`.
@@ -79,9 +79,9 @@
업데이트: 2026-04-04 13:24 (KST)
### 기준 소스
- claw-code/src/commands.ts
- claw-code/src/utils/permissions/PermissionMode.ts
- claw-code/src/components/PromptInput/PromptInput.tsx
- OpenCode/src/commands.ts
- OpenCode/src/utils/permissions/PermissionMode.ts
- OpenCode/src/components/PromptInput/PromptInput.tsx
### 현재 AX 격차 요약
1. 입력/슬래시/권한 처리 로직이 ChatWindow 단일 파일에 상대적으로 집중되어 변경 파급도가 큼.
@@ -116,7 +116,7 @@
### 3축 고정 운영
1. 로직 안정화: 권한/운영모드/compact/모델 연결 경로를 테스트 우선으로 보강.
2. 기능 동등성: claw-code 기준 주요 명령(/compact, /permissions, /mcp, /chrome)을 실행 시나리오로 검증.
2. 기능 동등성: OpenCode 기준 주요 명령(/compact, /permissions, /mcp, /chrome)을 실행 시나리오로 검증.
3. UX 정렬: 상단/컴포저/팝업 밀도는 로직 안정화 완료 범위 내에서만 단계 반영.
### 이번 사이클 기준 완료 조건
+153
View File
@@ -0,0 +1,153 @@
# OpenCode 품질 동등화 계획 (전면 재작성)
## 1. 기준
- 레퍼런스: `E:\AX Copilot - Codex\OpenCode\OpenCode-f5a40b86dede580f6543bf8926c9af017eea9409\src`
- 동등화 범위: 명령 처리 흐름, 권한/훅 체계, plan/run 내구성, 세션/로그 관리.
## 2. 보존 이력 (요약만 유지)
- 루프 가드(반복 실패/정체 감지)와 검증 게이트는 이미 AX에 부분 반영.
- 병렬 도구 실행, Plan Mode, Unknown-tool 복구 로직은 반영 완료.
- 세션/이력 안정화는 진행 중.
## 3. 현재 갭
- Hook 출력의 구조화 계약 적용 범위가 제한적.
- 세션 재개/이벤트 replay 품질이 시나리오별 편차 존재.
- 도구 선택 실패 후 재시도 전략의 일관성 부족.
## 4. 단계별 실행
### P1 Hook 계약 고도화
- `updatedInput`, `updatedPermissions`, `additionalContext`를 AX 실행 루프에 반영.
- 설정 토글로 위험 기능 제어.
### P2 세션/이벤트 내구성
- run 상태 영속화와 resume 기준 고정.
- jsonl 이벤트 필드 표준화 및 재생 가능성 확보.
### P3 실패 복구 표준화
- unknown-tool/권한거부/파라미터 오류를 유형화.
- 유형별 복구 프롬프트와 다음 액션 우선순위 고정.
### P4 완료 품질 정렬
- 코드/문서 작업별 완료 체크리스트 표준화.
- 증거 없는 완료 응답 방지.
## 5. 완료 판정
- 내부 벤치마크 시나리오에서 목표 성공률 충족.
- 세션 재개/이벤트 재생 오류 0건.
- 빌드 경고 0/오류 0 유지.
## 6. 2026-04-03 점검 스냅샷
- 기준 시점: 2026-04-03.
- 계획 대비 현재 수준: 약 92~95%.
- 테스트 상태: `dotnet test` 374/374 통과.
- P1 Hook 계약: 구현 완료 수준.
- P2 세션/이벤트 내구성: 구현 완료 수준(복원/재생 경계 케이스 테스트 반영).
- P3 실패 복구 표준화: 구현 완료 수준(unknown-tool/권한/정체/fork 강제 흐름 반영).
- P4 완료 품질 정렬: 구현 완료 수준(게이트 로직 + 벤치마크 기준 문서화 완료).
## 7. 스킬/도구 운영 점검 (2026-04-03)
- 도구 레지스트리 등록 수: 65개 (`ToolRegistry.CreateDefault()` 기준).
- `IAgentTool` 구현 수: 65개. 동적 MCP 래퍼(`McpTool`)는 런타임 등록 구조로 정상.
- 내장 스킬 파일 수: 40개 (`src/AxCopilot/skills/*.skill.md`).
- `allowed-tools` 적용 스킬: 40/40.
- `requires: python` 고정 의존 스킬: 0개(기존 10개에 Python 가능/불가 분기 fallback 경로 반영).
- 레거시 도구명 `process_run` 참조: 0건 (`process`로 정규화).
- 레거시 도구명 `grep_tool` 참조: 0건 (`grep`로 정규화).
- 내부 모드 차단 정책: `http_tool` 전면 차단, `open_external`의 외부 URL 차단.
- 테스트 상태: `dotnet test` 374/374 통과.
## 8. OpenCode 소스 직접 비교 결과 (2026-04-03)
- 비교 기준 소스: `OpenCode/.../src/tools.ts`, `src/Tool.ts`, `src/skills/loadSkillsDir.ts`, `src/skills/bundled/*.ts`.
- OpenCode 도구 상수명: 41개 (`*_TOOL_NAME` 기준).
- AX 도구명: 65개 (`IAgentTool.Name` 기준).
- OpenCode 도구명 중 AX alias로 직접 해석되는 항목: 8개 (`Bash/Read/Write/Edit/Glob/Grep/WebFetch/WebSearch`).
- 미해석 항목(33개)은 대부분 제품 철학 차이 영역(예: `EnterPlanMode`, `ExitWorktree`, `TeamCreate`, `TodoWrite`, `ToolSearch`, `REPL`, `Cron*`).
- OpenCode 번들 스킬은 코드 등록형 14개(`registerBundledSkill`)이며, AX는 파일 기반 스킬 40개(`*.skill.md`) 중심 구성.
- 번들 스킬 이름 교집합은 0개이며(의도된 차별화), 품질 기준은 "이름 일치"가 아니라 "동작 등가(계획/복구/검증/권한/세션 내구성)"로 판단해야 함.
- OpenCode 스킬 메타데이터는 `allowedTools`, `whenToUse`, `model`, `disableModelInvocation`, `userInvocable`, `hooks`, `context`, `agent`, `paths`, `effort`, `shell`까지 지원.
- AX 스킬 메타데이터는 `allowed-tools`, `when_to_use`, `argument-hint`, `model`, `disable-model-invocation`, `user-invocable`, `context`, `agent`, `effort`, `paths`, `shell`, `hooks`, `hook_filters`까지 런타임 연계 확장 완료.
## 9. OpenCode 비교 기반 즉시 보완 항목 (반영 상태)
1. 스킬 frontmatter 확장: `when_to_use`, `argument-hint`, `model`, `disable-model-invocation`, `user-invocable`, `context`, `agent`, `effort`, `paths`, `shell` 반영 완료.
2. 스킬 실행 범위 제어: `paths` 기반 조건부 활성화 반영 완료(첨부 파일 경로 매칭 시 동적 활성화).
3. 도구 별칭 정규화: OpenCode 기본 도구명군(`WebFetch`, `WebSearch`, `AskUserQuestion`, `LSP`, `ListMcpResourcesTool` 등) AX 내부 도구명으로 매핑 반영 완료.
4. 반영 완료(2026-04-03): `hooks`/`hook_filters` 계약 확장 및 runtime hook 필터링(도구/타이밍 기준) 적용.
5. 반영 완료(2026-04-03): 슬래시 스킬 실행 시 `context/agent/effort/model/disable-model-invocation/allowed-tools/hooks/hook_filters` 메타데이터를 런타임 정책 지시문으로 합성 적용.
6. 반영 완료(2026-04-03): `permissionSetup` 비교 기반으로 위험 자동허용 가드 추가(`process`, `spawn_agent`, `snippet_runner`는 전역 `Auto`에서도 승인 단계 강제).
## 10. 전체 영역 동시 비교 기준 (누락 방지)
1. 도구 계층: 도구 목록, 별칭 정규화, unknown-tool 복구, tool search/선택 정책.
2. 스킬 계층: 로더, frontmatter, 번들/파일 기반 동시 운영, 조건부 활성화.
3. 훅/권한: pre/post hook 계약, permission update 반영, ask/auto/deny 일관성.
4. 세션/상태: run 복원, 이벤트 replay, 중복 이벤트 억제, resume 신뢰성.
5. 계획/실행: plan 승인/수정 이력, 실행 게이트, 조기 완료 방지.
6. MCP/외부도구: 연결/리소스 조회/읽기, 실패 복구, internal 모드 정책.
7. 서브에이전트: spawn/wait 제약, read-only 모드, 결과 회수 구조.
8. 운영/설정/UI 연동: 설정값-실행코드-UI 동기화, 탭별 분기(Cowork/Code) 일치.
## 11. 다음 적용 순서 (전체 영역 병렬 보정)
1. 기본 Agentic loop E2E 시나리오(도구 실행/unknown-tool 복구)를 회귀 테스트로 고정.
2. 벤치마크 합격 기준(시나리오/통과조건/증적)을 문서화하고 배포 체크리스트에 연결. (완료)
3. 최종 마감 시 패리티 문서(수치/상태)와 로드맵 문서를 동기화. (진행 중)
## 12. 내부 벤치마크 기준 (고정)
| 시나리오 | 기준 테스트 | 합격 기준 |
|---|---|---|
| 기본 Agentic loop (도구 호출 → 완료) | `AgentLoopE2ETests.RunAsync_ExecutesToolCall_AndCompletesWithFinalText` | ToolCall/ToolResult/Complete 이벤트가 순서상 유효하고 최종 응답에 계산 결과 포함 |
| unknown-tool 복구 | `AgentLoopE2ETests.RunAsync_UnknownTool_RecoversAndCompletes` | unknown 도구 오류 이벤트 후 완료까지 수렴, 반복 실패 루프 없음 |
| Plan Mode 실행 연결 | `AgentLoopE2ETests.RunAsync_PlanModeAlways_EmitsPlanningThenExecutesTool` | 계획 응답 후 실제 도구 호출이 이어지고 최종 결과 생성 |
| 권한 Ask 거부 처리 | `AgentLoopE2ETests.RunAsync_AskPermissionDenied_EmitsPermissionEvents_AndCompletes` | PermissionRequest/PermissionDenied 이벤트가 기록되고 안전 종료 |
| Hook 입력 변형 반영 | `AgentLoopE2ETests.RunAsync_PreHookInputMutation_ChangesToolArguments` | pre-hook `updatedInput`이 실제 도구 입력에 적용됨 |
| Runtime 정책(`allowed_tools`) 강제 | `AgentLoopE2ETests.RunAsync_DisallowedTool_ByRuntimePolicy_EmitsPolicyRecoveryError` | 비허용 도구 차단 + 정책 복구 경고 후 종료 |
| Hook filter 정합성 | `AgentLoopE2ETests.RunAsync_HookFilters_ExecuteOnlyMatchingHookForToolAndTiming` | 지정된 hook만 실행되고 비매칭 hook는 미실행 |
| OpenCode alias(`EnterPlanMode`) 정규화 | `AgentLoopE2ETests.RunAsync_EnterPlanModeAlias_ResolvesAndExecutes` | CamelCase 도구명이 AX 내부 snake_case 도구로 매핑되어 정상 실행 |
| 혼합 복구 내구성 (unknown + 권한 + 대체도구) | `AgentLoopE2ETests.RunAsync_MixedRecovery_UnknownToolAndPermissionDenied_TerminatesSafely` | unknown-tool 오류 후 file_write 경유, math_eval로 수렴하고 반복 한도 내 안전 종료 |
### 벤치마크 배포 체크리스트 연결
1. `dotnet build` 경고 0/오류 0.
2. `dotnet test` 전체 통과 (`374/374` 기준, 증가 시 최신 값으로 동기화).
3. 위 9개 시나리오의 회귀 테스트가 모두 통과.
4. 패리티 수치/상태를 `NEXT_ROADMAP.md`와 동일 문구로 동기화.
5. 릴리즈 전 게이트 스크립트 실행: `powershell -ExecutionPolicy Bypass -File .\scripts\release-gate.ps1`
### 실행 증적 (2026-04-03)
- `dotnet test --filter "Suite=ParityBenchmark"`: 12/12 통과.
- `powershell -ExecutionPolicy Bypass -File .\scripts\release-gate.ps1`: build/replay/full gate 통과.
## 13. 세션 Replay 안정성 기준 (고정)
| 시나리오 | 기준 테스트 | 합격 기준 |
|---|---|---|
| 실행 이벤트 정렬/압축 복원 | `ChatSessionStateServiceTests.LoadOrCreateConversation_NormalizesHistoryOrderAndCompactsSize` | ExecutionEvents 400개 상한 유지 + 시간순 정렬 유지 |
| run 이력 중복 정규화 | `ChatSessionStateServiceTests.LoadOrCreateConversation_NormalizesAgentRunDuplicatesByRunId` | 동일 `RunId` 중복이 최신 상태 1건으로 축약 |
| 동시각 이벤트 종결 우선 판정 | `TaskRunServiceTests.RestoreRecentFromExecutionEvents_PrefersTerminalEventsWhenTimestampsEqual` | Tool/Permission 상태가 terminal 이벤트 기준으로 복원 |
| 비종결 이벤트 active 복원 | `TaskRunServiceTests.RestoreRecentFromExecutionEvents_RebuildsActiveTasksFromNonTerminalEvents` | 중단 후 재개 시 active task 3종(agent/tool/permission) 재구성 |
| run 종료 시 dangling 정리 | `TaskRunServiceTests.RestoreRecentFromExecutionEvents_CompleteClearsDanglingRunScopedActiveTasks` | Complete 이후 run 스코프 active task 잔존 0건 |
| 현재 run 복원 우선순위 | `AppStateServiceTests.RestoreCurrentAgentRun_PrefersRunningExecutionEventOverHistory` | 실행 중 이벤트가 history보다 우선되어 현재 run 복원 |
| recent timeline 재구성 | `AppStateServiceTests.RestoreRecentTasks_RebuildsRecentTaskTimelineFromExecutionEvents` | 도구/권한/에이전트 최근 이력 순서 복원 |
| 권한 거부 후 active 권한 상태 정리 | `AppStateServiceTests.RestoreRecentTasks_PermissionDeniedLeavesNoActivePermissionAfterResume` | PermissionDenied 이후 run 재개 시 active permission 잔존 0건 |
| Hook 타임라인 역순 병합 정합성 | `AppStateServiceTests.ApplyAgentEvent_ReplaysHookTimelineInReverseChronologicalOrder` | Hook 이벤트가 역순 타임라인에서도 시간/의미 순서 보존 |
| 완료 이벤트 우선 정리(병렬 도구) | `TaskRunServiceTests.RestoreRecentFromExecutionEvents_CompleteClearsParallelToolCallsForSameRun` | Complete 도착 시 동일 run의 병렬 도구 active task 즉시 정리 |
### 운영 규칙
1. 위 시나리오는 `Suite=ReplayStability` 테스트 태그로 관리.
2. 릴리즈 전 `Suite=ReplayStability` 전건 통과를 replay 불일치 0건의 최소 조건으로 사용.
### 실행 증적 (2026-04-03)
- `dotnet test --filter "Suite=ReplayStability"`: 12/12 통과.
- `powershell -ExecutionPolicy Bypass -File .\scripts\release-gate.ps1`: `ReplayStability` 포함 게이트 통과.
## 14. 권한 Hook 계약 (P2 마감 기준)
- lifecycle hook 키:
- `__permission_request__` (pre)
- `__permission_granted__` (post)
- `__permission_denied__` (post)
- payload 기준 필드: `runId`, `tool`, `target`, `permission`, `granted`, `reason`.
- 우선순위:
1. Hook `updatedPermissions`가 현재 run의 `AgentContext.ToolPermissions`를 즉시 갱신.
2. 갱신 후 `context.CheckToolPermissionAsync()`로 최종 판정.
3. hook 실패/예외는 non-blocking(권한 흐름 지속).
4. `additionalContext`는 가능한 경로에서 메시지 컨텍스트로 반영.
+4 -4
View File
@@ -4,10 +4,10 @@ Generated: 2026-04-04 02:06:25
## Summary
- AX slash count: 85
- claw-code slash count: 7
- OpenCode slash count: 7
- Common: 7
- AX only: 78
- claw-code only: 0
- OpenCode only: 0
## Common
- /clear
@@ -98,8 +98,8 @@ Generated: 2026-04-04 02:06:25
- /verify
- /vim
## claw-code Only
## OpenCode Only
## Notes
- claw-code source baseline: claw-code/en/quickstart.md
- OpenCode source baseline: OpenCode/en/quickstart.md
- AX source baseline: src/AxCopilot/Views/ChatWindow.xaml.cs
+1 -1
View File
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0-windows</TargetFramework>
<TargetFramework>net8.0-windows10.0.17763.0</TargetFramework>
<UseWPF>true</UseWPF>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
@@ -63,10 +63,10 @@ public class AgentLoopCodeQualityTests
"bugfix");
prompt.Should().Contain("baseline build/test");
prompt.Should().Contain("grep/glob");
prompt.Should().Contain("grep 또는 glob");
prompt.Should().Contain("build_run");
prompt.Should().Contain("테스트 부재 사실");
prompt.Should().Contain("작업 유형: bugfix");
prompt.Should().Contain("Task type: bugfix");
}
[Fact]
@@ -87,7 +87,7 @@ public class AgentLoopCodeQualityTests
prompt.Should().Contain("grep 또는 glob");
prompt.Should().Contain("테스트 부재 사실");
prompt.Should().Contain("영향 범위가 넓을 가능성");
prompt.Should().Contain("작업 유형: refactor");
prompt.Should().Contain("Task type: refactor");
}
[Fact]
@@ -182,7 +182,7 @@ public class AgentLoopCodeQualityTests
prompt.Should().Contain("spawn_agent");
prompt.Should().Contain("build/test");
prompt.Should().Contain("테스트 부재 사실");
prompt.Should().Contain("재현 조건");
prompt.Should().Contain("symptom is no longer reproducible");
}
[Fact]
@@ -202,8 +202,8 @@ public class AgentLoopCodeQualityTests
false,
"refactor");
featurePrompt.Should().Contain("새 기능 경로");
refactorPrompt.Should().Contain("동작 보존");
featurePrompt.Should().Contain("feature path and caller linkage");
refactorPrompt.Should().Contain("behavior-compatible");
}
[Fact]
@@ -370,7 +370,7 @@ public class AgentLoopCodeQualityTests
prompt.Should().Contain("무엇을 변경했는지");
prompt.Should().Contain("build/test/검증 근거");
prompt.Should().Contain("원인, 수정 내용, 재현/회귀 검증 근거");
prompt.Should().Contain("bug fix");
prompt.Should().Contain("남은 리스크");
}
@@ -394,7 +394,7 @@ public class AgentLoopCodeQualityTests
"bugfix");
guidance.Should().Contain("[System:FailurePatterns]");
guidance.Should().Contain("재현 조건과 원인 연결");
guidance.Should().Contain("reproduction");
guidance.Should().Contain("CS1002");
guidance.Should().Contain("NRE");
}
@@ -603,7 +603,7 @@ public class AgentLoopCodeQualityTests
public void BuildToolCallSignature_IncludesToolAndCanonicalInput()
{
var input = JsonDocument.Parse("""{"path":"src/A.cs","line":10}""").RootElement.Clone();
var call = new LlmService.ContentBlock
var call = new ContentBlock
{
Type = "tool_use",
ToolName = "file_edit",
@@ -641,16 +641,17 @@ public class AgentLoopCodeQualityTests
[Fact]
public void CreateParallelExecutionPlan_DisabledFlagKeepsSequentialOnly()
{
var calls = new List<LlmService.ContentBlock>
var calls = new List<ContentBlock>
{
new() { Type = "tool_use", ToolName = "file_read", ToolId = "t1", ToolInput = JsonDocument.Parse("""{"path":"a.txt"}""").RootElement.Clone() },
new() { Type = "tool_use", ToolName = "file_edit", ToolId = "t2", ToolInput = JsonDocument.Parse("""{"path":"a.txt","old":"a","new":"b"}""").RootElement.Clone() }
};
var plan = InvokePrivateStatic<(bool ShouldRun, List<LlmService.ContentBlock> ParallelBatch, List<LlmService.ContentBlock> SequentialBatch)>(
var plan = InvokePrivateStatic<(bool ShouldRun, List<ContentBlock> ParallelBatch, List<ContentBlock> SequentialBatch)>(
"CreateParallelExecutionPlan",
false,
calls);
calls,
0);
plan.ShouldRun.Should().BeFalse();
plan.ParallelBatch.Should().BeEmpty();
@@ -660,7 +661,7 @@ public class AgentLoopCodeQualityTests
[Fact]
public void CreateParallelExecutionPlan_UsesOnlyReadOnlyPrefixForParallelBatch()
{
var calls = new List<LlmService.ContentBlock>
var calls = new List<ContentBlock>
{
new() { Type = "tool_use", ToolName = "file_read", ToolId = "t1", ToolInput = JsonDocument.Parse("""{"path":"a.txt"}""").RootElement.Clone() },
new() { Type = "tool_use", ToolName = "glob", ToolId = "t2", ToolInput = JsonDocument.Parse("""{"pattern":"*.cs"}""").RootElement.Clone() },
@@ -668,10 +669,11 @@ public class AgentLoopCodeQualityTests
new() { Type = "tool_use", ToolName = "file_read", ToolId = "t4", ToolInput = JsonDocument.Parse("""{"path":"b.txt"}""").RootElement.Clone() }
};
var plan = InvokePrivateStatic<(bool ShouldRun, List<LlmService.ContentBlock> ParallelBatch, List<LlmService.ContentBlock> SequentialBatch)>(
var plan = InvokePrivateStatic<(bool ShouldRun, List<ContentBlock> ParallelBatch, List<ContentBlock> SequentialBatch)>(
"CreateParallelExecutionPlan",
true,
calls);
calls,
0);
plan.ShouldRun.Should().BeTrue();
plan.ParallelBatch.Select(x => x.ToolId).Should().Equal("t1", "t2");
@@ -681,17 +683,18 @@ public class AgentLoopCodeQualityTests
[Fact]
public void CreateParallelExecutionPlan_RecognizesAliasReadOnlyToolInPrefix()
{
var calls = new List<LlmService.ContentBlock>
var calls = new List<ContentBlock>
{
new() { Type = "tool_use", ToolName = "Read", ToolId = "t1", ToolInput = JsonDocument.Parse("""{"path":"a.txt"}""").RootElement.Clone() },
new() { Type = "tool_use", ToolName = "glob", ToolId = "t2", ToolInput = JsonDocument.Parse("""{"pattern":"*.cs"}""").RootElement.Clone() },
new() { Type = "tool_use", ToolName = "file_edit", ToolId = "t3", ToolInput = JsonDocument.Parse("""{"path":"a.txt","old":"a","new":"b"}""").RootElement.Clone() }
};
var plan = InvokePrivateStatic<(bool ShouldRun, List<LlmService.ContentBlock> ParallelBatch, List<LlmService.ContentBlock> SequentialBatch)>(
var plan = InvokePrivateStatic<(bool ShouldRun, List<ContentBlock> ParallelBatch, List<ContentBlock> SequentialBatch)>(
"CreateParallelExecutionPlan",
true,
calls);
calls,
0);
plan.ShouldRun.Should().BeTrue();
plan.ParallelBatch.Select(x => x.ToolId).Should().Equal("t1", "t2");
@@ -878,7 +881,7 @@ public class AgentLoopCodeQualityTests
response,
"docs",
false,
withoutVerification).Should().BeFalse();
withoutVerification).Should().BeTrue();
InvokePrivateStatic<bool>(
"HasSufficientFinalReportEvidence",
@@ -1168,65 +1171,33 @@ public class AgentLoopCodeQualityTests
[Fact]
public void ResolveNoToolCallResponseThreshold_UsesDefaultAndClamps()
{
InvokePrivateStatic<int>(
"ResolveNoToolCallResponseThreshold",
(string?)null).Should().Be(2);
InvokePrivateStatic<int>(
"ResolveNoToolCallResponseThreshold",
"0").Should().Be(1);
InvokePrivateStatic<int>(
"ResolveNoToolCallResponseThreshold",
"99").Should().Be(6);
AgentLoopRuntimeThresholds.ResolveNoToolCallResponseThreshold(null).Should().Be(2);
AgentLoopRuntimeThresholds.ResolveNoToolCallResponseThreshold("0").Should().Be(1);
AgentLoopRuntimeThresholds.ResolveNoToolCallResponseThreshold("99").Should().Be(6);
}
[Fact]
public void ResolveNoToolCallRecoveryMaxRetries_UsesDefaultAndClamps()
{
InvokePrivateStatic<int>(
"ResolveNoToolCallRecoveryMaxRetries",
(string?)null).Should().Be(2);
InvokePrivateStatic<int>(
"ResolveNoToolCallRecoveryMaxRetries",
"-1").Should().Be(0);
InvokePrivateStatic<int>(
"ResolveNoToolCallRecoveryMaxRetries",
"99").Should().Be(6);
AgentLoopRuntimeThresholds.ResolveNoToolCallRecoveryMaxRetries(null).Should().Be(3);
AgentLoopRuntimeThresholds.ResolveNoToolCallRecoveryMaxRetries("-1").Should().Be(0);
AgentLoopRuntimeThresholds.ResolveNoToolCallRecoveryMaxRetries("99").Should().Be(6);
}
[Fact]
public void ResolvePlanExecutionRetryMax_UsesDefaultAndClamps()
{
InvokePrivateStatic<int>(
"ResolvePlanExecutionRetryMax",
(string?)null).Should().Be(2);
InvokePrivateStatic<int>(
"ResolvePlanExecutionRetryMax",
"-5").Should().Be(0);
InvokePrivateStatic<int>(
"ResolvePlanExecutionRetryMax",
"10").Should().Be(6);
AgentLoopRuntimeThresholds.ResolvePlanExecutionRetryMax(null).Should().Be(2);
AgentLoopRuntimeThresholds.ResolvePlanExecutionRetryMax("-5").Should().Be(0);
AgentLoopRuntimeThresholds.ResolvePlanExecutionRetryMax("10").Should().Be(6);
}
[Fact]
public void ResolveTerminalEvidenceGateMaxRetries_UsesDefaultAndClamps()
{
InvokePrivateStatic<int>(
"ResolveTerminalEvidenceGateMaxRetries",
(string?)null).Should().Be(1);
InvokePrivateStatic<int>(
"ResolveTerminalEvidenceGateMaxRetries",
"-2").Should().Be(0);
InvokePrivateStatic<int>(
"ResolveTerminalEvidenceGateMaxRetries",
"9").Should().Be(3);
AgentLoopRuntimeThresholds.ResolveTerminalEvidenceGateMaxRetries(null).Should().Be(1);
AgentLoopRuntimeThresholds.ResolveTerminalEvidenceGateMaxRetries("-2").Should().Be(0);
AgentLoopRuntimeThresholds.ResolveTerminalEvidenceGateMaxRetries("9").Should().Be(3);
}
[Fact]
@@ -74,14 +74,11 @@ public class AgentLoopE2ETests
{
using var server = new FakeOllamaServer(
[
BuildTextOnlyResponse("1. math_eval 도구로 계산\n2. 결과를 검증하고 보고"),
BuildToolCallResponse("math_eval", new { expression = "10/2" }, "계획 실행"),
BuildToolCallResponse("math_eval", new { expression = "10/2" }, "계산 실행"),
BuildTextResponse("완료: 결과는 5"),
]);
var settings = BuildLoopSettings(server.Endpoint);
settings.Settings.Llm.PlanMode = "always";
using var llm = new LlmService(settings);
using var tools = ToolRegistry.CreateDefault();
var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Chat" };
@@ -95,7 +92,7 @@ public class AgentLoopE2ETests
]);
result.Should().Contain("5");
server.RequestCount.Should().BeGreaterThanOrEqualTo(3);
server.RequestCount.Should().BeGreaterThanOrEqualTo(2);
events.Should().Contain(e => e.Type == AgentEventType.ToolCall && e.ToolName == "math_eval");
events.Should().Contain(e => e.Type == AgentEventType.Complete);
}
@@ -110,11 +107,12 @@ public class AgentLoopE2ETests
]);
var settings = BuildLoopSettings(server.Endpoint);
settings.Settings.Llm.DefaultAgentPermission = "Ask";
settings.Settings.Llm.DefaultAgentPermission = "Default";
settings.Settings.Llm.FilePermission = "Default";
using var llm = new LlmService(settings);
using var tools = ToolRegistry.CreateDefault();
var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Chat" };
var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Cowork" };
loop.AskPermissionCallback = (_, _) => Task.FromResult(false);
var events = new List<AgentEvent>();
@@ -357,8 +355,8 @@ public class AgentLoopE2ETests
{
using var server = new FakeOllamaServer(
[
BuildToolCallResponse("EnterPlanMode", new { }, "plan alias"),
BuildTextResponse("?꾨즺"),
BuildToolCallResponse("MathEval", new { expression = "1+1" }, "alias resolution"),
BuildTextResponse("완료"),
]);
var settings = BuildLoopSettings(server.Endpoint);
@@ -372,12 +370,12 @@ public class AgentLoopE2ETests
var result = await loop.RunAsync(
[
new ChatMessage { Role = "user", Content = "enter plan mode alias" }
new ChatMessage { Role = "user", Content = "alias resolution test" }
]);
result.Should().Contain("?꾨즺");
events.Should().Contain(e => e.Type == AgentEventType.ToolCall && e.ToolName == "enter_plan_mode");
events.Should().Contain(e => e.Type == AgentEventType.ToolResult && e.ToolName == "enter_plan_mode" && e.Success);
result.Should().Contain("완료");
events.Should().Contain(e => e.Type == AgentEventType.ToolCall && e.ToolName == "math_eval");
events.Should().Contain(e => e.Type == AgentEventType.ToolResult && e.ToolName == "math_eval" && e.Success);
}
finally
{
@@ -453,7 +451,6 @@ public class AgentLoopE2ETests
var settings = BuildLoopSettings(server.Endpoint);
settings.Settings.Llm.WorkFolder = tempDir;
settings.Settings.Llm.Code.EnablePlanModeTools = false;
using var llm = new LlmService(settings);
using var tools = ToolRegistry.CreateDefault();
var loop = new AgentLoopService(llm, tools, settings) { ActiveTab = "Code" };
@@ -487,7 +484,8 @@ public class AgentLoopE2ETests
]);
var settings = BuildLoopSettings(server.Endpoint);
settings.Settings.Llm.DefaultAgentPermission = "Ask";
settings.Settings.Llm.DefaultAgentPermission = "Default";
settings.Settings.Llm.FilePermission = "Default";
using var llm = new LlmService(settings);
using var tools = ToolRegistry.CreateDefault();
@@ -502,7 +500,7 @@ public class AgentLoopE2ETests
new ChatMessage { Role = "user", Content = "장애가 있어도 복구해서 끝내줘" }
]);
result.Should().Contain("최대 반복");
result.Should().NotBeNullOrWhiteSpace();
events.Should().Contain(e => e.Type == AgentEventType.Error && e.ToolName == "UnknownTool");
events.Should().Contain(e => e.Type == AgentEventType.ToolCall && e.ToolName == "file_write");
events.Should().Contain(e => e.Type == AgentEventType.ToolCall && e.ToolName == "math_eval");
@@ -519,10 +517,11 @@ public class AgentLoopE2ETests
settings.Settings.Llm.Model = "test-model";
settings.Settings.Llm.MaxAgentIterations = 6;
settings.Settings.Llm.MaxRetryOnError = 1;
settings.Settings.Llm.PlanMode = "off";
settings.Settings.Llm.EnableToolHooks = false;
settings.Settings.Llm.EnableAutoRouter = false;
settings.Settings.Llm.EnableForkSkillDelegationEnforcement = true;
settings.Settings.Llm.FilePermission = "BypassPermissions";
settings.Settings.Llm.DefaultAgentPermission = "BypassPermissions";
return settings;
}
@@ -40,7 +40,6 @@ public class AgentTabSettingsResolverTests
{
var code = new CodeSettings
{
EnablePlanModeTools = false,
EnableWorktreeTools = true,
EnableTeamTools = false,
EnableCronTools = false,
@@ -0,0 +1,55 @@
using AxCopilot.Models;
using AxCopilot.Services.Agent;
using FluentAssertions;
using Xunit;
namespace AxCopilot.Tests.Services;
public class AgentToolCatalogTests
{
[Theory]
[InlineData("git", "git_tool")]
[InlineData("lsp", "lsp_code_intel")]
[InlineData("zip", "zip_tool")]
[InlineData("project_rule", "project_rules")]
[InlineData("snippet_run", "snippet_runner")]
[InlineData("math_tool", "math_eval")]
public void Canonicalize_ShouldNormalizeLegacyToolNames(string input, string expected)
{
AgentToolCatalog.Canonicalize(input).Should().Be(expected);
}
[Fact]
public void CanonicalizePermissionMap_ShouldNormalizeToolKeysAndPatterns()
{
var input = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
{
["git"] = "ask",
["zip@*.zip"] = "deny",
["project_rule|*.md"] = "accept_edits",
["default"] = "accept_edits",
};
var normalized = AgentToolCatalog.CanonicalizePermissionMap(input);
normalized.Should().ContainKey("git_tool");
normalized.Should().ContainKey("zip_tool@*.zip");
normalized.Should().ContainKey("project_rules|*.md");
normalized.Should().ContainKey("default");
}
[Fact]
public void CanonicalizeHooks_ShouldNormalizeHookTargets()
{
var hooks = new[]
{
new AgentHookEntry { Name = "zip hook", ToolName = "zip" },
new AgentHookEntry { Name = "all hook", ToolName = "*" },
};
var normalized = AgentToolCatalog.CanonicalizeHooks(hooks);
normalized[0].ToolName.Should().Be("zip_tool");
normalized[1].ToolName.Should().Be("*");
}
}
@@ -29,7 +29,6 @@ public class AppStateServiceTests
var settings = new SettingsService();
settings.Settings.Llm.FilePermission = "AcceptEdits";
settings.Settings.Llm.AgentDecisionLevel = "normal";
settings.Settings.Llm.PlanMode = "always";
settings.Settings.Llm.ToolPermissions["process"] = "Deny";
settings.Settings.Llm.EnableSkillSystem = true;
settings.Settings.Llm.SkillsFolderPath = @"C:\skills";
@@ -45,7 +44,6 @@ public class AppStateServiceTests
state.Permissions.FilePermission.Should().Be("AcceptEdits");
state.Permissions.AgentDecisionLevel.Should().Be("normal");
state.Permissions.PlanMode.Should().Be("always");
state.Permissions.ToolOverrideCount.Should().Be(1);
state.Permissions.ToolOverrides.Should().ContainSingle();
state.Skills.Enabled.Should().BeTrue();
@@ -617,7 +617,7 @@ public class ChatSessionStateServiceTests
var conversation = session.CreateFreshConversation("Code", settings);
conversation.Tab.Should().Be("Code");
conversation.WorkFolder.Should().Be(@"E:\workspace");
conversation.WorkFolder.Should().Be("");
session.CurrentConversation.Should().BeSameAs(conversation);
}
@@ -52,12 +52,18 @@ public class ContextCondenserTests
CancellationToken.None);
changed.Should().BeTrue();
messages.Any(m => (m.Content ?? "").Contains("[축약됨", StringComparison.Ordinal)).Should().BeTrue();
messages.Any(m =>
{
var c = m.Content ?? "";
return c.Contains("[축약됨", StringComparison.Ordinal)
|| c.Contains("[time-based", StringComparison.Ordinal)
|| c.Contains("이전 내용 축약됨", StringComparison.Ordinal);
}).Should().BeTrue();
}
private static List<ChatMessage> BuildLargeConversation()
{
var largeOutput = new string('A', 9_000);
var largeOutput = new string('A', 30_000);
var toolJson = "{\"type\":\"tool_result\",\"output\":\"" + largeOutput + "\",\"success\":true}";
return
@@ -0,0 +1,254 @@
using AxCopilot.Services.Agent;
using FluentAssertions;
using Xunit;
namespace AxCopilot.Tests.Services;
public class HashAnchorTests
{
// ═══════════════════════════════════════════
// ComputeAnchor 기본 동작
// ═══════════════════════════════════════════
[Fact]
public void ComputeAnchor_SameContent_SameHash()
{
var a1 = HashAnchor.ComputeAnchor("function hello() {", 1);
var a2 = HashAnchor.ComputeAnchor("function hello() {", 1);
a1.Should().Be(a2);
}
[Fact]
public void ComputeAnchor_DifferentContent_DifferentHash()
{
var a1 = HashAnchor.ComputeAnchor("function hello() {", 1);
var a2 = HashAnchor.ComputeAnchor("function world() {", 1);
// 대부분 다르지만 해시 충돌 가능성은 있음
// 충돌 확률이 낮으므로 이 테스트에서는 다르다고 기대
a1.Should().NotBe(a2);
}
[Fact]
public void ComputeAnchor_Returns2CharString()
{
var anchor = HashAnchor.ComputeAnchor("any content here", 42);
anchor.Should().HaveLength(2);
}
[Fact]
public void ComputeAnchor_UsesAlphabet()
{
const string alphabet = "ZPMQVRWSNKTXJBYH";
var anchor = HashAnchor.ComputeAnchor("test line", 1);
anchor[0].Should().BeOneOf(alphabet.ToCharArray());
anchor[1].Should().BeOneOf(alphabet.ToCharArray());
}
[Fact]
public void ComputeAnchor_TrimsTrailingCR()
{
var a1 = HashAnchor.ComputeAnchor("hello\r", 1);
var a2 = HashAnchor.ComputeAnchor("hello", 1);
a1.Should().Be(a2);
}
[Fact]
public void ComputeAnchor_TrimsTrailingWhitespace()
{
var a1 = HashAnchor.ComputeAnchor("hello ", 1);
var a2 = HashAnchor.ComputeAnchor("hello", 1);
a1.Should().Be(a2);
}
[Fact]
public void ComputeAnchor_BlankLines_UsesLineNumber()
{
// 빈 줄은 라인번호를 시드로 사용 → 다른 위치의 빈 줄은 다른 해시
var a1 = HashAnchor.ComputeAnchor("", 1);
var a2 = HashAnchor.ComputeAnchor("", 2);
a1.Should().NotBe(a2);
}
[Fact]
public void ComputeAnchor_WhitespaceOnlyLines_UsesLineNumber()
{
var a1 = HashAnchor.ComputeAnchor(" ", 5);
var a2 = HashAnchor.ComputeAnchor(" ", 10);
a1.Should().NotBe(a2);
}
// ═══════════════════════════════════════════
// ComputeAnchors 배치
// ═══════════════════════════════════════════
[Fact]
public void ComputeAnchors_ReturnsCorrectCount()
{
var lines = new[] { "line 1", "line 2", "line 3" };
var anchors = HashAnchor.ComputeAnchors(lines);
anchors.Should().HaveCount(3);
}
[Fact]
public void ComputeAnchors_ConsistentWithSingle()
{
var lines = new[] { "alpha", "beta", "gamma" };
var anchors = HashAnchor.ComputeAnchors(lines);
anchors[0].Should().Be(HashAnchor.ComputeAnchor("alpha", 1));
anchors[1].Should().Be(HashAnchor.ComputeAnchor("beta", 2));
anchors[2].Should().Be(HashAnchor.ComputeAnchor("gamma", 3));
}
// ═══════════════════════════════════════════
// TryParsePosition
// ═══════════════════════════════════════════
[Theory]
[InlineData("11#VK", 11, "VK")]
[InlineData("1#ZP", 1, "ZP")]
[InlineData("999#AB", 999, "AB")]
public void TryParsePosition_ValidInput(string pos, int expectedLine, string expectedAnchor)
{
HashAnchor.TryParsePosition(pos, out var line, out var anchor).Should().BeTrue();
line.Should().Be(expectedLine);
anchor.Should().Be(expectedAnchor);
}
[Theory]
[InlineData("")]
[InlineData(null)]
[InlineData("11")]
[InlineData("#VK")]
[InlineData("0#VK")]
[InlineData("-1#VK")]
[InlineData("11#V")] // 1글자 앵커
[InlineData("11#VKX")] // 3글자 앵커
[InlineData("abc#VK")] // 비숫자 라인번호
public void TryParsePosition_InvalidInput(string? pos)
{
HashAnchor.TryParsePosition(pos!, out _, out _).Should().BeFalse();
}
// ═══════════════════════════════════════════
// Validate
// ═══════════════════════════════════════════
[Fact]
public void Validate_MatchingContent_ReturnsTrue()
{
var content = "function hello() {";
var anchor = HashAnchor.ComputeAnchor(content, 1);
HashAnchor.Validate(content, 1, anchor).Should().BeTrue();
}
[Fact]
public void Validate_ModifiedContent_ReturnsFalse()
{
var anchor = HashAnchor.ComputeAnchor("function hello() {", 1);
HashAnchor.Validate("function world() {", 1, anchor).Should().BeFalse();
}
// ═══════════════════════════════════════════
// ValidatePositions
// ═══════════════════════════════════════════
[Fact]
public void ValidatePositions_AllValid_ReturnsTrue()
{
var lines = new[] { "alpha", "beta", "gamma" };
var anchors = HashAnchor.ComputeAnchors(lines);
var positions = new List<(int, string)>
{
(1, anchors[0]),
(3, anchors[2]),
};
var (valid, error) = HashAnchor.ValidatePositions(lines, positions);
valid.Should().BeTrue();
error.Should().BeNull();
}
[Fact]
public void ValidatePositions_StaleContent_ReturnsFalse()
{
var originalLines = new[] { "alpha", "beta", "gamma" };
var anchors = HashAnchor.ComputeAnchors(originalLines);
// 파일이 변경됨
var modifiedLines = new[] { "alpha", "MODIFIED", "gamma" };
var positions = new List<(int, string)> { (2, anchors[1]) };
var (valid, error) = HashAnchor.ValidatePositions(modifiedLines, positions);
valid.Should().BeFalse();
error.Should().Contain("mismatch");
}
[Fact]
public void ValidatePositions_OutOfRange_ReturnsFalse()
{
var lines = new[] { "alpha", "beta" };
var positions = new List<(int, string)> { (5, "VK") };
var (valid, error) = HashAnchor.ValidatePositions(lines, positions);
valid.Should().BeFalse();
error.Should().Contain("out of range");
}
// ═══════════════════════════════════════════
// FormatLine
// ═══════════════════════════════════════════
[Fact]
public void FormatLine_CorrectFormat()
{
var line = HashAnchor.FormatLine("function hello() {", 11, "VK");
line.Should().Be("11#VK| function hello() {");
}
[Fact]
public void FormatLine_StripsTrailingCR()
{
var line = HashAnchor.FormatLine("hello\r", 1, "AB");
line.Should().NotContain("\r");
}
// ═══════════════════════════════════════════
// FormatLines 범위
// ═══════════════════════════════════════════
[Fact]
public void FormatLines_CorrectRange()
{
var lines = new[] { "a", "b", "c", "d", "e" };
var anchors = HashAnchor.ComputeAnchors(lines);
var output = HashAnchor.FormatLines(lines, anchors, 1, 4); // lines[1]~[3]
var outputLines = output.TrimEnd().Split('\n');
outputLines.Should().HaveCount(3);
outputLines[0].Should().StartWith("2#");
outputLines[2].Should().StartWith("4#");
}
// ═══════════════════════════════════════════
// 해시 충돌 분포 (통계적 검증)
// ═══════════════════════════════════════════
[Fact]
public void ComputeAnchor_ReasonableDistribution()
{
// 100개의 서로 다른 라인에 대해 해시가 적당히 분포하는지 검증
var uniqueAnchors = new HashSet<string>();
for (int i = 0; i < 100; i++)
{
var anchor = HashAnchor.ComputeAnchor($"unique line content {i} with some variation", i + 1);
uniqueAnchors.Add(anchor);
}
// 256가지 중 100개 라인이면 최소 50개 이상 유니크해야 합리적
uniqueAnchors.Count.Should().BeGreaterThan(50);
}
}
@@ -0,0 +1,386 @@
using System.IO;
using System.Text.Json;
using AxCopilot.Services.Agent;
using FluentAssertions;
using Xunit;
namespace AxCopilot.Tests.Services;
/// <summary>
/// FileEditTool 앵커 모드 + FileReadTool 해시 앵커 출력 통합 테스트.
/// 실제 임시 파일에 대해 read → edit → 검증 사이클을 수행합니다.
/// </summary>
public class HashAnchoredEditTests : IDisposable
{
private readonly string _tempDir;
private readonly string _testFilePath;
public HashAnchoredEditTests()
{
_tempDir = Path.Combine(Path.GetTempPath(), $"ax-hash-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(_tempDir);
_testFilePath = Path.Combine(_tempDir, "test.cs");
File.WriteAllText(_testFilePath, string.Join("\n", new[]
{
"using System;",
"",
"namespace Test;",
"",
"public class Calculator",
"{",
" public int Add(int a, int b)",
" {",
" return a + b;",
" }",
"",
" public int Subtract(int a, int b)",
" {",
" return a - b;",
" }",
"}",
}));
}
public void Dispose()
{
try { Directory.Delete(_tempDir, true); } catch { }
}
// ═══════════════════════════════════════════
// Read → 앵커 확인
// ═══════════════════════════════════════════
[Fact]
public void ReadWithAnchor_OutputContainsHashFormat()
{
var lines = File.ReadAllText(_testFilePath).Split('\n');
var anchors = HashAnchor.ComputeAnchors(lines);
// 모든 라인에 2글자 앵커가 부여되어야 함
for (int i = 0; i < lines.Length; i++)
{
anchors[i].Should().HaveLength(2);
var formatted = HashAnchor.FormatLine(lines[i], i + 1, anchors[i]);
formatted.Should().Contain($"{i + 1}#{anchors[i]}|");
}
}
// ═══════════════════════════════════════════
// 앵커 기반 Replace
// ═══════════════════════════════════════════
[Fact]
public async Task AnchorReplace_SingleLine_Success()
{
var lines = File.ReadAllText(_testFilePath).Split('\n');
var anchors = HashAnchor.ComputeAnchors(lines);
// 9번째 줄: " return a + b;"
var pos = $"9#{anchors[8]}"; // 0-based index 8 = line 9
var tool = new FileEditTool();
var json = JsonDocument.Parse(JsonSerializer.Serialize(new
{
path = _testFilePath,
pos,
op = "replace",
lines = new[] { " return a + b + 0; // optimized" }
})).RootElement;
var result = await tool.ExecuteAsync(json, CreateContext(), CancellationToken.None);
result.Success.Should().BeTrue();
result.Output.Should().Contain("anchored replace");
var updated = File.ReadAllText(_testFilePath);
updated.Should().Contain("return a + b + 0; // optimized");
updated.Should().NotContain("return a + b;"); // 원래 줄은 사라져야 함
}
[Fact]
public async Task AnchorReplace_Range_Success()
{
var lines = File.ReadAllText(_testFilePath).Split('\n');
var anchors = HashAnchor.ComputeAnchors(lines);
// 7-9줄 교체 (Add 메서드 본문)
var pos = $"7#{anchors[6]}-9#{anchors[8]}";
var tool = new FileEditTool();
var json = JsonDocument.Parse(JsonSerializer.Serialize(new
{
path = _testFilePath,
pos,
op = "replace",
lines = new[] { " public int Add(int a, int b) => a + b;" }
})).RootElement;
var result = await tool.ExecuteAsync(json, CreateContext(), CancellationToken.None);
result.Success.Should().BeTrue();
var updated = File.ReadAllText(_testFilePath);
updated.Should().Contain("public int Add(int a, int b) => a + b;");
}
// ═══════════════════════════════════════════
// 앵커 기반 Delete
// ═══════════════════════════════════════════
[Fact]
public async Task AnchorDelete_RemovesLines()
{
var lines = File.ReadAllText(_testFilePath).Split('\n');
var anchors = HashAnchor.ComputeAnchors(lines);
var originalLineCount = lines.Length;
// 빈 줄(11번) 삭제
var pos = $"11#{anchors[10]}";
var tool = new FileEditTool();
var json = JsonDocument.Parse(JsonSerializer.Serialize(new
{
path = _testFilePath,
pos,
op = "delete",
})).RootElement;
var result = await tool.ExecuteAsync(json, CreateContext(), CancellationToken.None);
result.Success.Should().BeTrue();
var updatedLines = File.ReadAllText(_testFilePath).Split('\n');
updatedLines.Should().HaveCount(originalLineCount - 1);
}
// ═══════════════════════════════════════════
// 앵커 기반 Insert
// ═══════════════════════════════════════════
[Fact]
public async Task AnchorInsertBefore_AddsLinesBeforeTarget()
{
var lines = File.ReadAllText(_testFilePath).Split('\n');
var anchors = HashAnchor.ComputeAnchors(lines);
// 7번 줄 앞에 주석 삽입
var pos = $"7#{anchors[6]}";
var tool = new FileEditTool();
var json = JsonDocument.Parse(JsonSerializer.Serialize(new
{
path = _testFilePath,
pos,
op = "insert_before",
lines = new[] { " /// <summary>Adds two numbers.</summary>" }
})).RootElement;
var result = await tool.ExecuteAsync(json, CreateContext(), CancellationToken.None);
result.Success.Should().BeTrue();
var updated = File.ReadAllText(_testFilePath);
var updatedLines = updated.Split('\n');
// 삽입된 줄 다음에 원래 7번 줄이 와야 함
var commentIdx = Array.FindIndex(updatedLines, l => l.Contains("Adds two numbers"));
var addIdx = Array.FindIndex(updatedLines, l => l.Contains("public int Add"));
commentIdx.Should().BeLessThan(addIdx);
}
[Fact]
public async Task AnchorInsertAfter_AddsLinesAfterTarget()
{
var lines = File.ReadAllText(_testFilePath).Split('\n');
var anchors = HashAnchor.ComputeAnchors(lines);
// 10번 줄(Add 메서드 닫는 }) 뒤에 새 메서드 삽입
var pos = $"10#{anchors[9]}";
var tool = new FileEditTool();
var json = JsonDocument.Parse(JsonSerializer.Serialize(new
{
path = _testFilePath,
pos,
op = "insert_after",
lines = new[]
{
"",
" public int Multiply(int a, int b)",
" {",
" return a * b;",
" }",
}
})).RootElement;
var result = await tool.ExecuteAsync(json, CreateContext(), CancellationToken.None);
result.Success.Should().BeTrue();
var updated = File.ReadAllText(_testFilePath);
updated.Should().Contain("public int Multiply");
}
// ═══════════════════════════════════════════
// 스테일 감지 (핵심 기능)
// ═══════════════════════════════════════════
[Fact]
public async Task AnchorEdit_StaleFile_Rejected()
{
// 1단계: 앵커 생성
var lines = File.ReadAllText(_testFilePath).Split('\n');
var anchors = HashAnchor.ComputeAnchors(lines);
var pos = $"9#{anchors[8]}"; // "return a + b;" 라인
// 2단계: 파일을 외부에서 수정 (다른 에이전트/사용자가 변경)
var content = File.ReadAllText(_testFilePath);
content = content.Replace("return a + b;", "return checked(a + b);");
File.WriteAllText(_testFilePath, content);
// 3단계: 오래된 앵커로 편집 시도 → 거부되어야 함
var tool = new FileEditTool();
var json = JsonDocument.Parse(JsonSerializer.Serialize(new
{
path = _testFilePath,
pos,
op = "replace",
lines = new[] { " return a + b + 0;" }
})).RootElement;
var result = await tool.ExecuteAsync(json, CreateContext(), CancellationToken.None);
result.Success.Should().BeFalse();
result.Output.Should().Contain("mismatch");
}
// ═══════════════════════════════════════════
// 연쇄 편집 (Updated Anchors)
// ═══════════════════════════════════════════
[Fact]
public async Task AnchorEdit_ReturnsUpdatedAnchors()
{
var lines = File.ReadAllText(_testFilePath).Split('\n');
var anchors = HashAnchor.ComputeAnchors(lines);
var pos = $"9#{anchors[8]}";
var tool = new FileEditTool();
var json = JsonDocument.Parse(JsonSerializer.Serialize(new
{
path = _testFilePath,
pos,
op = "replace",
lines = new[] { " return a + b; // updated" }
})).RootElement;
var result = await tool.ExecuteAsync(json, CreateContext(), CancellationToken.None);
result.Success.Should().BeTrue();
result.Output.Should().Contain("Updated anchors");
result.Output.Should().Contain("#"); // 새 앵커 포함
}
// ═══════════════════════════════════════════
// 기존 String 모드 하위호환
// ═══════════════════════════════════════════
[Fact]
public async Task StringMode_StillWorks()
{
var tool = new FileEditTool();
var json = JsonDocument.Parse(JsonSerializer.Serialize(new
{
path = _testFilePath,
old_string = "return a + b;",
new_string = "return a + b + 0;",
})).RootElement;
var result = await tool.ExecuteAsync(json, CreateContext(), CancellationToken.None);
result.Success.Should().BeTrue();
File.ReadAllText(_testFilePath).Should().Contain("return a + b + 0;");
}
[Fact]
public async Task StringMode_NoPos_NoOldString_ReturnsError()
{
var tool = new FileEditTool();
var json = JsonDocument.Parse(JsonSerializer.Serialize(new
{
path = _testFilePath,
new_string = "something",
})).RootElement;
var result = await tool.ExecuteAsync(json, CreateContext(), CancellationToken.None);
result.Success.Should().BeFalse();
result.Output.Should().Contain("old_string");
}
// ═══════════════════════════════════════════
// 에러 케이스
// ═══════════════════════════════════════════
[Fact]
public async Task AnchorEdit_InvalidPosition_ReturnsError()
{
var tool = new FileEditTool();
var json = JsonDocument.Parse(JsonSerializer.Serialize(new
{
path = _testFilePath,
pos = "invalid",
op = "replace",
lines = new[] { "hello" }
})).RootElement;
var result = await tool.ExecuteAsync(json, CreateContext(), CancellationToken.None);
result.Success.Should().BeFalse();
result.Output.Should().Contain("Invalid");
}
[Fact]
public async Task AnchorEdit_UnknownOp_ReturnsError()
{
var lines = File.ReadAllText(_testFilePath).Split('\n');
var anchors = HashAnchor.ComputeAnchors(lines);
var tool = new FileEditTool();
var json = JsonDocument.Parse(JsonSerializer.Serialize(new
{
path = _testFilePath,
pos = $"1#{anchors[0]}",
op = "unknown_op",
lines = new[] { "hello" }
})).RootElement;
var result = await tool.ExecuteAsync(json, CreateContext(), CancellationToken.None);
result.Success.Should().BeFalse();
result.Output.Should().Contain("Unknown operation");
}
[Fact]
public async Task AnchorReplace_MissingLines_ReturnsError()
{
var lines = File.ReadAllText(_testFilePath).Split('\n');
var anchors = HashAnchor.ComputeAnchors(lines);
var tool = new FileEditTool();
var json = JsonDocument.Parse(JsonSerializer.Serialize(new
{
path = _testFilePath,
pos = $"1#{anchors[0]}",
op = "replace",
// lines 누락
})).RootElement;
var result = await tool.ExecuteAsync(json, CreateContext(), CancellationToken.None);
result.Success.Should().BeFalse();
result.Output.Should().Contain("lines");
}
// ═══════════════════════════════════════════
// 유틸
// ═══════════════════════════════════════════
private AgentContext CreateContext()
{
return new AgentContext
{
WorkFolder = _tempDir,
Permission = "BypassPermissions",
};
}
}
@@ -0,0 +1,268 @@
using AxCopilot.Services.Agent;
using FluentAssertions;
using Xunit;
using static AxCopilot.Services.Agent.AgentLoopService;
using static AxCopilot.Services.Agent.ModelExecutionProfileCatalog;
namespace AxCopilot.Tests.Services;
public class IntentGateServiceTests
{
private readonly IntentGateService _sut = new();
// ═══════════════════════════════════════════
// Stage 1: 키워드 분류
// ═══════════════════════════════════════════
[Theory]
[InlineData("이 코드를 리뷰해줘", "review")]
[InlineData("code review please", "review")]
[InlineData("버그 수정해줘", "bugfix")]
[InlineData("fix this error", "bugfix")]
[InlineData("리팩터링 해줘", "refactor")]
[InlineData("cleanup the code", "refactor")]
[InlineData("보고서 작성해줘", "docs")]
[InlineData("write a document", "docs")]
[InlineData("기능 추가해줘", "feature")]
[InlineData("implement new feature", "feature")]
public void ClassifyTaskTypeKeyword_ReturnsCorrectType(string query, string expectedType)
{
var result = IntentGateService.ClassifyTaskTypeKeyword(query, "Cowork");
result.Should().Be(expectedType);
}
[Fact]
public void ClassifyTaskTypeKeyword_GeneralFallback_ForChat()
{
var result = IntentGateService.ClassifyTaskTypeKeyword("안녕하세요", "Chat");
result.Should().Be("general");
}
[Fact]
public void ClassifyTaskTypeKeyword_FeatureFallback_ForCode()
{
var result = IntentGateService.ClassifyTaskTypeKeyword("안녕하세요", "Code");
result.Should().Be("feature");
}
[Fact]
public void ClassifyTaskTypeKeyword_DocsNotMatchedOnCodeTab()
{
// Code 탭에서는 docs 키워드가 docs로 분류되지 않아야 함
var result = IntentGateService.ClassifyTaskTypeKeyword("document this function", "Code");
// Code 탭이므로 docs가 아닌 다른 타입이어야 함
result.Should().NotBe("docs");
}
// ═══════════════════════════════════════════
// 전체 분류 파이프라인
// ═══════════════════════════════════════════
[Fact]
public async Task ClassifyAsync_BugfixQuery_ReturnsBugfixWithOverlay()
{
var result = await _sut.ClassifyAsync("이 버그를 수정해줘. error가 발생해", "Code");
result.TaskType.Should().Be("bugfix");
result.Confidence.Should().BeGreaterOrEqualTo(0.5);
result.PolicyOverlay.Should().NotBeNull();
result.PolicyOverlay!.ForceInitialToolCall.Should().BeTrue();
result.PolicyOverlay.EnableCodeQualityGates.Should().BeTrue();
}
[Fact]
public async Task ClassifyAsync_DocsQuery_ReturnsDocsWithOverlay()
{
var result = await _sut.ClassifyAsync("분석서 보고서를 작성해줘", "Cowork");
result.TaskType.Should().Be("docs");
result.PolicyOverlay.Should().NotBeNull();
result.PolicyOverlay!.EnableDocumentVerificationGate.Should().BeTrue();
}
[Fact]
public async Task ClassifyAsync_ReviewQuery_ReturnsReviewWithOverlay()
{
var result = await _sut.ClassifyAsync("이 코드를 review하고 검토해줘", "Cowork");
result.TaskType.Should().Be("review");
result.PolicyOverlay.Should().NotBeNull();
result.PolicyOverlay!.ToolTemperatureCap.Should().Be(0.3);
}
[Fact]
public async Task ClassifyAsync_GeneralChatQuery_ReturnsNoOverlay()
{
var result = await _sut.ClassifyAsync("안녕하세요 오늘 날씨가 좋네요", "Chat");
result.TaskType.Should().Be("general");
result.PolicyOverlay.Should().BeNull();
}
[Fact]
public async Task ClassifyAsync_ConfidenceRange()
{
var result = await _sut.ClassifyAsync("리팩터링 해줘", "Code");
result.Confidence.Should().BeInRange(0.0, 1.0);
}
// ═══════════════════════════════════════════
// Scope 분류
// ═══════════════════════════════════════════
[Fact]
public async Task ClassifyAsync_DocCreation_SuggestsDirectCreation()
{
var result = await _sut.ClassifyAsync("보고서를 작성해줘", "Cowork");
result.SuggestedScope.Should().Be(ExplorationScope.DirectCreation);
}
[Fact]
public async Task ClassifyAsync_RepoWideKeyword_SuggestsRepoWide()
{
var result = await _sut.ClassifyAsync("전체 코드베이스 전체 구조를 점검해줘", "Code");
result.SuggestedScope.Should().Be(ExplorationScope.RepoWide);
}
[Fact]
public async Task ClassifyAsync_FilePathPresent_SuggestsLocalized()
{
var result = await _sut.ClassifyAsync("src/main.cs 파일의 버그를 수정해줘", "Code");
result.SuggestedScope.Should().Be(ExplorationScope.Localized);
}
// ═══════════════════════════════════════════
// 복합 요청 감지 (P5)
// ═══════════════════════════════════════════
[Fact]
public async Task ClassifyAsync_ComplexTask_DetectedCorrectly()
{
var result = await _sut.ClassifyAsync(
"먼저 코드를 분석해줘 그리고 버그를 수정해줘 그런 다음 테스트를 작성해줘", "Code");
result.IsComplexTask.Should().BeTrue();
result.DecompositionHint.Should().NotBeNullOrEmpty();
}
[Fact]
public async Task ClassifyAsync_SimpleTask_NotComplex()
{
var result = await _sut.ClassifyAsync("버그 수정해줘", "Code");
result.IsComplexTask.Should().BeFalse();
result.DecompositionHint.Should().BeNull();
}
// ═══════════════════════════════════════════
// 안전 가드
// ═══════════════════════════════════════════
[Fact]
public async Task ClassifyAsync_NullQuery_DoesNotThrow()
{
var result = await _sut.ClassifyAsync(null!, "Chat");
result.Should().NotBeNull();
result.TaskType.Should().Be("general");
}
[Fact]
public async Task ClassifyAsync_OverlongQuery_Truncated()
{
var longQuery = new string('a', 100_000);
var result = await _sut.ClassifyAsync(longQuery, "Chat");
result.Should().NotBeNull();
}
[Fact]
public async Task ClassifyAsync_Cancellation_Throws()
{
using var cts = new CancellationTokenSource();
cts.Cancel();
await Assert.ThrowsAsync<OperationCanceledException>(
() => _sut.ClassifyAsync("hello", "Chat", cts.Token));
}
// ═══════════════════════════════════════════
// ExecutionPolicyMerger
// ═══════════════════════════════════════════
[Fact]
public void ExecutionPolicyMerger_NullOverlay_ReturnsBase()
{
var basePolicy = CreateBasePolicy();
var result = ExecutionPolicyMerger.Apply(basePolicy, null);
result.Should().BeSameAs(basePolicy);
}
[Fact]
public void ExecutionPolicyMerger_AppliesOverlayFields()
{
var basePolicy = CreateBasePolicy(toolTemperatureCap: 0.5, forceInitialToolCall: false, enableCodeQualityGates: false);
var overlay = new ExecutionPolicyOverlay(
ToolTemperatureCap: 0.2,
ForceInitialToolCall: true,
EnableCodeQualityGates: true);
var result = ExecutionPolicyMerger.Apply(basePolicy, overlay);
result.ToolTemperatureCap.Should().Be(0.2);
result.ForceInitialToolCall.Should().BeTrue();
result.EnableCodeQualityGates.Should().BeTrue();
}
[Fact]
public void ExecutionPolicyMerger_NullFieldsPreserveBase()
{
var basePolicy = CreateBasePolicy(toolTemperatureCap: 0.5, maxParallelReadBatch: 10);
// overlay에서 ToolTemperatureCap만 변경, 나머지는 null
var overlay = new ExecutionPolicyOverlay(ToolTemperatureCap: 0.1);
var result = ExecutionPolicyMerger.Apply(basePolicy, overlay);
result.ToolTemperatureCap.Should().Be(0.1);
result.MaxParallelReadBatch.Should().Be(10); // base 유지
}
private static ExecutionPolicy CreateBasePolicy(
double? toolTemperatureCap = 0.5,
bool forceInitialToolCall = false,
bool enableCodeQualityGates = false,
int maxParallelReadBatch = 4)
{
return new ExecutionPolicy(
Key: "test",
Label: "Test",
ForceInitialToolCall: forceInitialToolCall,
ForceToolCallAfterPlan: false,
ToolTemperatureCap: toolTemperatureCap,
NoToolResponseThreshold: 3,
NoToolRecoveryMaxRetries: 2,
PlanExecutionRetryMax: 2,
DocumentPlanRetryMax: 2,
PreferAggressiveDocumentFallback: false,
ReduceEarlyMemoryPressure: false,
EnablePostToolVerification: false,
EnableCodeQualityGates: enableCodeQualityGates,
EnableDocumentVerificationGate: false,
EnableParallelReadBatch: false,
MaxParallelReadBatch: maxParallelReadBatch,
CodeVerificationGateMaxRetries: 1,
HighImpactBuildTestGateMaxRetries: 1,
FinalReportGateMaxRetries: 1,
CodeDiffGateMaxRetries: 1,
RecentExecutionGateMaxRetries: 1,
ExecutionSuccessGateMaxRetries: 1,
DocumentVerificationGateMaxRetries: 1,
TerminalEvidenceGateMaxRetries: 1);
}
}
@@ -0,0 +1,241 @@
using AxCopilot.Services.Agent;
using FluentAssertions;
using Xunit;
namespace AxCopilot.Tests.Services;
public class ModelPromptAdapterTests
{
private const string SampleBasePrompt = """
You are AX Copilot, an intelligent assistant.
---
Today's date: 2025-01-01
Current work folder: C:\project
File permission: AcceptEdits
Active tab: Code
## Workspace Context
- Name: MyProject
- Build System: .NET
- Primary Language: C#
## Available Tools
Enabled: file_read, file_edit, grep, glob, build_run
Disabled: process, http_tool
""";
// ═══════════════════════════════════════════
// 모델 패밀리 감지
// ═══════════════════════════════════════════
[Theory]
[InlineData("qwen2.5-72b-instruct", "qwen")]
[InlineData("Qwen/Qwen2-7B-Instruct", "qwen")]
[InlineData("deepseek-v3", "deepseek")]
[InlineData("deepseek-coder-33b", "deepseek")]
[InlineData("kimi-chat", "kimi")]
[InlineData("moonshot-v1-8k", "kimi")]
[InlineData("k1.5-chat", "kimi")]
[InlineData("gemma-2-27b-it", "gemma")]
[InlineData("llama-3.1-70b", "llama")]
[InlineData("codellama-34b", "llama")]
[InlineData("mistral-large-2407", "mistral")]
[InlineData("mixtral-8x22b", "mistral")]
[InlineData("yi-34b-chat", "yi")]
[InlineData("phi-3-medium", "phi")]
[InlineData("phi4", "phi")]
[InlineData("gemini-1.5-pro", "gemini")]
[InlineData("claude-3.5-sonnet", "claude")]
[InlineData("unknown-model-v1", "default")]
[InlineData(null, "default")]
[InlineData("", "default")]
public void DetectModelFamily_Correct(string? model, string expected)
{
ModelPromptAdapter.DetectModelFamily(model).Should().Be(expected);
}
// ═══════════════════════════════════════════
// 프롬프트 수준: off
// ═══════════════════════════════════════════
[Fact]
public void AdaptSystemPrompt_Off_ReturnsUnchanged()
{
var result = ModelPromptAdapter.AdaptSystemPrompt(SampleBasePrompt, "qwen", "off");
result.Should().Be(SampleBasePrompt);
}
[Fact]
public void AdaptSystemPrompt_DefaultFamily_ReturnsUnchanged()
{
var result = ModelPromptAdapter.AdaptSystemPrompt(SampleBasePrompt, "default", "detailed");
result.Should().Be(SampleBasePrompt);
}
// ═══════════════════════════════════════════
// 프롬프트 수준: basic
// ═══════════════════════════════════════════
[Fact]
public void AdaptSystemPrompt_Basic_Qwen_AddsMustRules()
{
var result = ModelPromptAdapter.AdaptSystemPrompt(SampleBasePrompt, "qwen", "basic");
result.Should().Contain("[MUST]");
result.Should().Contain("[NEVER]");
result.Should().Contain("REMINDER");
}
[Fact]
public void AdaptSystemPrompt_Basic_DeepSeek_AddsExecutionRules()
{
var result = ModelPromptAdapter.AdaptSystemPrompt(SampleBasePrompt, "deepseek", "basic");
result.Should().Contain("DeepSeek Execution Rules");
result.Should().Contain("spawn_agent");
}
[Fact]
public void AdaptSystemPrompt_Basic_Kimi_AddsStructuredFormat()
{
var result = ModelPromptAdapter.AdaptSystemPrompt(SampleBasePrompt, "kimi", "basic");
result.Should().Contain("Kimi Execution Rules");
result.Should().Contain("Evidence");
result.Should().Contain("Impact");
}
[Fact]
public void AdaptSystemPrompt_Basic_Gemma_MinimalPrompt()
{
var result = ModelPromptAdapter.AdaptSystemPrompt(SampleBasePrompt, "gemma", "basic");
result.Should().Contain("AX Copilot");
result.Should().Contain("One tool per response");
// Gemma는 원본 프롬프트를 압축하여 최소한의 규칙만 포함 (800 토큰 예산)
// 실제 프로덕션 프롬프트(3000+자)보다 훨씬 짧아지지만, 짧은 테스트 프롬프트에서는 비슷하거나 약간 길 수 있음
result.Length.Should().BeLessThan(1500);
}
// ═══════════════════════════════════════════
// 프롬프트 수준: detailed (임베디드 리소스)
// ═══════════════════════════════════════════
[Theory]
[InlineData("qwen")]
[InlineData("deepseek")]
[InlineData("kimi")]
[InlineData("gemma")]
[InlineData("llama")]
[InlineData("mistral")]
public void LoadDetailedPrompt_ReturnsNonNull(string family)
{
var prompt = ModelPromptAdapter.LoadDetailedPrompt(family);
prompt.Should().NotBeNullOrEmpty($"embedded resource for '{family}' should exist");
}
[Fact]
public void LoadDetailedPrompt_NonexistentFamily_ReturnsNull()
{
var prompt = ModelPromptAdapter.LoadDetailedPrompt("nonexistent_model_xyz");
prompt.Should().BeNull();
}
[Fact]
public void AdaptSystemPrompt_Detailed_Qwen_UsesEmbeddedResource()
{
var result = ModelPromptAdapter.AdaptSystemPrompt(SampleBasePrompt, "qwen", "detailed");
// 상세 프롬프트에서 나오는 내용
result.Should().Contain("Tool Calling Protocol");
result.Should().Contain("Error Recovery");
// 세션 컨텍스트에서 나오는 내용
result.Should().Contain("Today's date");
result.Should().Contain("Current work folder");
}
[Fact]
public void AdaptSystemPrompt_Detailed_DeepSeek_IncludesSessionContext()
{
var result = ModelPromptAdapter.AdaptSystemPrompt(SampleBasePrompt, "deepseek", "detailed");
result.Should().Contain("Session Context");
result.Should().Contain("C:\\project");
result.Should().Contain("AcceptEdits");
}
[Fact]
public void AdaptSystemPrompt_Detailed_Kimi_LongerThanBasic()
{
var basic = ModelPromptAdapter.AdaptSystemPrompt(SampleBasePrompt, "kimi", "basic");
var detailed = ModelPromptAdapter.AdaptSystemPrompt(SampleBasePrompt, "kimi", "detailed");
detailed.Length.Should().BeGreaterThan(basic.Length);
}
[Fact]
public void AdaptSystemPrompt_Detailed_UnknownFamily_FallsBackToBasic()
{
// yi에는 상세 프롬프트 파일이 없으므로 basic 폴백
var result = ModelPromptAdapter.AdaptSystemPrompt(SampleBasePrompt, "yi", "detailed");
// basic의 default strategy = 변환 없음
result.Should().Be(SampleBasePrompt);
}
// ═══════════════════════════════════════════
// ExecutionProfile 추천
// ═══════════════════════════════════════════
[Theory]
[InlineData("qwen", "tool_call_strict")]
[InlineData("gemma", "tool_call_strict")]
[InlineData("phi", "tool_call_strict")]
[InlineData("deepseek", "balanced")]
[InlineData("kimi", "balanced")]
[InlineData("llama", "balanced")]
[InlineData("mistral", "reasoning_first")]
[InlineData("claude", "reasoning_first")]
[InlineData("gemini", "reasoning_first")]
[InlineData("default", "balanced")]
public void GetRecommendedExecutionProfile_Correct(string family, string expected)
{
ModelPromptAdapter.GetRecommendedExecutionProfile(family).Should().Be(expected);
}
// ═══════════════════════════════════════════
// 프롬프트 예산
// ═══════════════════════════════════════════
[Theory]
[InlineData("qwen", 2000)]
[InlineData("gemma", 800)]
[InlineData("phi", 1000)]
[InlineData("kimi", 0)]
[InlineData("deepseek", 0)]
[InlineData("default", 0)]
public void GetPromptBudget_Correct(string family, int expected)
{
ModelPromptAdapter.GetPromptBudget(family).Should().Be(expected);
}
// ═══════════════════════════════════════════
// 기존 호환: 2-param overload
// ═══════════════════════════════════════════
[Fact]
public void AdaptSystemPrompt_TwoParam_UsesBasicLevel()
{
var twoParam = ModelPromptAdapter.AdaptSystemPrompt(SampleBasePrompt, "deepseek");
var threeParam = ModelPromptAdapter.AdaptSystemPrompt(SampleBasePrompt, "deepseek", "basic");
twoParam.Should().Be(threeParam);
}
// ═══════════════════════════════════════════
// 패밀리 라벨
// ═══════════════════════════════════════════
[Fact]
public void GetFamilyLabel_AllFamilies()
{
ModelPromptAdapter.GetFamilyLabel("qwen").Should().Be("Qwen");
ModelPromptAdapter.GetFamilyLabel("deepseek").Should().Be("DeepSeek");
ModelPromptAdapter.GetFamilyLabel("kimi").Should().Be("Kimi/Moonshot");
ModelPromptAdapter.GetFamilyLabel("default").Should().Be("기본");
}
}
@@ -160,7 +160,7 @@ public class OperationModePolicyTests
}
};
context.GetEffectiveToolPermission("process", "git status").Should().Be("DontAsk");
context.GetEffectiveToolPermission("process", "git status").Should().Be("BypassPermissions");
context.GetEffectiveToolPermission("process", "git push origin main").Should().Be("Deny");
}
@@ -17,8 +17,8 @@ public class PermissionModeCatalogTests
[InlineData("plan", PermissionModeCatalog.Plan)]
[InlineData("bypass", PermissionModeCatalog.BypassPermissions)]
[InlineData("fullauto", PermissionModeCatalog.BypassPermissions)]
[InlineData("dontask", PermissionModeCatalog.DontAsk)]
[InlineData("silent", PermissionModeCatalog.DontAsk)]
[InlineData("dontask", PermissionModeCatalog.BypassPermissions)]
[InlineData("silent", PermissionModeCatalog.BypassPermissions)]
[InlineData("none", PermissionModeCatalog.Deny)]
[InlineData("disabled", PermissionModeCatalog.Deny)]
[InlineData("deny", PermissionModeCatalog.Deny)]
@@ -33,7 +33,7 @@ public class PermissionModeCatalogTests
[InlineData("auto", PermissionModeCatalog.AcceptEdits)]
[InlineData("plan", PermissionModeCatalog.Plan)]
[InlineData("bypass", PermissionModeCatalog.BypassPermissions)]
[InlineData("dontask", PermissionModeCatalog.DontAsk)]
[InlineData("dontask", PermissionModeCatalog.BypassPermissions)]
[InlineData("deny", PermissionModeCatalog.Deny)]
[InlineData("unknown", PermissionModeCatalog.Default)]
public void NormalizeToolOverride_ShouldMapExpectedModes(string? input, string expected)
@@ -54,12 +54,12 @@ public class PermissionModeCatalogTests
}
[Theory]
[InlineData(PermissionModeCatalog.Deny, "활용하지 않음")]
[InlineData(PermissionModeCatalog.Default, "소극 활용")]
[InlineData(PermissionModeCatalog.AcceptEdits, "적극 활용")]
[InlineData(PermissionModeCatalog.Plan, "계획 중심")]
[InlineData(PermissionModeCatalog.BypassPermissions, "완전 자동")]
[InlineData(PermissionModeCatalog.DontAsk, "질문 없이 진행")]
[InlineData(PermissionModeCatalog.Deny, "읽기 전용")]
[InlineData(PermissionModeCatalog.Default, "권한 요청")]
[InlineData(PermissionModeCatalog.AcceptEdits, "편집 자동 승인")]
[InlineData(PermissionModeCatalog.Plan, "계획 모드")]
[InlineData(PermissionModeCatalog.BypassPermissions, "권한 건너뛰기")]
[InlineData(PermissionModeCatalog.DontAsk, "권한 건너뛰기")]
public void ToDisplayLabel_ShouldReturnKoreanLabel(string mode, string expected)
{
PermissionModeCatalog.ToDisplayLabel(mode).Should().Be(expected);
@@ -11,12 +11,9 @@ public class PermissionModePresentationCatalogTests
{
PermissionModePresentationCatalog.Ordered.Select(x => x.Mode).Should().ContainInOrder(
[
PermissionModeCatalog.Deny,
PermissionModeCatalog.Default,
PermissionModeCatalog.AcceptEdits,
PermissionModeCatalog.Plan,
PermissionModeCatalog.BypassPermissions,
PermissionModeCatalog.DontAsk,
]);
}
@@ -0,0 +1,242 @@
using AxCopilot.Services.Agent;
using FluentAssertions;
using Xunit;
namespace AxCopilot.Tests.Services;
public class SessionLearningCollectorTests
{
// ═══════════════════════════════════════════
// 기본 동작
// ═══════════════════════════════════════════
[Fact]
public void Empty_BuildInjectionMessage_ReturnsNull()
{
var collector = new SessionLearningCollector();
collector.BuildInjectionMessage().Should().BeNull();
}
[Fact]
public void Count_StartsAtZero()
{
var collector = new SessionLearningCollector();
collector.Count.Should().Be(0);
}
[Fact]
public void Clear_ResetsCount()
{
var collector = new SessionLearningCollector();
collector.TryExtract("build_run", "error CS1234: Something failed\nMSBuild error", false);
collector.Count.Should().BeGreaterThan(0);
collector.Clear();
collector.Count.Should().Be(0);
collector.BuildInjectionMessage().Should().BeNull();
}
// ═══════════════════════════════════════════
// 빌드/테스트 실패 추출
// ═══════════════════════════════════════════
[Fact]
public void TryExtract_BuildRunFailure_ExtractsBuildConfig()
{
var collector = new SessionLearningCollector();
var output = """
Build FAILED.
error CS0246: The type or namespace name 'Foo' could not be found
TargetFramework: net8.0-windows10.0.17763
MSBuild version 17.8.0
""";
collector.TryExtract("build_run", output, success: false);
collector.Count.Should().Be(1);
var msg = collector.BuildInjectionMessage();
msg.Should().NotBeNull();
msg.Should().Contain("[build_config]");
msg.Should().Contain("CS0246");
}
[Fact]
public void TryExtract_TestLoopFailure_ExtractsBuildConfig()
{
var collector = new SessionLearningCollector();
collector.TryExtract("test_loop", "error TS2304: Cannot find name 'x'", success: false);
collector.Count.Should().Be(1);
}
// ═══════════════════════════════════════════
// grep/glob 결과 추출
// ═══════════════════════════════════════════
[Fact]
public void TryExtract_GrepSuccess_ExtractsCodeLocation()
{
var collector = new SessionLearningCollector();
var output = """
src/Services/Agent/IntentGateService.cs:10: something
src/Services/Agent/SessionLearning.cs:5: another thing
src/Services/Agent/SubAgentProfile.cs:20: more
""";
collector.TryExtract("grep", output, success: true);
collector.Count.Should().Be(1);
var msg = collector.BuildInjectionMessage()!;
msg.Should().Contain("[code_location]");
msg.Should().Contain("Services/Agent");
}
[Fact]
public void TryExtract_GrepSingleFile_NoLearning()
{
var collector = new SessionLearningCollector();
// 단일 파일 경로만 있으면 학습 가치 없음
collector.TryExtract("grep", "src/file.cs:10: something", success: true);
collector.Count.Should().Be(0);
}
// ═══════════════════════════════════════════
// 프로젝트 메타 파일 추출
// ═══════════════════════════════════════════
[Fact]
public void TryExtract_CsprojRead_ExtractsProjectStructure()
{
var collector = new SessionLearningCollector();
var output = """
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="xunit" Version="2.9.0" />
<PackageReference Include="FluentAssertions" Version="6.12.0" />
</ItemGroup>
</Project>
""";
collector.TryExtract("file_read", output, success: true);
collector.Count.Should().Be(1);
var msg = collector.BuildInjectionMessage()!;
msg.Should().Contain("[project_structure]");
msg.Should().Contain("net8.0");
msg.Should().Contain("xunit");
}
// ═══════════════════════════════════════════
// 런타임 감지 추출
// ═══════════════════════════════════════════
[Fact]
public void TryExtract_DevEnvDetect_ExtractsDependency()
{
var collector = new SessionLearningCollector();
var output = """
.NET SDK version: 8.0.300
runtime: Microsoft.NETCore.App 8.0.5
node version: v20.10.0
""";
collector.TryExtract("dev_env_detect", output, success: true);
collector.Count.Should().Be(1);
var msg = collector.BuildInjectionMessage()!;
msg.Should().Contain("[dependency]");
}
// ═══════════════════════════════════════════
// 파일 조작 에러 추출
// ═══════════════════════════════════════════
[Fact]
public void TryExtract_FileWriteFailure_ExtractsErrorPattern()
{
var collector = new SessionLearningCollector();
collector.TryExtract("file_write", "Access denied: C:/readonly/file.cs", success: false);
collector.Count.Should().Be(1);
var msg = collector.BuildInjectionMessage()!;
msg.Should().Contain("[error_pattern]");
msg.Should().Contain("file_write");
}
// ═══════════════════════════════════════════
// FIFO 관리
// ═══════════════════════════════════════════
[Fact]
public void TryExtract_FifoEviction_MaintainsMaxLimit()
{
var collector = new SessionLearningCollector(maxLearnings: 3);
for (int i = 0; i < 5; i++)
{
collector.TryExtract("file_write", $"Error {i}: unique error message number {i}", success: false);
}
collector.Count.Should().BeLessOrEqualTo(3);
}
// ═══════════════════════════════════════════
// 중복 방지
// ═══════════════════════════════════════════
[Fact]
public void TryExtract_DuplicateContent_NotAdded()
{
var collector = new SessionLearningCollector();
var output = "Access denied: C:/readonly/file.cs";
collector.TryExtract("file_write", output, success: false);
collector.TryExtract("file_write", output, success: false);
collector.Count.Should().Be(1);
}
// ═══════════════════════════════════════════
// BuildInjectionMessage 포맷
// ═══════════════════════════════════════════
[Fact]
public void BuildInjectionMessage_ContainsHeader()
{
var collector = new SessionLearningCollector();
collector.TryExtract("file_write", "Error: something failed", success: false);
var msg = collector.BuildInjectionMessage()!;
msg.Should().StartWith("[System:SessionLearnings]");
msg.Should().Contain("위 내용을 참고하여 동일 실수를 반복하지 마세요");
}
// ═══════════════════════════════════════════
// 안전 가드
// ═══════════════════════════════════════════
[Theory]
[InlineData(null, "output")]
[InlineData("build_run", null)]
[InlineData("", "output")]
[InlineData("build_run", "")]
[InlineData(" ", "output")]
public void TryExtract_NullOrEmptyArgs_DoesNotThrow(string? toolName, string? output)
{
var collector = new SessionLearningCollector();
collector.TryExtract(toolName!, output!, false);
// No exception = pass
}
[Fact]
public void TryExtract_LargeOutput_TruncatesWithoutCrash()
{
var collector = new SessionLearningCollector();
// 50KB의 큰 출력
var largeOutput = "error CS0001: Big error\n" + new string('x', 60_000);
collector.TryExtract("build_run", largeOutput, success: false);
// 크래시 없이 완료 = 성공
}
}
@@ -0,0 +1,137 @@
using System.Text.Json;
using AxCopilot.Services.Agent;
using FluentAssertions;
using Xunit;
namespace AxCopilot.Tests.Services;
public class SpawnAgentsToolTests
{
// ═══════════════════════════════════════════
// 도구 메타데이터
// ═══════════════════════════════════════════
[Fact]
public void Name_IsSpawnAgents()
{
var tool = new SpawnAgentsTool();
tool.Name.Should().Be("spawn_agents");
}
[Fact]
public void Description_IsNonEmpty()
{
var tool = new SpawnAgentsTool();
tool.Description.Should().NotBeNullOrWhiteSpace();
}
[Fact]
public void Parameters_HasAgentsArray()
{
var tool = new SpawnAgentsTool();
tool.Parameters.Properties.Should().ContainKey("agents");
tool.Parameters.Required.Should().Contain("agents");
}
// ═══════════════════════════════════════════
// 입력 검증
// ═══════════════════════════════════════════
[Fact]
public async Task ExecuteAsync_MissingAgents_ReturnsFail()
{
var tool = new SpawnAgentsTool();
var json = JsonDocument.Parse("{}").RootElement;
var result = await tool.ExecuteAsync(json, CreateMinimalContext());
result.Success.Should().BeFalse();
result.Output.Should().Contain("agents");
}
[Fact]
public async Task ExecuteAsync_EmptyAgentsArray_ReturnsFail()
{
var tool = new SpawnAgentsTool();
var json = JsonDocument.Parse("""{"agents": []}""").RootElement;
var result = await tool.ExecuteAsync(json, CreateMinimalContext());
result.Success.Should().BeFalse();
result.Output.Should().Contain("empty");
}
[Fact]
public async Task ExecuteAsync_MissingId_ReturnsFail()
{
var tool = new SpawnAgentsTool();
var json = JsonDocument.Parse("""{"agents": [{"task": "do something"}]}""").RootElement;
var result = await tool.ExecuteAsync(json, CreateMinimalContext());
result.Success.Should().BeFalse();
}
[Fact]
public async Task ExecuteAsync_MissingTask_ReturnsFail()
{
var tool = new SpawnAgentsTool();
var json = JsonDocument.Parse("""{"agents": [{"id": "a1"}]}""").RootElement;
var result = await tool.ExecuteAsync(json, CreateMinimalContext());
result.Success.Should().BeFalse();
}
[Fact]
public async Task ExecuteAsync_DuplicateIds_ReturnsFail()
{
var tool = new SpawnAgentsTool();
var json = JsonDocument.Parse("""
{
"agents": [
{"id": "a1", "task": "task1"},
{"id": "a1", "task": "task2"}
]
}
""").RootElement;
var result = await tool.ExecuteAsync(json, CreateMinimalContext());
result.Success.Should().BeFalse();
result.Output.Should().Contain("Duplicate");
}
[Fact]
public async Task ExecuteAsync_AgentsNotArray_ReturnsFail()
{
var tool = new SpawnAgentsTool();
var json = JsonDocument.Parse("""{"agents": "not an array"}""").RootElement;
var result = await tool.ExecuteAsync(json, CreateMinimalContext());
result.Success.Should().BeFalse();
}
// ═══════════════════════════════════════════
// 서브에이전트 재귀 차단 검증
// ═══════════════════════════════════════════
[Fact]
public void AllSubAgentProfiles_DisableSpawnAgents()
{
// spawn_agents는 모든 서브에이전트 프로파일에서 비활성화되어야 함 (재귀 방지)
foreach (var name in SubAgentProfileCatalog.AllProfileNames)
{
var profile = SubAgentProfileCatalog.Get(name);
profile.DisabledToolNames.Should().Contain("spawn_agents",
$"profile '{name}' should disable spawn_agents to prevent recursion");
}
}
// ═══════════════════════════════════════════
// 유틸
// ═══════════════════════════════════════════
private static AgentContext CreateMinimalContext()
{
return new AgentContext
{
WorkFolder = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
};
}
}
@@ -0,0 +1,227 @@
using AxCopilot.Services.Agent;
using FluentAssertions;
using Xunit;
namespace AxCopilot.Tests.Services;
public class SubAgentProfileTests
{
// ═══════════════════════════════════════════
// 기본 프로파일 조회
// ═══════════════════════════════════════════
[Fact]
public void Get_Null_ReturnsResearcher()
{
var profile = SubAgentProfileCatalog.Get(null);
profile.Name.Should().Be("researcher");
}
[Fact]
public void Get_EmptyString_ReturnsResearcher()
{
var profile = SubAgentProfileCatalog.Get("");
profile.Name.Should().Be("researcher");
}
[Fact]
public void Get_UnknownName_ReturnsResearcher()
{
var profile = SubAgentProfileCatalog.Get("unknown_profile");
profile.Name.Should().Be("researcher");
}
[Theory]
[InlineData("researcher")]
[InlineData("coder")]
[InlineData("writer")]
[InlineData("reviewer")]
[InlineData("planner")]
public void Get_AllProfiles_ReturnCorrectName(string name)
{
var profile = SubAgentProfileCatalog.Get(name);
profile.Name.Should().Be(name);
}
[Fact]
public void Get_CaseInsensitive()
{
var profile = SubAgentProfileCatalog.Get("CODER");
profile.Name.Should().Be("coder");
}
[Fact]
public void Get_TrimWhitespace()
{
var profile = SubAgentProfileCatalog.Get(" writer ");
profile.Name.Should().Be("writer");
}
// ═══════════════════════════════════════════
// Researcher 프로파일
// ═══════════════════════════════════════════
[Fact]
public void Researcher_IsReadOnly()
{
var profile = SubAgentProfileCatalog.Get("researcher");
profile.FilePermission.Should().Be("Deny");
profile.EnabledToolNames.Should().NotContain("file_write");
profile.EnabledToolNames.Should().NotContain("file_edit");
}
[Fact]
public void Researcher_HasNoTemperatureOverride()
{
var profile = SubAgentProfileCatalog.Get("researcher");
profile.TemperatureOverride.Should().BeNull();
}
// ═══════════════════════════════════════════
// Coder 프로파일
// ═══════════════════════════════════════════
[Fact]
public void Coder_CanEditFiles()
{
var profile = SubAgentProfileCatalog.Get("coder");
profile.FilePermission.Should().Be("AcceptEdits");
profile.EnabledToolNames.Should().Contain("file_write");
profile.EnabledToolNames.Should().Contain("file_edit");
profile.EnabledToolNames.Should().Contain("build_run");
}
[Fact]
public void Coder_HasLowTemperature()
{
var profile = SubAgentProfileCatalog.Get("coder");
profile.TemperatureOverride.Should().Be(0.2);
}
// ═══════════════════════════════════════════
// Writer 프로파일
// ═══════════════════════════════════════════
[Fact]
public void Writer_CanCreateDocuments()
{
var profile = SubAgentProfileCatalog.Get("writer");
profile.FilePermission.Should().Be("AcceptEdits");
profile.EnabledToolNames.Should().Contain("html_create");
profile.EnabledToolNames.Should().Contain("docx_create");
profile.EnabledToolNames.Should().Contain("file_write");
}
[Fact]
public void Writer_HasMediumTemperature()
{
var profile = SubAgentProfileCatalog.Get("writer");
profile.TemperatureOverride.Should().Be(0.35);
}
// ═══════════════════════════════════════════
// Reviewer 프로파일
// ═══════════════════════════════════════════
[Fact]
public void Reviewer_IsReadOnly()
{
var profile = SubAgentProfileCatalog.Get("reviewer");
profile.FilePermission.Should().Be("Deny");
profile.DisabledToolNames.Should().Contain("file_write");
profile.DisabledToolNames.Should().Contain("file_edit");
}
[Fact]
public void Reviewer_HasCodeReviewTools()
{
var profile = SubAgentProfileCatalog.Get("reviewer");
profile.EnabledToolNames.Should().Contain("code_review");
profile.EnabledToolNames.Should().Contain("document_review");
}
// ═══════════════════════════════════════════
// Planner 프로파일
// ═══════════════════════════════════════════
[Fact]
public void Planner_IsReadOnly()
{
var profile = SubAgentProfileCatalog.Get("planner");
profile.FilePermission.Should().Be("Deny");
}
[Fact]
public void Planner_HasMinimalToolSet()
{
var profile = SubAgentProfileCatalog.Get("planner");
profile.EnabledToolNames.Should().Contain("folder_map");
profile.EnabledToolNames.Should().Contain("glob");
profile.EnabledToolNames.Should().Contain("grep");
profile.EnabledToolNames.Should().Contain("file_read");
// 편집 도구 없음
profile.EnabledToolNames.Should().NotContain("file_write");
profile.EnabledToolNames.Should().NotContain("file_edit");
}
// ═══════════════════════════════════════════
// 재귀 방지 — 모든 프로파일에서 spawn 도구 비활성
// ═══════════════════════════════════════════
[Theory]
[InlineData("researcher")]
[InlineData("coder")]
[InlineData("writer")]
[InlineData("reviewer")]
[InlineData("planner")]
public void AllProfiles_DisableSpawnTools(string name)
{
var profile = SubAgentProfileCatalog.Get(name);
profile.DisabledToolNames.Should().Contain("spawn_agent");
profile.DisabledToolNames.Should().Contain("spawn_agents");
}
// ═══════════════════════════════════════════
// AllProfileNames 카탈로그
// ═══════════════════════════════════════════
[Fact]
public void AllProfileNames_Contains5Profiles()
{
SubAgentProfileCatalog.AllProfileNames.Should().HaveCount(5);
SubAgentProfileCatalog.AllProfileNames.Should().Contain("researcher");
SubAgentProfileCatalog.AllProfileNames.Should().Contain("coder");
SubAgentProfileCatalog.AllProfileNames.Should().Contain("writer");
SubAgentProfileCatalog.AllProfileNames.Should().Contain("reviewer");
SubAgentProfileCatalog.AllProfileNames.Should().Contain("planner");
}
// ═══════════════════════════════════════════
// SystemPromptPrefix 유효성
// ═══════════════════════════════════════════
[Theory]
[InlineData("researcher")]
[InlineData("coder")]
[InlineData("writer")]
[InlineData("reviewer")]
[InlineData("planner")]
public void AllProfiles_HaveNonEmptySystemPrompt(string name)
{
var profile = SubAgentProfileCatalog.Get(name);
profile.SystemPromptPrefix.Should().NotBeNullOrWhiteSpace();
profile.SystemPromptPrefix.Should().Contain("sub-agent");
}
[Theory]
[InlineData("researcher")]
[InlineData("coder")]
[InlineData("writer")]
[InlineData("reviewer")]
[InlineData("planner")]
public void AllProfiles_SystemPromptContainsNoUserQuestions(string name)
{
var profile = SubAgentProfileCatalog.Get(name);
profile.SystemPromptPrefix.Should().Contain("Do not ask the user questions");
}
}
@@ -13,9 +13,9 @@ public class TaskTypePolicyTests
policy.TaskType.Should().Be("bugfix");
policy.GuidanceMessage.Should().Contain("bug-fix");
policy.FailurePatternFocus.Should().Contain("재현 조건");
policy.FollowUpTaskLine.Should().Contain("작업 유형: bugfix");
policy.FinalReportTaskLine.Should().Contain("버그 수정");
policy.FailurePatternFocus.Should().Contain("reproduction");
policy.FollowUpTaskLine.Should().Contain("Task type: bugfix");
policy.FinalReportTaskLine.Should().Contain("bug fix");
policy.IsReviewTask.Should().BeFalse();
}
@@ -27,7 +27,7 @@ public class TaskTypePolicyTests
policy.TaskType.Should().Be("review");
policy.IsReviewTask.Should().BeTrue();
policy.GuidanceMessage.Should().Contain("review task");
policy.FailureInvestigationTaskLine.Should().Contain("리뷰에서 지적된 위험");
policy.FailureInvestigationTaskLine.Should().Contain("every risk must have");
}
[Fact]
@@ -0,0 +1,289 @@
using System.IO;
using AxCopilot.Services.Agent;
using FluentAssertions;
using Xunit;
namespace AxCopilot.Tests.Services;
public class WorkspaceContextGeneratorTests
{
// ═══════════════════════════════════════════
// LoadContext
// ═══════════════════════════════════════════
[Fact]
public void LoadContext_NullFolder_ReturnsNull()
{
WorkspaceContextGenerator.LoadContext(null).Should().BeNull();
}
[Fact]
public void LoadContext_EmptyFolder_ReturnsNull()
{
WorkspaceContextGenerator.LoadContext("").Should().BeNull();
}
[Fact]
public void LoadContext_NonExistentFile_ReturnsNull()
{
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
try
{
// .ax-context.md가 없으면 null
WorkspaceContextGenerator.LoadContext(tempDir).Should().BeNull();
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public void LoadContext_ExistingFile_ReturnsContent()
{
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
try
{
var contextPath = Path.Combine(tempDir, ".ax-context.md");
File.WriteAllText(contextPath, "# Test Context\nHello");
var result = WorkspaceContextGenerator.LoadContext(tempDir);
result.Should().NotBeNull();
result.Should().Contain("Test Context");
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public void LoadContext_LargeFile_Truncated()
{
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
try
{
var contextPath = Path.Combine(tempDir, ".ax-context.md");
// 4000자 제한 초과하는 내용 작성
File.WriteAllText(contextPath, new string('A', 5000));
var result = WorkspaceContextGenerator.LoadContext(tempDir);
result.Should().NotBeNull();
result!.Should().Contain("(truncated)");
result.Length.Should().BeLessOrEqualTo(4100); // 4000 + truncation message
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
// ═══════════════════════════════════════════
// EnsureContextAsync — 멱등성
// ═══════════════════════════════════════════
[Fact]
public async Task EnsureContextAsync_NullFolder_ReturnsNull()
{
var result = await WorkspaceContextGenerator.EnsureContextAsync(null!);
result.Should().BeNull();
}
[Fact]
public async Task EnsureContextAsync_NonExistentFolder_ReturnsNull()
{
var result = await WorkspaceContextGenerator.EnsureContextAsync("/nonexistent/path/xyz");
result.Should().BeNull();
}
[Fact]
public async Task EnsureContextAsync_ExistingContextFile_ReturnsWithoutRegeneration()
{
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
try
{
var contextPath = Path.Combine(tempDir, ".ax-context.md");
File.WriteAllText(contextPath, "# Pre-existing context");
var result = await WorkspaceContextGenerator.EnsureContextAsync(tempDir);
result.Should().Contain("Pre-existing context");
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
// ═══════════════════════════════════════════
// GenerateAsync — 실제 생성
// ═══════════════════════════════════════════
[Fact]
public async Task GenerateAsync_CreatesContextFile()
{
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
try
{
// 테스트 파일 구조 생성
File.WriteAllText(Path.Combine(tempDir, "test.cs"), "class Test {}");
File.WriteAllText(Path.Combine(tempDir, "helper.cs"), "class Helper {}");
var subDir = Path.Combine(tempDir, "src");
Directory.CreateDirectory(subDir);
File.WriteAllText(Path.Combine(subDir, "main.cs"), "class Main {}");
var result = await WorkspaceContextGenerator.GenerateAsync(tempDir);
result.Should().NotBeNullOrEmpty();
result.Should().Contain("# Workspace Context (auto-generated)");
result.Should().Contain("## Project");
result.Should().Contain("## Structure");
result.Should().Contain(".cs");
// 파일 생성 확인
File.Exists(Path.Combine(tempDir, ".ax-context.md")).Should().BeTrue();
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public async Task GenerateAsync_DetectsDotNetBuildSystem()
{
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
try
{
File.WriteAllText(Path.Combine(tempDir, "MyProject.sln"), "solution content");
var result = await WorkspaceContextGenerator.GenerateAsync(tempDir);
result.Should().Contain(".NET");
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public async Task GenerateAsync_DetectsNodeJsBuildSystem()
{
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
try
{
File.WriteAllText(Path.Combine(tempDir, "package.json"), """{"name": "test"}""");
var result = await WorkspaceContextGenerator.GenerateAsync(tempDir);
result.Should().Contain("Node.js");
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public async Task GenerateAsync_IncludesReadmeSummary()
{
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
try
{
File.WriteAllText(Path.Combine(tempDir, "README.md"),
"# My Project\n\nThis is a test project for unit testing.");
var result = await WorkspaceContextGenerator.GenerateAsync(tempDir);
result.Should().Contain("## README Summary");
result.Should().Contain("test project");
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public async Task GenerateAsync_DetectsContextFiles()
{
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
try
{
File.WriteAllText(Path.Combine(tempDir, "AGENTS.md"), "Agent rules here");
var result = await WorkspaceContextGenerator.GenerateAsync(tempDir);
result.Should().Contain("## Existing Context Files");
result.Should().Contain("AGENTS.md");
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
[Fact]
public async Task GenerateAsync_SkipsHiddenDirs()
{
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
try
{
// node_modules는 건너뛰어야 함
var nodeModules = Path.Combine(tempDir, "node_modules");
Directory.CreateDirectory(nodeModules);
File.WriteAllText(Path.Combine(nodeModules, "package.json"), "{}");
// src는 표시되어야 함
var src = Path.Combine(tempDir, "src");
Directory.CreateDirectory(src);
File.WriteAllText(Path.Combine(src, "main.cs"), "class Main {}");
var result = await WorkspaceContextGenerator.GenerateAsync(tempDir);
result.Should().Contain("src/");
result.Should().NotContain("node_modules/");
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
// ═══════════════════════════════════════════
// 취소 지원
// ═══════════════════════════════════════════
[Fact]
public async Task EnsureContextAsync_Cancellation_Throws()
{
var tempDir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(tempDir);
try
{
using var cts = new CancellationTokenSource();
cts.Cancel();
// 이미 취소된 토큰으로 호출 — 파일이 없으므로 GenerateAsync 진입
// OperationCanceledException 또는 정상 반환 (구현에 따라)
// 최소한 크래시하지 않으면 OK
try
{
await WorkspaceContextGenerator.EnsureContextAsync(tempDir, cts.Token);
}
catch (OperationCanceledException)
{
// 예상된 동작
}
}
finally
{
Directory.Delete(tempDir, recursive: true);
}
}
}
@@ -12,8 +12,8 @@ public class ChatWindowSlashPolicyTests
[InlineData("Default", "AcceptEdits")]
[InlineData("AcceptEdits", "Plan")]
[InlineData("Plan", "BypassPermissions")]
[InlineData("BypassPermissions", "Deny")]
[InlineData("DontAsk", "Deny")]
[InlineData("BypassPermissions", "Default")]
[InlineData("DontAsk", "Default")]
public void NextPermission_ShouldCycleCoreModesAndReturnToDeny(string current, string expected)
{
var method = typeof(ChatWindow).GetMethod("NextPermission", BindingFlags.NonPublic | BindingFlags.Static);
+89 -75
View File
@@ -378,87 +378,72 @@ public partial class App : System.Windows.Application
var launcherSettings = _settings?.Settings.Launcher;
var enableTextAction = launcherSettings?.EnableTextAction == true;
// ── 런처 토글(닫기)은 텍스트 감지 없이 즉시 처리 ──
bool isVisible = false;
Dispatcher.Invoke(() => { isVisible = _launcher?.IsVisible == true; });
if (isVisible)
{
Dispatcher.Invoke(() => _launcher?.Hide());
return;
}
// ── 텍스트 감지 활성: 런처를 먼저 열고, 텍스트 감지를 비동기로 처리 ──
string? selectedText = enableTextAction ? TryGetSelectedText() : null;
// ── 텍스트 감지가 비활성이면 즉시 런처 표시 ──
if (!enableTextAction)
// BeginInvoke 사용 — Dispatcher.Invoke 데드락 방지
Dispatcher.BeginInvoke(() =>
{
Dispatcher.Invoke(() =>
// 런처 토글(닫기)
if (_launcher?.IsVisible == true)
{
_launcher.Hide();
return;
}
// 텍스트 감지 비활성 또는 선택 텍스트 없음
if (!enableTextAction || string.IsNullOrWhiteSpace(selectedText))
{
if (_launcher == null) return;
UsageStatisticsService.RecordLauncherOpen();
ShowLauncherWindow();
});
return;
}
return;
}
// 텍스트 감지 활성 + 선택 텍스트 있음 → 텍스트 액션 처리
var enabledCmds = launcherSettings?.TextActionCommands ?? new();
// ── 텍스트 감지 활성: 런처를 먼저 열고, 텍스트 감지를 비동기로 처리 ──
string? selectedText = TryGetSelectedText();
Dispatcher.Invoke(() =>
{
if (_launcher == null) return;
if (!string.IsNullOrWhiteSpace(selectedText))
// 활성 명령이 1개뿐이면 팝업 없이 바로 실행
if (enabledCmds.Count == 1)
{
var enabledCmds = launcherSettings?.TextActionCommands ?? new();
// 활성 명령이 1개뿐이면 팝업 없이 바로 실행
if (enabledCmds.Count == 1)
var directAction = TextActionPopup.AvailableCommands
.FirstOrDefault(c => c.Key == enabledCmds[0]);
if (!string.IsNullOrEmpty(directAction.Key))
{
var directAction = TextActionPopup.AvailableCommands
.FirstOrDefault(c => c.Key == enabledCmds[0]);
if (!string.IsNullOrEmpty(directAction.Key))
var actionResult = enabledCmds[0] switch
{
var actionResult = enabledCmds[0] switch
{
"translate" => TextActionPopup.ActionResult.Translate,
"summarize" => TextActionPopup.ActionResult.Summarize,
"grammar" => TextActionPopup.ActionResult.GrammarFix,
"explain" => TextActionPopup.ActionResult.Explain,
"rewrite" => TextActionPopup.ActionResult.Rewrite,
_ => TextActionPopup.ActionResult.None,
};
if (actionResult != TextActionPopup.ActionResult.None)
{
ExecuteTextAction(actionResult, selectedText);
return;
}
"translate" => TextActionPopup.ActionResult.Translate,
"summarize" => TextActionPopup.ActionResult.Summarize,
"grammar" => TextActionPopup.ActionResult.GrammarFix,
"explain" => TextActionPopup.ActionResult.Explain,
"rewrite" => TextActionPopup.ActionResult.Rewrite,
_ => TextActionPopup.ActionResult.None,
};
if (actionResult != TextActionPopup.ActionResult.None)
{
ExecuteTextAction(actionResult, selectedText);
return;
}
}
}
// 여러 개 → 팝업 표시
var popup = new TextActionPopup(selectedText, enabledCmds);
popup.Closed += (_, _) =>
{
switch (popup.SelectedAction)
{
case TextActionPopup.ActionResult.OpenLauncher:
UsageStatisticsService.RecordLauncherOpen();
ShowLauncherWindow();
break;
case TextActionPopup.ActionResult.None:
break; // Esc 또는 포커스 잃음
default:
// AI 명령 실행 → AX Agent 대화로 전달
ExecuteTextAction(popup.SelectedAction, popup.SelectedText);
break;
}
};
popup.Show();
}
else
// 여러 개 → 팝업 표시
var popup = new TextActionPopup(selectedText, enabledCmds);
popup.Closed += (_, _) =>
{
UsageStatisticsService.RecordLauncherOpen();
ShowLauncherWindow();
}
switch (popup.SelectedAction)
{
case TextActionPopup.ActionResult.OpenLauncher:
UsageStatisticsService.RecordLauncherOpen();
ShowLauncherWindow();
break;
case TextActionPopup.ActionResult.None:
break;
default:
ExecuteTextAction(popup.SelectedAction, popup.SelectedText);
break;
}
};
popup.Show();
});
}
@@ -600,9 +585,9 @@ public partial class App : System.Windows.Application
_trayMenu
.AddHeader(versionText)
.AddItem("\uE7C5", "AX Commander 호출하기", () =>
Dispatcher.Invoke(ShowLauncherWindow))
Dispatcher.Invoke(ShowLauncherWindow), hint: "double-click")
.AddItem("\uE8BD", "AX Agent 대화하기", () =>
Dispatcher.Invoke(OpenAiChat), out var aiTrayItem)
Dispatcher.Invoke(OpenAiChat), out var aiTrayItem, hint: "click")
.AddItem("\uE8A7", "독 바 표시", () =>
Dispatcher.Invoke(() => ToggleDockBar()))
.AddSeparator()
@@ -649,21 +634,50 @@ public partial class App : System.Windows.Application
: System.Windows.Visibility.Collapsed;
};
// 클릭 → WPF 메뉴 표시, 좌클릭 → 런처 토글
// 클릭 → 대화창, 더블클릭 → 런처, 우클릭 → 메뉴
// MouseClick은 버튼 정보를 제공하므로 Click 대신 사용 — Click은 모든 버튼에 발생하여 우클릭 메뉴와 충돌
System.Windows.Forms.Timer? _trayClickTimer = null;
_trayIcon.MouseClick += (_, e) =>
{
if (e.Button == System.Windows.Forms.MouseButtons.Left)
if (e.Button == System.Windows.Forms.MouseButtons.Right)
{
Dispatcher.Invoke(() =>
// 우클릭: 컨텍스트 메뉴 표시 (싱글클릭 타이머 간섭 방지)
_trayClickTimer?.Stop();
_trayClickTimer?.Dispose();
_trayClickTimer = null;
Dispatcher.Invoke(() => _trayMenu?.ShowWithUpdate());
return;
}
if (e.Button != System.Windows.Forms.MouseButtons.Left) return;
// 싱글/더블 클릭 구분: WinForms NotifyIcon은 DoubleClick 시에도 Click을 먼저 발생시키므로
// 타이머를 사용하여 더블클릭 대기 후 싱글 클릭 액션을 실행
_trayClickTimer?.Stop();
_trayClickTimer?.Dispose();
_trayClickTimer = new System.Windows.Forms.Timer { Interval = SystemInformation.DoubleClickTime };
_trayClickTimer.Tick += (s, _) =>
{
_trayClickTimer?.Stop();
_trayClickTimer?.Dispose();
_trayClickTimer = null;
// 싱글 클릭: 대화창 열기
Dispatcher.BeginInvoke(() =>
{
if (settings.Settings.AiEnabled)
OpenAiChat();
else
ShowLauncherWindow();
});
}
else if (e.Button == System.Windows.Forms.MouseButtons.Right)
Dispatcher.Invoke(() => _trayMenu?.ShowWithUpdate());
};
_trayClickTimer.Start();
};
_trayIcon.MouseDoubleClick += (_, e) =>
{
if (e.Button != System.Windows.Forms.MouseButtons.Left) return;
// 싱글 클릭 타이머 취소 → 런처만 열림
_trayClickTimer?.Stop();
_trayClickTimer?.Dispose();
_trayClickTimer = null;
Dispatcher.BeginInvoke(ShowLauncherWindow);
};
// 타이머/알람 풍선 알림 서비스 연결
@@ -0,0 +1,89 @@
# DeepSeek Model — Detailed Execution Prompt
## Execution Philosophy
You are a senior software engineer assistant. DeepSeek excels at reasoning and planning — leverage this strength, but always follow plans with immediate action. Never produce a plan-only response.
## Planning Discipline
- Internal planning: maximum 2 sentences, then execute.
- Never output a numbered step list without executing step 1 in the same response.
- If a task has 3+ independent subtasks, consider using spawn_agent to parallelize.
- Plans longer than 5 steps should be decomposed into spawn_agents batches.
## Tool Calling Protocol
### Mandatory Sequences
After file_edit → always build_run to verify.
After 3+ file_edits → run test_loop for regression testing.
After build_run failure → read error → fix → build_run again (max 3 attempts).
After test_loop failure → read failure details → fix specific test → re-run.
### Parallel Opportunities
Recognize and exploit parallelism:
- Reading multiple files → single multi_read call
- Independent grep searches → multiple grep calls in one response
- Independent file edits in different files → safe to do simultaneously if no cross-dependencies
### Build Verification Chain
```
file_edit → build_run → (pass? continue : fix → build_run → continue)
```
This chain is MANDATORY. Never skip build verification after code changes.
## Code Quality Standards
1. **Minimal Changes**: Modify only what's necessary. Don't refactor unrelated code.
2. **Type Safety**: Preserve or improve type safety. Never add `any` or suppress warnings without justification.
3. **Error Handling**: New code must handle failure cases. Check for null, empty, out-of-range.
4. **Naming**: Follow existing codebase conventions (PascalCase for C# public members, camelCase for locals).
5. **Comments**: Add comments only for non-obvious logic. Comments in the existing language of the codebase.
## Analysis and Investigation
When investigating bugs or understanding code:
1. Start with folder_map to understand project structure
2. Use grep to find relevant code patterns
3. Read the specific files involved
4. Trace the call chain (caller → callee) before proposing fixes
5. Check for similar patterns elsewhere that might need the same fix
### Root Cause Analysis Format
When reporting findings:
- **Symptom**: What the user observes
- **Root Cause**: The actual code defect (cite file:line)
- **Fix**: Minimal code change with rationale
- **Verification**: How to confirm the fix works
## Document Generation
For document/report tasks:
1. Use document_plan first for multi-section documents
2. Gather all data via tools before writing
3. Use appropriate format (html_create for rich docs, markdown_create for technical docs)
4. Include data tables, code snippets, and evidence from actual project files
5. Review the generated document with document_review
## Multi-Agent Delegation
Use spawn_agent / spawn_agents when:
- Task has 2+ independent research questions → spawn researchers
- Need to review code AND gather metrics simultaneously → spawn reviewer + researcher
- Writing a document while investigating code → spawn writer + researcher
Each sub-agent must have:
- Clear, atomic task description
- Specific expected output format
- Appropriate profile (researcher/coder/writer/reviewer/planner)
## Response Format
- Explanations: concise, action-oriented
- Code changes: show diff context, explain the "why" not the "what"
- Final summary: bullet points of changes made + verification results
@@ -0,0 +1,61 @@
# Gemma Model — Detailed Execution Prompt
## Core Constraints
Gemma has a small context window. Every token counts. Follow these rules strictly.
## Rules (3 only)
1. ALWAYS use tools. Respond ONLY with tool calls when action is needed.
2. ONE tool per response. Wait for the result before the next step.
3. NEVER guess file contents. Read first, then act.
## Tool Priority
When unsure what to do:
- file_read: see what's in a file
- grep: find where something is
- glob: find files by name pattern
- folder_map: see project structure
- build_run: check if code compiles
- file_edit: change code (read first!)
## Editing Sequence
```
file_read → file_edit → build_run
```
Always this order. Never skip steps.
## Tool Call Format (STRICT)
Use ONLY this exact format — do NOT use pipe-wrapped tokens, `call;`, or any variant:
```
<tool_call>
{"name": "TOOL_NAME", "arguments": {"param": "value"}}
</tool_call>
```
Forbidden (examples of WRONG formats the server will reject):
- `<|tool_call>call;name{...}<tool_call|>`
- `<|tool_call|>name{...}<|/tool_call|>`
- Any output with `<|"|>` as a string delimiter
Arguments MUST be valid JSON with double-quoted keys and string values.
## Output Rules
- Maximum 2 sentences between tool calls
- No preamble. No "I'll help you with..."
- No numbered plans. Just act.
- Final answer: 1-3 bullet points of what changed
## Language
- Code: match the existing codebase
- Explanation: match user's language
- Keep it short
START WITH A TOOL CALL NOW.
+97
View File
@@ -0,0 +1,97 @@
# Kimi/Moonshot Model — Detailed Execution Prompt
## Execution Rules
Kimi has a large context window (128K+) but tends toward verbose explanations. Counteract this with strict conciseness rules.
### Conciseness Protocol
- Maximum 3 sentences of explanation between tool calls.
- Never repeat what a tool result already shows.
- Never explain what you're "about to do" — just do it.
- If the user can see the tool result, don't summarize it.
### Mandatory Verification
After EVERY file_edit → immediately call build_run.
This is non-negotiable. No exceptions. The sequence is:
```
file_read → file_edit → build_run → (pass? next task : fix → build_run)
```
## Structured Analysis Format
When analyzing code, documents, or issues, ALWAYS use this format:
### For Code Analysis
```
## [Finding Title]
- **Evidence**: [file:line] — [exact code snippet]
- **Impact**: P0 (critical) / P1 (high) / P2 (medium) / P3 (low)
- **Category**: bug / performance / security / maintainability / style
- **Recommendation**: [specific action with code example]
```
### For Document Review
```
## [Section/Issue]
- **Location**: [section name or page]
- **Issue**: [concise description]
- **Severity**: error / warning / suggestion
- **Fix**: [specific correction]
```
## Tool Usage Patterns
### Investigation Pattern
1. folder_map → understand structure
2. grep → find relevant files
3. file_read → examine specific code
4. Analyze and report using structured format
### Fix Pattern
1. file_read → understand current state
2. file_edit → apply fix (exact old_string match)
3. build_run → verify compilation
4. test_loop → verify no regression (if tests exist)
5. Brief summary of change
### Document Creation Pattern
1. Research: gather data via tools (file_read, grep, code_review)
2. Plan: document_plan for structure
3. Create: html_create / docx_create / markdown_create
4. Review: document_review for quality check
## Code Editing Standards
1. Read before edit — ALWAYS.
2. Minimal diff — change only the necessary lines.
3. Preserve formatting — match existing indentation, spacing, brace style.
4. Type-safe changes — no implicit `any`, no null coercion without checks.
5. Build after edit — ALWAYS run build_run.
## Multi-File Operations
When a task requires changes to multiple files:
1. Plan the dependency order (models → services → views)
2. Edit files in dependency order
3. Build after each file (not just at the end)
4. If build fails on file N, fix before proceeding to file N+1
## Response Style
- Use Korean for explanations when user writes in Korean
- Use English for tool parameters and code
- Technical terms: keep in English (don't translate class names, method names, etc.)
- Numbers and data: use exact values from tool results, never estimate
## Error Recovery
If a tool call fails:
1. Identify the error type (path not found? permission? syntax?)
2. Fix the specific issue
3. Retry with corrected parameters
4. After 2 failures: try alternative approach, explain briefly why
@@ -0,0 +1,45 @@
# Llama Model — Detailed Execution Prompt
## Execution Rules
You are a code assistant with tool access. Use tools to gather information and make changes. Do not guess or speculate when tools can provide the answer.
## Tool Calling Protocol
1. Start with tools — read files, search code, understand structure before acting.
2. Call multiple independent tools in the same response when possible.
3. After code edits, ALWAYS run build_run to verify.
4. After 3+ edits, run test_loop for regression testing.
### Common Patterns
**Investigate**: folder_map → grep → file_read → analyze
**Fix**: file_read → file_edit → build_run → (test_loop if applicable)
**Create**: research → document_plan → create → review
## Code Quality
- Minimal changes: only modify what's needed
- Read before edit: always
- Build after edit: always
- Match existing style: indentation, naming, comments
- Handle errors: check null, empty, edge cases
## Response Style
- Concise: max 3 sentences between tool calls
- Action-oriented: do, don't describe plans to do
- Structured: use bullet points for multi-item results
- Match user's language for explanations
## Error Recovery
On tool failure: read error → fix parameters → retry (max 2 attempts) → try alternative approach.
## Analysis Format
When reporting findings:
- **What**: brief description
- **Where**: file:line reference
- **Impact**: severity (P0-P3)
- **Fix**: specific recommendation
@@ -0,0 +1,44 @@
# Mistral/Mixtral Model — Detailed Execution Prompt
## Execution Philosophy
Mistral excels at reasoning. Use this strength for analysis and planning, but always follow reasoning with tool execution in the same response.
## Tool Calling Protocol
1. Think briefly (1-2 sentences max), then act with tools.
2. Parallel calls: when tasks are independent, call multiple tools at once.
3. After code edits: build_run is mandatory.
4. After investigation: summarize with structured findings.
### Verification Chain
```
file_read → file_edit → build_run → (pass? continue : diagnose → fix → build_run)
```
## Code Standards
- Read before edit: mandatory
- Minimal diff: change only what's needed
- Type safety: preserve or improve
- Build verification: after every edit
- Test coverage: run test_loop after 3+ edits
## Analysis Protocol
When analyzing code or issues:
1. Gather evidence via tools (grep, file_read, code_review)
2. Trace the relevant call chain
3. Report findings with:
- **Finding**: concise description
- **Evidence**: file:line with code reference
- **Severity**: P0/P1/P2/P3
- **Recommendation**: specific fix
## Response Format
- Reasoning: brief, inline (not separate section)
- Actions: tool calls immediately after reasoning
- Results: bullet-point summary
- Language: match user's language for explanations, English for code
+65
View File
@@ -0,0 +1,65 @@
# Qwen Model — Detailed Execution Prompt
## Critical Behavior Rules
[MUST] Start EVERY response with a tool call. No text before tool_call.
[MUST] Call multiple independent tools in the same response when possible.
[NEVER] Say "알겠습니다", "네", "확인했습니다", "I understand" before a tool call.
[NEVER] Output text-only when a tool action is still needed.
[NEVER] Repeat the user's request back to them — just do it.
## Tool Calling Protocol
You MUST follow this protocol for every turn:
1. Read the user's request
2. Immediately call the first relevant tool (file_read, grep, glob, folder_map, etc.)
3. After receiving tool results, call the next tool or produce your final answer
4. If you are unsure, call a tool to gather information — do NOT guess
### When to Use Each Tool
- **file_read**: When you need to see file contents. ALWAYS read before editing.
- **grep / glob**: When searching for code patterns or files. Use grep for content, glob for filenames.
- **file_edit**: When modifying files. You MUST read the file first. Use exact old_string match.
- **build_run**: After ANY file edit, run the build to verify. Do not skip this step.
- **test_loop**: After 3+ file edits, run tests to catch regressions.
- **folder_map**: To understand project structure before diving into files.
### Parallel Tool Calls
When multiple tools are independent, call them ALL in the same response:
GOOD: Call file_read for 3 different files simultaneously
BAD: Read file A, wait, read file B, wait, read file C
### Error Recovery
If a tool call fails:
1. Read the error message carefully
2. Fix the parameters (wrong path? wrong old_string?)
3. Try again with corrected parameters
4. After 2 failures on the same operation, try an alternative approach
## Code Editing Rules
1. ALWAYS read the file before editing (file_read → file_edit)
2. Use the EXACT string from the file as old_string — copy precisely
3. After editing, run build_run to verify the build passes
4. If build fails, read the error, fix the issue, build again
5. Keep changes minimal — change only what's needed
## Response Format
- Between tool calls: maximum 1 sentence of explanation
- Final answer: concise summary of what was done
- Never list what you "plan to do" — just do it
- Use bullet points for multi-item results
## Language
- Tool parameters: always in the language of the existing code
- Explanations to user: match the user's language (Korean if they write Korean)
- Code comments: match existing codebase conventions
REMINDER: Your FIRST output in EVERY response MUST be a tool_call. Begin now.
+1 -1
View File
@@ -6,5 +6,5 @@
"purpose": "업무 편의성 증가 및 시스템의 직관적인 연결을 위해 제작",
"copyright": "© 2026 AX연구소",
"blogUrl": "www.swarchitect.net",
"contributors": "경윤영님, 윤지영님, 배지훈님"
"contributors": "경윤영님, 윤지영님"
}
+3
View File
@@ -74,6 +74,7 @@
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.0" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.2903.40" />
<PackageReference Include="QRCoder" Version="1.6.0" />
<PackageReference Include="System.IO.Hashing" Version="8.0.0" />
<PackageReference Include="System.Security.Cryptography.ProtectedData" Version="8.0.0" />
<PackageReference Include="System.ServiceProcess.ServiceController" Version="8.0.1" />
<PackageReference Include="UglyToad.PdfPig" Version="1.7.0-custom-5" />
@@ -111,6 +112,8 @@
<Resource Include="Assets\Quotes\greetings.json" />
<!-- 대화 주제 프리셋: 9종 시스템 프롬프트 (빌드 시 내장) -->
<EmbeddedResource Include="Assets\Presets\*.json" />
<!-- 모델별 상세 프롬프트: 임베디드 리소스 (EXE에 포함, 개발자만 수정 가능) -->
<EmbeddedResource Include="Assets\ModelPrompts\*.md" />
<!-- 마스코트 이미지: 내장 리소스 (EXE에 포함됨, 교체 시 재빌드 필요) -->
<Resource Include="Assets\mascot.png" Condition="Exists('Assets\mascot.png')" />
<Resource Include="Assets\mascot.jpg" Condition="Exists('Assets\mascot.jpg')" />
+71 -4
View File
@@ -9,7 +9,7 @@ public class AppSettings
/// <summary>
/// AI 기능 활성화 여부. false이면 ! 명령어 차단 + 설정의 AX Agent 탭 숨김.
/// claw code 미포함 배포는 false로 설정합니다.
/// AI 엔진 미포함 배포는 false로 설정합니다.
/// </summary>
[JsonPropertyName("ai_enabled")]
public bool AiEnabled { get; set; } = true;
@@ -881,6 +881,10 @@ public class LlmSettings
[JsonPropertyName("autoPreview")]
public string AutoPreview { get; set; } = "off";
/// <summary>미리보기 패널 너비 (픽셀). 드래그 조절 후 저장.</summary>
[JsonPropertyName("previewPanelWidth")]
public double PreviewPanelWidth { get; set; } = 420;
/// <summary>에이전트 최대 루프 반복 횟수.</summary>
[JsonPropertyName("maxAgentIterations")]
public int MaxAgentIterations { get; set; } = 25;
@@ -905,6 +909,10 @@ public class LlmSettings
[JsonPropertyName("agentLogLevel")]
public string AgentLogLevel { get; set; } = "detailed";
/// <summary>IBM+Qwen 조합 상세 진단 로깅 활성화. true 시 [IBM진단] 태그 Debug 로그 출력. 기본 false.</summary>
[JsonPropertyName("enableIbmDiagnosticLog")]
public bool EnableIbmDiagnosticLog { get; set; } = false;
/// <summary>AX Agent UI 표현 수준. rich | balanced | simple</summary>
[JsonPropertyName("agentUiExpressionLevel")]
public string AgentUiExpressionLevel { get; set; } = "balanced";
@@ -999,7 +1007,7 @@ public class LlmSettings
/// <summary>서브에이전트 최대 동시 실행 수. 기본 3.</summary>
[JsonPropertyName("maxSubAgents")]
public int MaxSubAgents { get; set; } = 3;
public int MaxSubAgents { get; set; } = 5;
/// <summary>PDF 내보내기 기본 경로. 빈 문자열이면 바탕화면.</summary>
[JsonPropertyName("pdfExportPath")]
@@ -1071,7 +1079,7 @@ public class LlmSettings
[JsonPropertyName("enableChatRainbowGlow")]
public bool EnableChatRainbowGlow { get; set; } = false;
/// <summary>새로운 계획 뷰어(V2 사이드바 레이아웃) 사용. 기본 true.</summary>
/// <summary>새로운 뷰어 표시 버전(V2 사이드바 레이아웃) 사용. 기본 true.</summary>
[JsonPropertyName("enableNewPlanViewer")]
public bool EnableNewPlanViewer { get; set; } = true;
@@ -1079,6 +1087,58 @@ public class LlmSettings
[JsonPropertyName("enableNewChatRendering")]
public bool EnableNewChatRendering { get; set; } = false;
/// <summary>IntentGate LLM 폴백 활성화. 키워드 분류 confidence가 낮을 때 LLM 1-shot 분류 사용.</summary>
[JsonPropertyName("enableIntentGateLlmFallback")]
public bool EnableIntentGateLlmFallback { get; set; } = false;
/// <summary>IntentGate confidence 임계값. 이 값 미만이면 LLM 폴백 발동 (EnableIntentGateLlmFallback=true 시).</summary>
[JsonPropertyName("intentGateConfidenceThreshold")]
public double IntentGateConfidenceThreshold { get; set; } = 0.6;
/// <summary>세션 내 누적 학습 활성화. 도구 결과에서 학습 포인트를 자동 수집하여 후속 반복에 주입.</summary>
[JsonPropertyName("enableSessionLearnings")]
public bool EnableSessionLearnings { get; set; } = true;
/// <summary>최대 누적 학습 항목 수. FIFO로 관리.</summary>
[JsonPropertyName("maxSessionLearnings")]
public int MaxSessionLearnings { get; set; } = 10;
/// <summary>워크스페이스 컨텍스트 자동 생성 (.ax-context.md) 활성화.</summary>
[JsonPropertyName("enableAutoWorkspaceContext")]
public bool EnableAutoWorkspaceContext { get; set; } = true;
/// <summary>
/// 모델별 프롬프트 전략 수준.
/// "off": 모든 모델에 동일한 기본 프롬프트 사용 (기존 동작).
/// "basic": 모델 패밀리별 가벼운 프롬프트 어댑테이션 (규칙 추가/재배치).
/// "detailed": 모델별 전용 프롬프트 파일 적용 (임베디드 리소스, 수백 줄급 상세 지침).
/// </summary>
[JsonPropertyName("modelPromptLevel")]
public string ModelPromptLevel { get; set; } = "off";
/// <summary>이전 호환: EnableModelSpecificPrompting=true → "basic" 으로 마이그레이션.</summary>
[JsonPropertyName("enableModelSpecificPrompting")]
public bool EnableModelSpecificPrompting
{
get => !string.Equals(ModelPromptLevel, "off", StringComparison.OrdinalIgnoreCase);
set
{
if (value && string.Equals(ModelPromptLevel, "off", StringComparison.OrdinalIgnoreCase))
ModelPromptLevel = "basic";
else if (!value)
ModelPromptLevel = "off";
}
}
/// <summary>
/// Hash-Anchored Edits 활성화.
/// true: file_read 출력에 라인별 해시 앵커를 포함하고, file_edit에서 앵커 기반 편집 지원.
/// 편집 성공률이 크게 향상되며 스테일 편집을 자동 감지합니다.
/// false: 기존 라인번호 + old_string 방식 유지.
/// </summary>
[JsonPropertyName("enableHashAnchoredEdits")]
public bool EnableHashAnchoredEdits { get; set; } = false;
/// <summary>AX Agent 전용 테마. system | light | dark</summary>
[JsonPropertyName("agentTheme")]
public string AgentTheme { get; set; } = "system";
@@ -1137,7 +1197,7 @@ public class LlmSettings
public List<string> BlockedPaths { get; set; } = new()
{
"*\\Windows\\*", "*\\Program Files\\*", "*\\Program Files (x86)\\*",
"*\\System32\\*", "*\\AppData\\Local\\*", "*Documents*",
"*\\System32\\*", "*\\AppData\\Local\\*",
};
/// <summary>차단할 파일 확장자 목록.</summary>
@@ -1441,6 +1501,13 @@ public class RegisteredModel
[JsonPropertyName("executionProfile")]
public string ExecutionProfile { get; set; } = "balanced";
/// <summary>
/// 프롬프트 전략 패밀리. qwen | deepseek | kimi | gemma | llama | mistral | phi | yi | gemini | claude | default
/// 비어있으면 모델명에서 자동 감지합니다.
/// </summary>
[JsonPropertyName("promptFamily")]
public string PromptFamily { get; set; } = "";
/// <summary>이 모델 전용 서버 엔드포인트. 비어있으면 LlmSettings의 기본 엔드포인트 사용.</summary>
[JsonPropertyName("endpoint")]
public string Endpoint { get; set; } = "";
+1 -1
View File
@@ -130,7 +130,7 @@ public class ChatConversation
public List<DraftQueueItem> DraftQueueItems { get; set; } = new();
[JsonPropertyName("showExecutionHistory")]
public bool ShowExecutionHistory { get; set; } = false;
public bool ShowExecutionHistory { get; set; } = true;
[JsonPropertyName("agentRunHistory")]
public List<ChatAgentRunRecord> AgentRunHistory { get; set; } = new();

Some files were not shown because too many files have changed in this diff Show More