# 자동 머지(Auto-Merge) 시스템 스펙 문서

## 1. 개요

`.done` 파일 감지 → 보고서 분석 → 자동 머지 → 테스트 → 완료 처리를 **완전 자동화**하는 시스템입니다.

### 목표
- 팀장의 작업 완료 → .done 파일 생성 → **자동 감지 및 머지** → 완전 자동화
- 기존 도구 연동: `report_parser.py` (머지 판단), `worktree_manager.py` (머지 실행)
- 에스컬레이션 규칙 명확화: 언제 자동 처리, 언제 수동 개입

### 처리 흐름

```
┌─────────────────────────────────────────────────────────────┐
│         .done 파일 감지 (memory/events/*.done)              │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ↓
┌─────────────────────────────────────────────────────────────┐
│   1. 원자적 선점: .done.clear 생성 (중복 처리 방지)        │
│      - FileExistsError: 이미 처리 중 → 스킵                │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ↓
┌─────────────────────────────────────────────────────────────┐
│   2. .done 파싱: task_id, team_id, merge_needed 추출        │
└────────────────────────┬────────────────────────────────────┘
                         │
                         ↓
┌─────────────────────────────────────────────────────────────┐
│   3. 보고서 분석: merge_needed, merge_branch 확인           │
│      - report_parser.py로 분석                              │
└────────────────────────┬────────────────────────────────────┘
                         │
                  ┌──────┴──────┐
                  │             │
                  ↓             ↓
           (True)          (False)
                  │             │
┌─────────────────────────┐  ┌──────────────────┐
│  4. 머지 실행            │  │  완료 로깅        │
│  worktree_manager.py    │  │  .done.clear 생성 │
└────────────┬─────────────┘  └──────────────────┘
             │
             ↓
┌─────────────────────────────────────────────────────────────┐
│  5. 테스트 실행 (프로젝트 레벨)                              │
│     - pytest 또는 프로젝트 설정 테스트                       │
└────────────┬────────────────────────────────────────────────┘
             │
       ┌─────┴─────┐
       │           │
    (Pass)      (Fail)
       │           │
       ↓           ↓
┌──────────────┐  ┌──────────────────────┐
│ 성공 로깅    │  │ 머지 revert          │
│ merge-log.json│ │ + 에스컬레이션       │
└──────────────┘  └──────────────────────┘
```

---

## 2. 현재 프로세스 (.done 프로토콜)

### 2.1 팀장의 작업 완료 절차

1. worktree에서 작업 완료
2. 보고서 작성: `/home/jay/workspace/memory/reports/{task_id}.md`
   - 머지 판단 섹션 포함:
     - 머지 필요: "머지 판단은 아누에게 위임", "merge 필요" 등
     - 머지 불필요: "머지 필요 없음", "merge 불필요" 등
   - 브랜치명 명시: `task/task-{id}-{team}`
3. .done 파일 생성: `/home/jay/workspace/memory/events/{task_id}.done`
4. task-timer 종료, 아누에게 통보

### 2.2 현재 문제점

- **대화 기반 감지**: 아누가 대화 중에만 .done 파일 감지
- **.done 파일 누적**: 대화 없으면 처리되지 않음
- **수동 머지**: 아누 또는 제이회장님이 직접 머지 실행
- **에러 처리 미흡**: 머지 충돌, 테스트 실패 시 수동 개입

---

## 3. 자동화 프로세스 (auto_merge.py)

### 3.1 핵심 스크립트

`/home/jay/workspace/scripts/auto_merge.py` — 자동 머지 시스템

#### 주요 기능

```python
class AutoMerger:
    """자동 머지 시스템"""

    def __init__(self, workspace_path: str = "/home/jay/workspace"):
        self.workspace = Path(workspace_path)
        self.events_dir = self.workspace / "memory" / "events"
        self.reports_dir = self.workspace / "memory" / "reports"
        self.logs_dir = self.workspace / "logs"
        self.merge_log = self.workspace / "memory" / "merge-log.json"

    def scan_done_files(self) -> list[Path]:
        """미처리 .done 파일 스캔 (*.done, *.done.clear 제외)"""

    def try_claim(self, done_file: Path) -> bool:
        """원자적 .done.clear 생성으로 처리 선점"""

    def parse_done(self, done_file: Path) -> dict:
        """JSON 파싱 → task_id, team_id, merge_needed 추출"""

    def analyze_report(self, task_id: str) -> dict:
        """report_parser.py로 보고서 분석"""

    def resolve_project_path(self, task_id: str, report_data: dict) -> str:
        """프로젝트 경로 해결"""

    def execute_merge(self, project_path: str, task_id: str, team_id: str) -> dict:
        """worktree_manager.py finish --action merge 실행"""

    def run_tests(self, project_path: str) -> dict:
        """프로젝트 테스트 실행"""

    def revert_merge(self, project_path: str) -> bool:
        """머지 실패 시 되돌리기"""

    def escalate(self, task_id: str, reason: str, context: dict = None) -> None:
        """아누에게 에스컬레이션 통보"""

    def log_result(self, task_id: str, result: dict) -> None:
        """merge-log.json에 결과 기록"""

    def cleanup_done_file(self, done_file: Path) -> None:
        """.done → .done.clear rename"""

    def run(self) -> dict:
        """메인 실행: 스캔 → 처리 → 결과 요약"""
```

### 3.2 처리 단계별 상세

#### Step 1: 미처리 .done 파일 스캔

```python
def scan_done_files(self) -> list[Path]:
    """
    memory/events/ 디렉토리에서 *.done 파일만 수집.
    - .done.clear, .done.error 제외
    - 시간 오래된 순서로 정렬 (FIFO 처리)
    """
    if not self.events_dir.exists():
        return []

    done_files = [
        f for f in self.events_dir.glob("*.done")
        if f.name.endswith(".done") and not f.name.endswith(".clear")
    ]
    return sorted(done_files, key=lambda f: f.stat().st_mtime)
```

#### Step 2: 원자적 선점 (.done.clear 생성)

```python
def try_claim(self, done_file: Path) -> bool:
    """
    .done.clear 파일을 x 플래그로 원자적 생성.
    - 성공: True 반환 (이 프로세스가 처리)
    - FileExistsError: False 반환 (다른 프로세스 처리 중)
    """
    clear_file = done_file.with_suffix(".done.clear")
    try:
        clear_file.touch(exist_ok=False)  # x 플래그 사용 불가시 check=False
        # 또는
        with open(clear_file, 'x') as f:
            pass
        return True
    except FileExistsError:
        return False
```

#### Step 3: .done 파일 파싱

```python
def parse_done(self, done_file: Path) -> dict:
    """
    .done JSON 파일 파싱.

    Returns:
    {
        "task_id": "task-391.1",
        "team_id": "dev2",
        "merge_needed": true,
        "merge_branch": "task/task-391.1-dev2",
        "timestamp": "2026-03-07T15:30:00+09:00"
    }
    """
    try:
        with open(done_file, 'r') as f:
            data = json.load(f)
        return {
            "task_id": data.get("task_id"),
            "team_id": data.get("team_id"),
            "merge_needed": data.get("merge_needed", False),
            "merge_branch": data.get("merge_branch"),
            "timestamp": data.get("timestamp")
        }
    except (json.JSONDecodeError, KeyError) as e:
        return {"error": str(e), "merge_needed": False}
```

#### Step 4: 보고서 분석 (report_parser.py 활용)

```python
def analyze_report(self, task_id: str) -> dict:
    """
    report_parser.parse_report() 활용하여 보고서 분석.

    Returns:
    {
        "merge_needed": bool,
        "merge_branch": str,
        "merge_reason": str,
        "confidence": float (0.0 ~ 1.0),
        "project_path": str (optional)
    }
    """
    from report_parser import parse_report

    report_path = self.reports_dir / f"{task_id}.md"
    if not report_path.exists():
        return {"merge_needed": False, "error": "Report not found"}

    result = parse_report(str(report_path))
    return {
        "merge_needed": result.get("merge_needed", False),
        "merge_branch": result.get("merge_branch"),
        "merge_reason": result.get("merge_reason", ""),
        "confidence": result.get("confidence", 0.0),
        "project_path": self._extract_project_path(result)
    }
```

#### Step 5: 프로젝트 경로 해결

```python
def resolve_project_path(self, task_id: str, report_data: dict) -> str:
    """
    프로젝트 경로를 다음 순서로 추출:
    1. 보고서에서 regex 추출: /home/jay/projects/...
    2. task 메타 파일에서: memory/tasks/{task_id}.md
    3. worktree 경로에서 역추적: .worktrees 상위
    4. 못 찾으면: escalate
    """
    # 1. 보고서에서
    if report_data.get("project_path"):
        return report_data["project_path"]

    # 2. task 메타에서
    task_file = self.workspace / "memory" / "tasks" / f"{task_id}.md"
    if task_file.exists():
        content = task_file.read_text()
        match = re.search(r'/home/jay/projects/\w+', content)
        if match:
            return match.group(0)

    # 3. worktree에서 역추적
    for project_dir in Path("/home/jay/projects").iterdir():
        if project_dir.is_dir():
            worktrees = project_dir / ".worktrees"
            if worktrees.exists():
                for wt in worktrees.iterdir():
                    # task_id와 매칭하는 worktree 찾기
                    if task_id in wt.name:
                        return str(project_dir)

    raise ValueError(f"Cannot resolve project path for {task_id}")
```

#### Step 6: 머지 실행 (worktree_manager.py 활용)

```python
def execute_merge(self, project_path: str, task_id: str, team_id: str) -> dict:
    """
    worktree_manager.py finish --action merge 실행.
    """
    import subprocess

    cmd = [
        "python3", "scripts/worktree_manager.py", "finish",
        project_path, task_id, team_id, "--action", "merge"
    ]

    try:
        result = subprocess.run(
            cmd,
            cwd=str(self.workspace),
            capture_output=True,
            text=True,
            timeout=60
        )

        if result.returncode != 0:
            # 머지 실패 (충돌 등)
            return {
                "status": "error",
                "error_type": "merge_conflict" if "conflict" in result.stderr.lower() else "merge_error",
                "message": result.stderr,
                "stdout": result.stdout
            }

        # 머지 성공
        merge_result = json.loads(result.stdout)
        return {
            "status": "success",
            "branch": merge_result.get("branch"),
            "message": "Merge completed successfully"
        }

    except subprocess.TimeoutExpired:
        return {
            "status": "error",
            "error_type": "timeout",
            "message": "Merge execution timed out (60s)"
        }
    except Exception as e:
        return {
            "status": "error",
            "error_type": "execution_error",
            "message": str(e)
        }
```

#### Step 7: 테스트 실행

```python
def run_tests(self, project_path: str) -> dict:
    """
    프로젝트 테스트 실행. 타임아웃 300초.
    """
    import subprocess

    project = Path(project_path)

    # pytest 확인
    if (project / "pytest.ini").exists() or (project / "tests").exists():
        cmd = ["pytest", "-v", "--tb=short"]
    # npm test 확인
    elif (project / "package.json").exists():
        cmd = ["npm", "test"]
    else:
        # 테스트 없음
        return {"status": "skipped", "message": "No test framework detected"}

    try:
        result = subprocess.run(
            cmd,
            cwd=str(project),
            capture_output=True,
            text=True,
            timeout=300  # 5분
        )

        if result.returncode == 0:
            return {
                "status": "passed",
                "output": result.stdout,
                "duration_seconds": 0  # 실제로는 시간 측정 필요
            }
        else:
            return {
                "status": "failed",
                "output": result.stderr or result.stdout,
                "message": "Test suite failed"
            }

    except subprocess.TimeoutExpired:
        return {
            "status": "failed",
            "error_type": "timeout",
            "message": "Test execution timed out (300s)"
        }
```

#### Step 8: 머지 Revert (테스트 실패 시)

```python
def revert_merge(self, project_path: str) -> bool:
    """
    테스트 실패 시 머지 되돌리기.
    git reset --hard HEAD~1
    """
    import subprocess

    try:
        subprocess.run(
            ["git", "reset", "--hard", "HEAD~1"],
            cwd=str(project_path),
            check=True,
            capture_output=True,
            text=True,
            timeout=30
        )
        return True
    except Exception as e:
        # Revert 실패시 에스컬레이션 (매우 위험)
        self.escalate(
            task_id="CRITICAL",
            reason=f"Merge revert failed: {str(e)}",
            context={"project_path": project_path}
        )
        return False
```

#### Step 9: 에스컬레이션 (아누에게 통보)

```python
def escalate(self, task_id: str, reason: str, context: dict = None) -> None:
    """
    아누에게 에스컬레이션 통보.
    cokacdir --cron으로 발송.
    """
    import subprocess
    import os

    chat_id = os.environ.get("COKACDIR_CHAT_ID", "6937032012")
    key = os.environ.get("COKACDIR_KEY_ANU", "")

    if not key:
        # .env.keys에서 로드
        env_keys = Path(self.workspace) / ".env.keys"
        if env_keys.exists():
            for line in env_keys.read_text().splitlines():
                if "COKACDIR_KEY_ANU=" in line:
                    key = line.split("=")[1].strip()
                    break

    message = f"🚨 자동 머지 실패\n"
    message += f"Task: {task_id}\n"
    message += f"사유: {reason}\n"
    if context:
        message += f"상세: {json.dumps(context, ensure_ascii=False, indent=2)}"

    cmd = [
        "cokacdir", "--cron", message,
        "--at", "1m",
        "--chat", chat_id,
        "--key", key
    ]

    try:
        result = subprocess.run(cmd, capture_output=True, text=True)
        if result.returncode != 0:
            print(f"Escalation send failed: {result.stderr}")
    except Exception as e:
        print(f"Escalation error: {e}")
```

#### Step 10: 로깅 및 파일 정리

```python
def log_result(self, task_id: str, result: dict) -> None:
    """
    merge-log.json에 결과 기록.
    """
    if not self.merge_log.exists():
        self.merge_log.write_text(json.dumps({"entries": []}, ensure_ascii=False, indent=2))

    log_data = json.loads(self.merge_log.read_text())

    entry = {
        "task_id": task_id,
        "timestamp": datetime.now(timezone.utc).isoformat(),
        "status": result.get("status", "unknown"),
        "action": result.get("action"),
        "merge_needed": result.get("merge_needed"),
        "project": result.get("project"),
        "branch": result.get("branch"),
        "test_result": result.get("test_result"),
        "error_reason": result.get("error_reason") if result.get("status") == "error" else None,
        "duration_seconds": result.get("duration_seconds")
    }

    log_data["entries"].append(entry)
    self.merge_log.write_text(json.dumps(log_data, ensure_ascii=False, indent=2))

def cleanup_done_file(self, done_file: Path) -> None:
    """
    처리 완료 후 .done.clear 생성 (이미 생성됨).
    .done 파일 삭제.
    """
    try:
        done_file.unlink()
    except FileNotFoundError:
        pass
```

---

## 4. 에스컬레이션 규칙

### 4.1 자동 처리 조건

| 조건 | 처리 | 완료 상태 |
|------|------|---------|
| merge_needed = False | 로깅만 → .done.clear 생성 | success |
| merge_needed = True & 머지 성공 & 테스트 통과 | 자동 머지 → 로깅 | success |
| merge_needed = True & 워크트리 없음 | 에스컬레이션 | escalated |

### 4.2 에스컬레이션 조건

| 상황 | 원인 | 처리 |
|------|------|------|
| 머지 충돌 | git merge conflict | 자동 abort → revert 시도 → escalate |
| 테스트 실패 | pytest/npm test 실패 | 자동 revert → escalate |
| 테스트 타임아웃 | 300초 초과 | 자동 cancel → revert → escalate |
| 프로젝트 경로 미발견 | 보고서/task/worktree에서 못 찾음 | escalate |
| JSON 파싱 실패 | .done 형식 오류 | escalate |
| worktree 없음 | merge_needed=True인데 워크트리 미존재 | escalate |

### 4.3 에스컬레이션 통보

```bash
cokacdir --cron "🚨 자동 머지 실패
Task: task-391.1
사유: merge_conflict
프로젝트: /home/jay/projects/ThreadAuto
브랜치: task/task-391.1-dev2
상세: (상세 에러 메시지)
→ 수동으로 /home/jay/workspace/memory/reports/task-391.1.md 확인 후 처리 필요" \
  --at "1m" --chat 6937032012 --key {COKACDIR_KEY_ANU}
```

---

## 5. 안전장치

### 5.1 원자적 선점

```python
# .done.clear를 x 플래그로 생성 → 다른 프로세스와 충돌 방지
try:
    with open(clear_file, 'x') as f:
        pass
except FileExistsError:
    # 이미 처리 중이므로 스킵
    return False
```

**효과**: 여러 cron 인스턴스가 같은 .done을 동시에 처리하는 것 방지

### 5.2 자동 Revert (테스트 실패 시)

```python
# 머지는 성공했지만 테스트 실패
if test_result["status"] == "failed":
    # git reset --hard HEAD~1로 자동 되돌리기
    revert_success = self.revert_merge(project_path)
    if revert_success:
        # escalate with context
        self.escalate(task_id, "test_failure", ...)
```

**효과**: 불안정한 코드가 main 브랜치에 머지되는 것 방지

### 5.3 타임아웃 보호

```python
# 테스트 실행 타임아웃: 300초
result = subprocess.run(..., timeout=300)

# 머지 실행 타임아웃: 60초
result = subprocess.run(..., timeout=60)
```

**효과**: 무한 대기 상태 방지

### 5.4 드라이런 모드

```bash
# --dry-run: 실제 동작 없이 분석만 수행
python3 scripts/auto_merge.py --dry-run
```

**효과**: 프로덕션 배포 전 로직 검증

### 5.5 로깅 및 감사

- `merge-log.json`: 모든 머지 기록 (성공/실패/에스컬레이션)
- 일별 로그: `/home/jay/workspace/logs/auto_merge_YYYY-MM-DD.log`
- 모든 에스컬레이션 사유 기록

---

## 6. 모니터링

### 6.1 merge-log.json 형식

```json
{
  "entries": [
    {
      "task_id": "task-391.1",
      "timestamp": "2026-03-07T15:30:00+09:00",
      "status": "success",
      "action": "auto_merged",
      "merge_needed": true,
      "project": "/home/jay/projects/ThreadAuto",
      "branch": "task/task-391.1-dev2",
      "test_result": "passed",
      "error_reason": null,
      "duration_seconds": 12.5
    },
    {
      "task_id": "task-392.1",
      "timestamp": "2026-03-07T15:35:00+09:00",
      "status": "escalated",
      "action": "escalated",
      "merge_needed": true,
      "project": "/home/jay/projects/ThreadAuto",
      "branch": "task/task-392.1-dev2",
      "test_result": null,
      "error_reason": "merge_conflict",
      "duration_seconds": 5.2
    },
    {
      "task_id": "task-393.1",
      "timestamp": "2026-03-07T15:40:00+09:00",
      "status": "success",
      "action": "logged_no_merge",
      "merge_needed": false,
      "project": null,
      "branch": null,
      "test_result": null,
      "error_reason": null,
      "duration_seconds": 0.5
    }
  ]
}
```

### 6.2 로그 파일 관리

- 경로: `/home/jay/workspace/logs/auto_merge_YYYY-MM-DD.log`
- 로테이션: 일별 자동 (cron 실행 시 자동)
- 형식: 각 .done 처리마다 한 줄 로그

```
[2026-03-07 15:30:00] ✅ task-391.1 merged (12.5s)
[2026-03-07 15:35:00] ⚠️ task-392.1 escalated: merge_conflict
[2026-03-07 15:40:00] ✓ task-393.1 logged (no merge)
```

### 6.3 일일 요약 보고

cron 실행 후 아누에게 요약 통보:

```bash
# auto_merge.py 완료 후
python3 scripts/notify-completion.py --type merge-summary
```

**내용**:
```
📊 자동 머지 일일 통계 (2026-03-07)
- 처리: 10건
  ├ 자동 머지: 8건 ✅
  ├ 머지 불필요: 1건 ✓
  └ 에스컬레이션: 1건 ⚠️
- 에러: 1건
  └ merge_conflict (task-392.1)
- 평균 처리 시간: 8.2초
```

---

## 7. 기존 모듈 연동

### 7.1 report_parser.py 활용

```python
from report_parser import parse_report

report_path = "/home/jay/workspace/memory/reports/task-391.1.md"
result = parse_report(report_path)

# 반환값
{
    "task_id": "task-391.1",
    "team": "dev2",
    "summary": "...",
    "test_total": 23,
    "test_passed": 23,
    "test_failed": 0,
    "bug_count": 0,
    "duration": "3분 12초",
    "files": ["/home/jay/projects/..."],
    "merge_needed": True,           # ← 자동 감지됨
    "merge_branch": "task/task-391.1-dev2",
    "merge_worktree": "/home/jay/projects/.../",
    "confidence": 0.95              # ← 신뢰도
}
```

### 7.2 worktree_manager.py 활용

```bash
# 머지 실행
python3 scripts/worktree_manager.py finish \
  /home/jay/projects/ThreadAuto \
  task-391.1 \
  dev2 \
  --action merge

# 응답
{
  "status": "merged",
  "branch": "task/task-391.1-dev2"
}
```

머지 실패 시:
```json
{
  "status": "error",
  "message": "Merge conflict detected while merging 'task/...' into 'main'"
}
```

### 7.3 notify-completion.py와의 관계

- **용도**: 팀장 → 아누 완료 통보 (Step 7)
- **auto_merge.py**: Step 4 자동 머지 담당
- **분리 이유**: 통보와 머지는 독립적 프로세스

---

## 8. CLI 인터페이스

### 8.1 기본 사용법

```bash
# 자동 실행 (모든 미처리 .done 파일)
python3 scripts/auto_merge.py

# 특정 task만 처리
python3 scripts/auto_merge.py --task-id task-391.1

# 드라이런 (실제 머지 없이 분석만)
python3 scripts/auto_merge.py --dry-run

# 특정 task 드라이런
python3 scripts/auto_merge.py --task-id task-391.1 --dry-run

# 에스컬레이션된 task 강제 재처리 (아누 지시 시)
python3 scripts/auto_merge.py --force-merge task-391.1

# 상세 로그 출력
python3 scripts/auto_merge.py -v
```

### 8.2 명령어 옵션

| 옵션 | 설명 | 기본값 |
|------|------|-------|
| `--task-id` | 특정 task만 처리 | 모든 .done |
| `--dry-run` | 실제 동작 없이 분석만 | False |
| `--force-merge` | 에스컬레이션된 task 재시도 | - |
| `--workspace` | workspace 경로 | /home/jay/workspace |
| `--log-level` | DEBUG/INFO/WARN/ERROR | INFO |
| `-v, --verbose` | 상세 로그 출력 | False |
| `--no-escalate` | 에스컬레이션 실행 안 함 (테스트용) | False |

### 8.3 종료 코드

| 코드 | 의미 |
|------|------|
| 0 | 모든 처리 완료 |
| 1 | 처리 중 에러 발생 |
| 2 | 명령어 인자 오류 |

---

## 9. 데이터 형식

### 9.1 .done 파일 JSON 스키마

```json
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": ["status", "task_id", "timestamp"],
  "properties": {
    "status": {
      "type": "string",
      "enum": ["completed", "done"],
      "description": "작업 상태"
    },
    "task_id": {
      "type": "string",
      "pattern": "^task-\\d+(\\.\\d+)?$",
      "description": "작업 ID"
    },
    "team_id": {
      "type": "string",
      "pattern": "^dev\\d+$",
      "description": "팀 ID"
    },
    "timestamp": {
      "type": "string",
      "format": "date-time",
      "description": "ISO8601 타임스탐프"
    },
    "merge_needed": {
      "type": "boolean",
      "default": false,
      "description": "머지 필요 여부"
    },
    "merge_branch": {
      "type": ["string", "null"],
      "pattern": "^task/task-\\d+(\\.\\d+)?-dev\\d+$",
      "description": "머지 대상 브랜치 (merge_needed=true일 때만)"
    },
    "summary": {
      "type": "string",
      "description": "작업 요약"
    }
  }
}
```

### 9.2 merge-log.json 스키마

```json
{
  "$schema": "http://json-schema.org/draft-07/schema#",
  "type": "object",
  "required": ["entries"],
  "properties": {
    "entries": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["task_id", "timestamp", "status"],
        "properties": {
          "task_id": {
            "type": "string",
            "description": "작업 ID"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time",
            "description": "처리 시간"
          },
          "status": {
            "type": "string",
            "enum": ["success", "escalated", "error", "skipped"],
            "description": "처리 결과"
          },
          "action": {
            "type": "string",
            "enum": ["auto_merged", "logged_no_merge", "escalated"],
            "description": "수행한 액션"
          },
          "merge_needed": {
            "type": "boolean",
            "description": "머지 필요 여부"
          },
          "project": {
            "type": ["string", "null"],
            "description": "프로젝트 경로"
          },
          "branch": {
            "type": ["string", "null"],
            "description": "머지 브랜치"
          },
          "test_result": {
            "type": ["string", "null"],
            "enum": ["passed", "failed", "skipped", "timeout", null],
            "description": "테스트 결과"
          },
          "error_reason": {
            "type": ["string", "null"],
            "enum": [
              "merge_conflict",
              "test_failure",
              "timeout",
              "project_not_found",
              "json_parse_error",
              "worktree_not_found",
              null
            ],
            "description": "에러 사유"
          },
          "duration_seconds": {
            "type": ["number", "null"],
            "description": "처리 소요 시간"
          }
        }
      }
    }
  }
}
```

### 9.3 .env.keys 환경변수

```bash
# /home/jay/workspace/.env.keys

# 자동 머지 시스템
COKACDIR_CHAT_ID=6937032012        # 제이회장님 chat ID
COKACDIR_KEY_ANU=c119085addb0f8b7  # 아누 에이전트 키

# 에스컬레이션 자동 발송용
AUTO_MERGE_ENABLED=true
AUTO_MERGE_TIMEOUT=300              # 테스트 타임아웃 (초)
AUTO_MERGE_MERGE_TIMEOUT=60         # 머지 타임아웃 (초)
```

---

## 10. Cron 등록

### 10.1 cron 설정

```bash
# crontab -e
# 1분마다 auto_merge.py 실행
* * * * * cd /home/jay/workspace && /usr/bin/python3 scripts/auto_merge.py >> /home/jay/workspace/logs/auto_merge.log 2>&1

# 매일 자정에 일일 요약 보고
0 0 * * * cd /home/jay/workspace && python3 scripts/notify-completion.py --type merge-daily-summary
```

### 10.2 로그 로테이션 (logrotate)

```bash
# /etc/logrotate.d/auto-merge
/home/jay/workspace/logs/auto_merge.log {
    daily
    rotate 30
    compress
    delaycompress
    missingok
    notifempty
    create 0644 jay jay
}
```

---

## 11. 예시 시나리오

### 시나리오 1: 정상 자동 머지

```
1. 팀장(dev2) worktree에서 작업 완료 → task-391.1.md 보고서 작성
   - "merge 판단은 아누에게 위임"
   - 브랜치: task/task-391.1-dev2

2. 팀장 → .done 파일 생성
   {
     "task_id": "task-391.1",
     "team_id": "dev2",
     "merge_needed": true,
     "merge_branch": "task/task-391.1-dev2"
   }

3. [자동화] cron으로 auto_merge.py 실행
   - .done 파일 감지
   - .done.clear 원자적 생성 (선점)
   - report_parser로 분석 (merge_needed=true 확인)
   - 프로젝트 경로 추출: /home/jay/projects/ThreadAuto
   - worktree_manager로 머지 실행
   - pytest 실행 → 통과

4. [결과]
   - merge-log.json 기록:
     {
       "task_id": "task-391.1",
       "status": "success",
       "action": "auto_merged",
       "test_result": "passed"
     }
   - .done 파일 삭제, .done.clear 유지
   - 완료 로그: "✅ task-391.1 merged (12.5s)"
```

### 시나리오 2: 머지 충돌 에스컬레이션

```
1. 팀장(dev2) 작업 완료 → .done 파일 생성
   merge_branch: task/task-391.2-dev2

2. [자동화] cron 실행
   - worktree_manager로 머지 시도
   - 충돌 감지 → git merge --abort 실행

3. [에스컬레이션]
   - merge-log.json: status="escalated", error_reason="merge_conflict"
   - cokacdir 발송: "🚨 자동 머지 실패: task-391.2 - merge_conflict"
   - .done.clear 생성 (처리 완료 표시)

4. [아누 수동 처리]
   - 보고서 확인
   - git merge 수동 해결
   - git push
```

### 시나리오 3: 머지 불필요

```
1. 팀장(dev1) 문서 수정 → .done 파일
   merge_needed: false

2. [자동화]
   - report_parser 분석: merge_needed=false 확인
   - 머지 스킵

3. [결과]
   - merge-log.json: action="logged_no_merge", status="success"
   - 로그: "✓ task-393.1 logged (no merge)"
```

---

## 12. 주의사항

### 12.1 환경 변수 관리

```bash
# .env.keys는 절대 public repo에 커밋하지 말 것
.env.keys          # git ignored
.env.keys.example  # 예시만 저장
```

### 12.2 report_parser, worktree_manager 수정 금지

- 기존 모듈은 **그대로 사용**
- auto_merge.py에서만 호출
- 수정 필요시 별도 이슈 등록

### 12.3 프로젝트별 테스트 명령어

```python
# 자동 감지 로직
def detect_test_command(project_path: str) -> list:
    """프로젝트의 테스트 명령어 자동 감지"""
    p = Path(project_path)

    # pytest
    if (p / "pytest.ini").exists() or (p / "tests").exists():
        return ["pytest", "-v"]

    # npm test
    if (p / "package.json").exists():
        return ["npm", "test"]

    # go test
    if any(p.glob("*.go")):
        return ["go", "test", "./..."]

    # 기본값
    return []
```

### 12.4 동시성 주의

- cron이 1분마다 실행되면서 동시 실행 가능
- `.done.clear` 원자적 생성으로 충돌 방지
- 같은 task_id에 대해서는 mutex 없이도 안전 (첫 인스턴스만 .done.clear 생성)

---

## 13. 향후 확장

### 13.1 자동 머지 권한 확대

현재: 머지만 자동 실행
향후: 테스트 통과 시 main 자동 푸시 가능

### 13.2 Slack/이메일 알림

```python
def notify_escalation(task_id: str, reason: str):
    # cokacdir 외 Slack/이메일도 함께 발송
    send_slack_message(...)
    send_email_notification(...)
```

### 13.3 대시보드 연동

`/home/jay/workspace/dashboard/` 에 머지 상태 JSON 제공

---

## 참고 문서

- 머지 감지 자동화: `/home/jay/workspace/memory/specs/merge-detection-spec.md`
- 팀장 워크플로우: `/home/jay/workspace/prompts/DIRECT-WORKFLOW.md`
- 작업 상세: `/home/jay/workspace/memory/tasks/task-392.1.md`

---

**문서 버전:** 1.0
**작성일:** 2026-03-07
**작성자:** 미미르(UX/UI 담당)
**상태:** 초안 완성
**승인:** 보류 (팀장 검토 대기)
