fix: .gitignore에서 .agent/ 트래킹 + 테스트 절대경로 제거 (이식성 보장)

This commit is contained in:
quantlab
2026-03-06 17:58:11 +09:00
parent b6ed432fe3
commit 5a931a5480
11 changed files with 425 additions and 7 deletions

View File

@@ -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 }
```

View File

@@ -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 태스크 목록 + 우선순위
- 다음 작업 제안

View File

@@ -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 보존을 보장합니다.

21
.agent/workflows/dev.md Normal file
View File

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

47
.agent/workflows/git.md Normal file
View File

@@ -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: 버전별 변경 이력

18
.agent/workflows/sync.md Normal file
View File

@@ -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 관련 페이지 업데이트

24
.agent/workflows/test.md Normal file
View File

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

View File

@@ -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()

3
.gitignore vendored
View File

@@ -1,4 +1,5 @@
.agent/ # .agent/workflows/services.md 는 토큰이 포함되어 있으므로 제외
.agent/workflows/services.md
sessions/ sessions/
__pycache__/ __pycache__/
*.pyc *.pyc

View File

@@ -5,15 +5,18 @@ Tests against the variet-agent project itself.
import sys import sys
import io import io
import os
if sys.stdout.encoding != "utf-8": if sys.stdout.encoding != "utf-8":
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") 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.project_indexer import ProjectIndex
from core.context_manager import ContextManager from core.context_manager import ContextManager
PROJECT = r"C:\Users\CafeVariet-GL552VW\Desktop\source_diff\variet-agent"
def test_indexer(): def test_indexer():
print("=" * 60) print("=" * 60)

View File

@@ -8,15 +8,17 @@ import io
import asyncio import asyncio
import json import json
import time import time
import os
if sys.stdout.encoding != "utf-8": if sys.stdout.encoding != "utf-8":
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") 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 from core.task_pipeline import TaskPipeline
PROJECT = r"C:\Users\CafeVariet-GL552VW\Desktop\source_diff\variet-agent"
USER_REQUEST = ( USER_REQUEST = (
"project_indexer.py의 find_relevant 함수가 공백이 포함된 쿼리를 처리하지 못합니다. " "project_indexer.py의 find_relevant 함수가 공백이 포함된 쿼리를 처리하지 못합니다. "
"'gemini caller'로 검색하면 gemini_caller.py를 찾지 못합니다. " "'gemini caller'로 검색하면 gemini_caller.py를 찾지 못합니다. "