diff --git a/.agent/workflows/check-gitea.md b/.agent/workflows/check-gitea.md new file mode 100644 index 0000000..bbb3cb9 --- /dev/null +++ b/.agent/workflows/check-gitea.md @@ -0,0 +1,38 @@ +--- +description: Gitea API로 variet-agent 저장소 커밋/이슈/PR 현황을 조회하는 워크플로우 +--- + +# Gitea 저장소 현황 조회 + +서비스 정보는 `.agent/workflows/services.md` 참조. + +// turbo-all + +## 절차 + +1. 최근 커밋 조회 (최신 10개): +```powershell +$h = @{Authorization="token 3a01b4b15a39921572e64c413353e870d4d2161b"} +$commits = Invoke-RestMethod -Uri "https://git.variet.net/api/v1/repos/Variet/variet-agent/commits?limit=10&sha=main" -Headers $h +$commits | ForEach-Object { Write-Host "$($_.sha.Substring(0,7)) $($_.commit.message.Split("`n")[0])" } +``` + +2. 열린 이슈 조회: +```powershell +$h = @{Authorization="token 3a01b4b15a39921572e64c413353e870d4d2161b"} +$issues = Invoke-RestMethod -Uri "https://git.variet.net/api/v1/repos/Variet/variet-agent/issues?state=open&type=issues" -Headers $h +$issues | ForEach-Object { Write-Host "#$($_.number) $($_.title)" } +``` + +3. PR 조회: +```powershell +$h = @{Authorization="token 3a01b4b15a39921572e64c413353e870d4d2161b"} +Invoke-RestMethod -Uri "https://git.variet.net/api/v1/repos/Variet/variet-agent/pulls?state=open" -Headers $h +``` + +4. Wiki 페이지 목록: +```powershell +$h = @{Authorization="token 3a01b4b15a39921572e64c413353e870d4d2161b"} +$pages = Invoke-RestMethod -Uri "https://git.variet.net/api/v1/repos/Variet/variet-agent/wiki/pages" -Headers $h +$pages | ForEach-Object { Write-Host $_.title } +``` diff --git a/.agent/workflows/check-status.md b/.agent/workflows/check-status.md new file mode 100644 index 0000000..c01cfac --- /dev/null +++ b/.agent/workflows/check-status.md @@ -0,0 +1,43 @@ +--- +description: 프로젝트 전체 작업 현황을 종합 체크하는 워크플로우 (Git + Vikunja + 로컬) +--- + +# 프로젝트 현황 종합 체크 + +"작업상황 체크", "현황 확인", "status check" 등 요청 시 이 워크플로우를 실행합니다. + +// turbo-all + +## 절차 + +1. Git 로컬 상태 확인: +```powershell +git -C "c:\Users\CafeVariet-GL552VW\Desktop\source_diff\variet-agent" status --short +``` +```powershell +git -C "c:\Users\CafeVariet-GL552VW\Desktop\source_diff\variet-agent" log --oneline -5 +``` + +2. Vikunja 태스크 현황 조회: +```powershell +$h = @{Authorization="Bearer tk_070f8e0b715e818bb7178c3815ed5389040eddca"} +$tasks = Invoke-RestMethod -Uri "https://plan.variet.net/api/v1/projects/7/tasks?per_page=50" -Headers $h +$todo = $tasks | Where-Object { -not $_.done } +$done = $tasks | Where-Object { $_.done } +Write-Host "=== Vikunja: TODO $($todo.Count), DONE $($done.Count) ===" +$todo | ForEach-Object { Write-Host " #$($_.id) $($_.title)" } +``` + +3. Gitea 최근 커밋 확인 (리모트): +```powershell +$h = @{Authorization="token 3a01b4b15a39921572e64c413353e870d4d2161b"} +$commits = Invoke-RestMethod -Uri "https://git.variet.net/api/v1/repos/Variet/variet-agent/commits?limit=5&sha=main" -Headers $h +Write-Host "=== Gitea: Recent Commits ===" +$commits | ForEach-Object { Write-Host " $($_.sha.Substring(0,7)) $($_.commit.message.Split("`n")[0])" } +``` + +4. 결과를 종합하여 사용자에게 보고: + - 로컬 uncommitted 변경 여부 + - 로컬 vs 리모트 커밋 차이 + - TODO 태스크 목록 + 우선순위 + - 다음 작업 제안 diff --git a/.agent/workflows/check-vikunja.md b/.agent/workflows/check-vikunja.md new file mode 100644 index 0000000..a82cd73 --- /dev/null +++ b/.agent/workflows/check-vikunja.md @@ -0,0 +1,50 @@ +--- +description: Vikunja API로 Variet Agent 프로젝트 태스크 현황을 조회하는 워크플로우 +--- + +# Vikunja 태스크 현황 조회 + +서비스 정보는 `.agent/workflows/services.md` 참조. + +// turbo-all + +## 절차 + +1. 간편 조회 (TODO/DONE 리스트): +```powershell +C:\ProgramData\miniforge3\envs\quant\python.exe .agent\workflows\vikunja_helper.py list +``` + +2. TODO만 조회: +```powershell +C:\ProgramData\miniforge3\envs\quant\python.exe .agent\workflows\vikunja_helper.py list todo +``` + +3. DONE만 조회: +```powershell +C:\ProgramData\miniforge3\envs\quant\python.exe .agent\workflows\vikunja_helper.py list done +``` + +4. 태스크 완료 처리 (**⚠️ 반드시 이 방법 사용 — 직접 API 호출 금지**): +```powershell +C:\ProgramData\miniforge3\envs\quant\python.exe .agent\workflows\vikunja_helper.py done {TASK_ID} +``` + 여러 태스크 동시: +```powershell +C:\ProgramData\miniforge3\envs\quant\python.exe .agent\workflows\vikunja_helper.py done 71 77 78 +``` + +5. 태스크 코멘트 추가: +```powershell +C:\ProgramData\miniforge3\envs\quant\python.exe .agent\workflows\vikunja_helper.py comment {TASK_ID} "내용" +``` + +6. 새 태스크 생성: +```powershell +C:\ProgramData\miniforge3\envs\quant\python.exe .agent\workflows\vikunja_helper.py create "제목" "설명" +``` + +> [!CAUTION] +> **절대로** `Invoke-RestMethod -Method Post -Body '{"done": true}'` 같은 직접 API 호출을 사용하지 마세요. +> Vikunja API는 POST 시 body에 포함되지 않은 필드를 빈값으로 덮어씁니다. +> `vikunja_helper.py`는 항상 GET → 기존 필드 보존 → POST 패턴을 사용하여 title/description 보존을 보장합니다. diff --git a/.agent/workflows/dev.md b/.agent/workflows/dev.md new file mode 100644 index 0000000..c77ac29 --- /dev/null +++ b/.agent/workflows/dev.md @@ -0,0 +1,21 @@ +--- +description: 개발 서버 실행 방법 +--- + +## 환경 설정 + +1. Python 환경 활성화 +// turbo +``` +C:\ProgramData\miniforge3\envs\quant\python.exe -m pip install -r requirements.txt +``` + +2. API 서버 실행 +``` +C:\ProgramData\miniforge3\envs\quant\python.exe -m uvicorn api.server:app --reload --host 0.0.0.0 --port 8100 +``` + +3. Discord Bot 실행 (별도 터미널) +``` +C:\ProgramData\miniforge3\envs\quant\python.exe -m api.discord_bot +``` diff --git a/.agent/workflows/git.md b/.agent/workflows/git.md new file mode 100644 index 0000000..ff51d47 --- /dev/null +++ b/.agent/workflows/git.md @@ -0,0 +1,47 @@ +--- +description: Git 및 Gitea 워크플로우 +--- + +## 저장소 정보 +- **Remote**: https://git.variet.net/Variet/variet-agent.git +- **기본 브랜치**: main +- **Vikunja 프로젝트**: https://plan.variet.net/projects/7 + +## 커밋 컨벤션 +``` +feat: 새 기능 +fix: 버그 수정 +docs: 문서 변경 +refactor: 리팩토링 +test: 테스트 추가/수정 +chore: 빌드/설정 변경 +``` + +## 작업 흐름 + +1. 브랜치 생성 +// turbo +``` +git checkout -b feat/feature-name +``` + +2. 커밋 +// turbo +``` +git add -A && git commit -m "feat: description" +``` + +3. 푸시 +``` +git push origin feat/feature-name +``` + +4. Gitea에서 PR 생성 또는 API로 자동 생성 + +## Wiki 업데이트 +작업 완료 시 Gitea Wiki에 관련 내용 업데이트. +Wiki 구조: +- Home: 프로젝트 개요 +- Architecture: 아키텍처 설명 +- Design-Decisions: 설계 결정 이유 +- Changelog: 버전별 변경 이력 diff --git a/.agent/workflows/sync.md b/.agent/workflows/sync.md new file mode 100644 index 0000000..64ed070 --- /dev/null +++ b/.agent/workflows/sync.md @@ -0,0 +1,18 @@ +--- +description: 작업 동기화 및 진행 보고 절차 +--- + +## 작업 시작 시 +1. Vikunja (plan.variet.net/projects/7) 에서 해당 태스크를 "진행 중"으로 변경 +2. 관련 브랜치 생성 (feat/ 또는 fix/) + +## 작업 중간 동기화 +1. 의미 있는 단위로 커밋 + 푸시 +2. 필요 시 Gitea Wiki 업데이트 (Architecture, Changelog 등) +3. 사용자에게 디스코드 또는 Vikunja 코멘트로 진행 상황 보고 + +## 작업 완료 시 +1. Gitea PR 생성 +2. Vikunja 태스크 "완료"로 변경 +3. Changelog 업데이트 +4. Wiki 관련 페이지 업데이트 diff --git a/.agent/workflows/test.md b/.agent/workflows/test.md new file mode 100644 index 0000000..bcd90ca --- /dev/null +++ b/.agent/workflows/test.md @@ -0,0 +1,24 @@ +--- +description: 테스트 실행 방법 +--- + +## 단위 테스트 +// turbo +``` +C:\ProgramData\miniforge3\envs\quant\python.exe -m pytest tests/ -v +``` + +## Context Manager 효과 테스트 +``` +C:\ProgramData\miniforge3\envs\quant\python.exe -m tests.test_context_manager +``` + +## Task Pipeline E2E +``` +C:\ProgramData\miniforge3\envs\quant\python.exe -m tests.test_pipeline_e2e +``` + +## Gemini CLI 연동 테스트 +``` +gemini -p "Hello, respond with 'OK'" --approval-mode yolo -o json +``` diff --git a/.agent/workflows/vikunja_helper.py b/.agent/workflows/vikunja_helper.py new file mode 100644 index 0000000..610e3ab --- /dev/null +++ b/.agent/workflows/vikunja_helper.py @@ -0,0 +1,171 @@ +"""Vikunja safe task updater — preserves existing fields when updating tasks. + +Usage: + python vikunja_helper.py done 75 # Mark task #75 as done + python vikunja_helper.py done 71 77 78 # Mark multiple tasks done + python vikunja_helper.py undone 75 # Mark task #75 as not done + python vikunja_helper.py comment 75 "text" # Add comment to task #75 + python vikunja_helper.py desc 75 "text" # Set description (appends if exists) + python vikunja_helper.py create "title" "description" # Create a new task + python vikunja_helper.py create "title" "description" --done # Create and mark done + python vikunja_helper.py list # List all tasks + python vikunja_helper.py list todo # List TODO only + python vikunja_helper.py list done # List DONE only +""" + +import sys +import json +import urllib.request +import urllib.error +import io + +# Fix Windows console encoding (cp949 → utf-8) +if sys.stdout.encoding != "utf-8": + sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") + +API_BASE = "https://plan.variet.net/api/v1" +TOKEN = "tk_070f8e0b715e818bb7178c3815ed5389040eddca" +PROJECT_ID = 7 + +HEADERS = { + "Authorization": f"Bearer {TOKEN}", + "Content-Type": "application/json", +} + + +def api_get(path: str): + req = urllib.request.Request(f"{API_BASE}{path}", headers=HEADERS) + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read().decode("utf-8")) + + +def api_post(path: str, data: dict): + body = json.dumps(data).encode("utf-8") + req = urllib.request.Request(f"{API_BASE}{path}", data=body, headers=HEADERS, method="POST") + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read().decode("utf-8")) + + +def api_put(path: str, data: dict): + body = json.dumps(data).encode("utf-8") + req = urllib.request.Request(f"{API_BASE}{path}", data=body, headers=HEADERS, method="PUT") + with urllib.request.urlopen(req) as resp: + return json.loads(resp.read().decode("utf-8")) + + +def get_task(task_id: int) -> dict: + """GET full task object — preserves all fields.""" + return api_get(f"/tasks/{task_id}") + + +def safe_update_task(task_id: int, updates: dict) -> dict: + """Safely update task: GET first, then POST with preserved fields.""" + task = get_task(task_id) + safe_body = { + "title": task.get("title", ""), + "description": task.get("description", ""), + "priority": task.get("priority", 0), + "done": task.get("done", False), + } + safe_body.update(updates) + return api_post(f"/tasks/{task_id}", safe_body) + + +def mark_done(task_ids: list[int]): + for tid in task_ids: + result = safe_update_task(tid, {"done": True}) + title = result.get("title", "?") + print(f" ✅ #{tid} → done=True [{title}]") + + +def mark_undone(task_ids: list[int]): + for tid in task_ids: + result = safe_update_task(tid, {"done": False}) + title = result.get("title", "?") + print(f" ⬜ #{tid} → done=False [{title}]") + + +def add_comment(task_id: int, comment: str): + result = api_put(f"/tasks/{task_id}/comments", {"comment": comment}) + print(f" 💬 #{task_id} comment added (id={result.get('id', '?')})") + + +def set_description(task_id: int, desc: str, append: bool = True): + task = get_task(task_id) + existing = task.get("description", "") or "" + if append and existing: + new_desc = existing.rstrip() + "\n\n" + desc + else: + new_desc = desc + result = safe_update_task(task_id, {"description": new_desc}) + print(f" 📝 #{task_id} description updated [{result.get('title', '?')}]") + + +def list_tasks(filter_: str = "all"): + tasks = api_get(f"/projects/{PROJECT_ID}/tasks?per_page=100") + if filter_ == "todo": + tasks = [t for t in tasks if not t["done"]] + elif filter_ == "done": + tasks = [t for t in tasks if t["done"]] + + tasks.sort(key=lambda t: t["id"]) + for t in tasks: + status = "✅" if t["done"] else "⬜" + desc = (t.get("description") or "")[:50].replace("\n", " ") + labels = ", ".join(l["title"] for l in (t.get("labels") or [])) + print(f" {status} #{t['id']:3d} {t['title'][:40]:<40} [{labels}] {desc}") + print(f"\n Total: {len(tasks)} tasks") + + +def create_task(title: str, description: str = "", done: bool = False): + """Create a new task in the project.""" + payload = {"title": title, "description": description} + result = api_put(f"/projects/{PROJECT_ID}/tasks", payload) + task_id = result["id"] + print(f" ✨ #{task_id} created: {result.get('title', '?')}") + if done: + result = safe_update_task(task_id, {"done": True}) + print(f" ✅ #{task_id} → done=True") + return result + + +def main(): + if len(sys.argv) < 2: + print(__doc__) + return + + cmd = sys.argv[1].lower() + + if cmd == "done": + ids = [int(x) for x in sys.argv[2:]] + mark_done(ids) + elif cmd == "undone": + ids = [int(x) for x in sys.argv[2:]] + mark_undone(ids) + elif cmd == "comment": + tid = int(sys.argv[2]) + comment = sys.argv[3] + add_comment(tid, comment) + elif cmd == "desc": + tid = int(sys.argv[2]) + desc = sys.argv[3] + set_description(tid, desc) + elif cmd == "list": + f = sys.argv[2] if len(sys.argv) > 2 else "all" + list_tasks(f) + elif cmd == "create": + title = sys.argv[2] if len(sys.argv) > 2 else "" + desc = sys.argv[3] if len(sys.argv) > 3 else "" + is_done = "--done" in sys.argv + if not title: + print("Error: title is required") + print(' Usage: vikunja_helper.py create "title" "description" [--done]') + return + create_task(title, desc, done=is_done) + else: + print(f"Unknown command: {cmd}") + print(__doc__) + + +if __name__ == "__main__": + main() diff --git a/.gitignore b/.gitignore index e70b2dd..88f5ea2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ -.agent/ +# .agent/workflows/services.md 는 토큰이 포함되어 있으므로 제외 +.agent/workflows/services.md sessions/ __pycache__/ *.pyc diff --git a/tests/test_core.py b/tests/test_core.py index 78628b0..dbff511 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -5,15 +5,18 @@ Tests against the variet-agent project itself. import sys import io +import os + if sys.stdout.encoding != "utf-8": sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") -sys.path.insert(0, r"C:\Users\CafeVariet-GL552VW\Desktop\source_diff\variet-agent") + +# 프로젝트 루트를 동적으로 결정 (tests/ 상위 디렉토리) +PROJECT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, PROJECT) from core.project_indexer import ProjectIndex from core.context_manager import ContextManager -PROJECT = r"C:\Users\CafeVariet-GL552VW\Desktop\source_diff\variet-agent" - def test_indexer(): print("=" * 60) diff --git a/tests/test_pipeline_e2e.py b/tests/test_pipeline_e2e.py index 142bed1..93290a5 100644 --- a/tests/test_pipeline_e2e.py +++ b/tests/test_pipeline_e2e.py @@ -8,15 +8,17 @@ import io import asyncio import json import time +import os if sys.stdout.encoding != "utf-8": sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") -sys.path.insert(0, r"C:\Users\CafeVariet-GL552VW\Desktop\source_diff\variet-agent") + +# 프로젝트 루트를 동적으로 결정 (tests/ 상위 디렉토리) +PROJECT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +sys.path.insert(0, PROJECT) from core.task_pipeline import TaskPipeline -PROJECT = r"C:\Users\CafeVariet-GL552VW\Desktop\source_diff\variet-agent" - USER_REQUEST = ( "project_indexer.py의 find_relevant 함수가 공백이 포함된 쿼리를 처리하지 못합니다. " "'gemini caller'로 검색하면 gemini_caller.py를 찾지 못합니다. "