#!/bin/bash
# 사용법: finish-task.sh <task_id> [team_short] [project_path]
# L1 마무리 스크립트: QC → 머지 → .done 생성 → task-timer end → notify-completion
set -euo pipefail
TASK_ID="$1"
TEAM_SHORT="${2:-""}"
PROJECT_PATH="${3:-""}"
WORKSPACE="/home/jay/workspace"
EVENTS_DIR="$WORKSPACE/memory/events"
DONE_FILE="$EVENTS_DIR/${TASK_ID}.done"

# ERR/EXIT trap: 비정상 종료 시에도 timer end 호출
_timer_ended=0
cleanup_timer() {
    if [ "$_timer_ended" -eq 0 ]; then
        _timer_ended=1
        # QC FAIL 종료 시에는 timer end를 호출하지 않음 (재시도 가능하므로)
        # .done이 생성된 후 오류 발생 시에만 timer end 호출
        if [ -f "$DONE_FILE" ]; then
            python3 "$WORKSPACE/memory/task-timer.py" end "$TASK_ID" 2>&1 || true
        fi
    fi
}
trap cleanup_timer EXIT

# ── task-2569 RC-3: stash lifecycle audit (회장 박제 사건 2026-05-13) ──
_STASH_AUDIT_BEFORE=$(git stash list 2>/dev/null | wc -l)
if [ "$_STASH_AUDIT_BEFORE" -gt 5 ]; then
    echo "[WARN][task-2569] stash 누적 감지 ($_STASH_AUDIT_BEFORE개) — task-2568 박제 사건 재발 가능. git stash list 확인 권장." >&2
fi
# audit log 박제 (JSONL)
mkdir -p "$WORKSPACE/memory/logs"
python3 - "$_STASH_AUDIT_BEFORE" "${TASK_ID:-unknown}" <<'PYEOF' >> "$WORKSPACE/memory/logs/cleanup-audit.jsonl" 2>/dev/null || true
import json, sys, time
print(json.dumps({"ts": time.time(), "audit": "finish-task-start", "task_id": sys.argv[2], "stash_count": int(sys.argv[1])}, ensure_ascii=False))
PYEOF

# 0-cancelled. .cancelled 마커 감지 시 .done 생성 차단 (task-2352)
CANCELLED_FILE="$EVENTS_DIR/${TASK_ID}.cancelled"
if [ -f "$CANCELLED_FILE" ]; then
    echo "[CANCELLED] task $TASK_ID는 취소된 작업입니다 (.cancelled 마커 발견). .done 생성 차단."
    # task-timer end는 호출 (cancelled 상태 마킹)
    python3 "$WORKSPACE/memory/task-timer.py" end "$TASK_ID" --qc-result CANCELLED 2>&1 || true
    _timer_ended=1
    exit 0
fi

# 0-stopmarker. task 파일 상단 STOP 마커 가드 (task-2352)
TASK_FILE_PRECHECK="$WORKSPACE/memory/tasks/${TASK_ID}.md"
if [ -f "$TASK_FILE_PRECHECK" ]; then
    HEAD3=$(head -3 "$TASK_FILE_PRECHECK" 2>/dev/null || true)
    if echo "$HEAD3" | grep -qE "CANCELLED|작업 취소됨"; then
        echo "[CANCELLED] task 파일 상단 STOP 마커 감지. .cancelled 생성 후 종료."
        : > "$CANCELLED_FILE" || true
        python3 -c "import json,sys; from datetime import datetime; json.dump({'task_id':'$TASK_ID','cancelled_at':datetime.now().isoformat(),'reason':'task-file STOP marker (auto)'}, open('$CANCELLED_FILE','w'))" 2>/dev/null || true
        python3 "$WORKSPACE/memory/task-timer.py" end "$TASK_ID" --qc-result CANCELLED 2>&1 || true
        _timer_ended=1
        exit 0
    fi
fi

# team_short 미지정 시 task-timers.json에서 자동 추출
if [ -z "$TEAM_SHORT" ]; then
    TEAM_SHORT=$(python3 -c "
import json
try:
    with open('$WORKSPACE/memory/task-timers.json') as f:
        data = json.load(f)
    tasks = data.get('tasks', data)
    t = tasks.get('$TASK_ID', {})
    print(t.get('team', ''))
except Exception:
    print('')
" 2>/dev/null || echo "")
fi

# PROJECT_PATH 워크트리 자동 인식 (task-2348)
# PROJECT_PATH가 비어있거나 WORKSPACE 자체인 경우, task-timers.json의 worktree_path로 자동 보정
if [ -z "$PROJECT_PATH" ] || [ "$PROJECT_PATH" = "$WORKSPACE" ]; then
    _AUTO_PROJECT_PATH=$(python3 -c "
import json, os

workspace = '$WORKSPACE'
task_id = '$TASK_ID'

# 1차: task-timers.json worktree_path
try:
    with open(f'{workspace}/memory/task-timers.json') as f:
        data = json.load(f)
    tasks = data.get('tasks', data)
    t = tasks.get(task_id, {})
    wt = (t.get('worktree_path', '') or '').strip()
    if wt and os.path.isdir(wt) and wt != workspace:
        print(wt)
        exit(0)
except Exception:
    pass

# 2차: task 파일에서 worktree 경로 파싱 (## worktree: /path 형식)
import re
task_file = f'{workspace}/memory/tasks/{task_id}.md'
try:
    with open(task_file, encoding='utf-8') as f:
        content = f.read()
    m = re.search(r'##\s*worktree\s*:\s*(\S+)', content, re.IGNORECASE)
    if m:
        wt = m.group(1).strip()
        if os.path.isdir(wt) and wt != workspace:
            print(wt)
            exit(0)
except Exception:
    pass

print('')
" 2>/dev/null || echo "")
    if [ -n "$_AUTO_PROJECT_PATH" ]; then
        echo "[INFO] PROJECT_PATH 자동 인식: $_AUTO_PROJECT_PATH (task-timers.json worktree_path)"
        PROJECT_PATH="$_AUTO_PROJECT_PATH"
    fi
    unset _AUTO_PROJECT_PATH
fi

# 0-pre. 보고서 세션 통계 idempotent 정리 (task-2348)
REPORT_FILE="$WORKSPACE/memory/reports/${TASK_ID}.md"
if [ -f "$REPORT_FILE" ]; then
    python3 - "$REPORT_FILE" <<'PYEOF'
import sys, re

path = sys.argv[1]
with open(path, encoding='utf-8') as f:
    content = f.read()

# "## 세션 통계" 섹션 모두 찾기 (각 섹션은 다음 ## 헤더 또는 파일 끝까지)
pattern = re.compile(r'^(## 세션 통계\b.*?)(?=^## |\Z)', re.MULTILINE | re.DOTALL)
matches = list(pattern.finditer(content))

if len(matches) < 2:
    # 0개 또는 1개면 정리 불필요
    sys.exit(0)

# 마지막 1개만 남기고 앞의 것들 모두 제거 (역순으로 삭제해 offset 오염 방지)
for m in reversed(matches[:-1]):
    content = content[:m.start()] + content[m.end():]

with open(path, 'w', encoding='utf-8') as f:
    f.write(content)
print(f'[INFO] 세션 통계 섹션 정리 완료 (제거 {len(matches)-1}개, 유지 1개)')
PYEOF
fi

# 0. 노하우 업데이트 검증 (디자인/마케팅 작업만)
TASK_FILE="$WORKSPACE/memory/tasks/${TASK_ID}.md"

# IS_DESIGN_TASK 판별 (task-2348: false positive 방어)
# 다음 조건 중 하나 이상 충족 시에만 디자인 작업으로 판정:
#  (1) affected_files 섹션에 이미지/에셋 파일 경로 패턴 존재
#  (2) 명시적 "## 디자인 작업: yes" 또는 YAML "design_task: true" 플래그
#  (3) 기존 키워드 매칭 AND task 제목(첫 줄 # 헤더)에도 디자인 키워드 포함
IS_DESIGN_TASK=0
if [ -f "$TASK_FILE" ]; then
    IS_DESIGN_TASK=$(python3 - "$TASK_FILE" <<'IS_DESIGN_PYEOF'
import sys, re

path = sys.argv[1]
try:
    with open(path, encoding='utf-8') as f:
        content = f.read()
except Exception:
    print(0)
    sys.exit(0)

# (1) affected_files 섹션에 이미지/에셋 경로 패턴
affected_section = re.search(r'## affected_files\b.*?(?=\n## |\Z)', content, re.DOTALL | re.IGNORECASE)
if affected_section:
    asset_pattern = re.compile(
        r'\.(png|jpg|jpeg|webp|gif|svg)\b|/assets/|/images/|/public/.*\.(png|jpg|svg)',
        re.IGNORECASE
    )
    if asset_pattern.search(affected_section.group()):
        print(1)
        sys.exit(0)

# (2) 명시적 디자인 작업 플래그
if re.search(r'^##\s*디자인 작업\s*:\s*yes', content, re.MULTILINE | re.IGNORECASE):
    print(1)
    sys.exit(0)
if re.search(r'^design_task\s*:\s*true', content, re.MULTILINE | re.IGNORECASE):
    print(1)
    sys.exit(0)

# (3) 기존 키워드 매칭 AND 제목(첫 # 헤더)에도 디자인 키워드 포함
keyword_pattern = re.compile(r'디자인|배너|이미지|banner|image|광고|마케팅|카피|copywriting', re.IGNORECASE)
if keyword_pattern.search(content):
    title_match = re.search(r'^#\s+(.+)', content, re.MULTILINE)
    if title_match:
        title = title_match.group(1)
        design_title_pattern = re.compile(r'디자인|배너|이미지|banner|image|광고|마케팅|카피', re.IGNORECASE)
        if design_title_pattern.search(title):
            print(1)
            sys.exit(0)

print(0)
IS_DESIGN_PYEOF
    2>/dev/null || echo "0")
fi

if [ -f "$TASK_FILE" ]; then
    # IS_DESIGN_TASK 기반 디자인/마케팅 작업 판별 (강화된 조건)
    if [ "${IS_DESIGN_TASK}" = "1" ]; then
        # task-timer에서 start_time 가져오기
        TASK_START=$(python3 -c "
import json
with open('$WORKSPACE/memory/task-timers.json') as f:
    data = json.load(f)
tasks = data.get('tasks', data)
t = tasks.get('$TASK_ID', {})
print(t.get('start_time', ''))
" 2>/dev/null || echo "")

        if [ -n "$TASK_START" ]; then
            # 노하우 파일들의 수정 시간이 작업 시작 이후인지 확인
            KNOWHOW_CHECK=$(python3 -c "
import os, sys
from datetime import datetime

start_str = '$TASK_START'
start_time = datetime.fromisoformat(start_str)

knowhow_files = [
    '$WORKSPACE/memory/specs/knowhow-design.md',
    '$WORKSPACE/memory/specs/design-qc-knowhow.md',
    '$WORKSPACE/memory/specs/knowhow-marketing.md',
]

updated = []
for f in knowhow_files:
    if os.path.exists(f):
        mtime = datetime.fromtimestamp(os.path.getmtime(f))
        if mtime > start_time:
            updated.append(os.path.basename(f))

if not updated:
    print('FAIL')
    print('노하우 파일이 작업 시작 이후 업데이트되지 않았습니다.', file=sys.stderr)
    print(f'작업 시작: {start_str}', file=sys.stderr)
    for f in knowhow_files:
        if os.path.exists(f):
            mtime = datetime.fromtimestamp(os.path.getmtime(f)).isoformat()
            print(f'  {os.path.basename(f)}: {mtime}', file=sys.stderr)
else:
    print('PASS')
" 2>/dev/null)

            if [ "$KNOWHOW_CHECK" = "FAIL" ]; then
                echo "[ERROR] ❌ 노하우 파일 업데이트 미확인. .done 생성 차단."
                echo "[ERROR] 디자인/마케팅 QC 후에는 반드시 노하우 파일을 업데이트해야 합니다."
                echo "[ERROR] 대상: knowhow-design.md, design-qc-knowhow.md, knowhow-marketing.md 중 최소 1개"
                echo "[HINT] --skip-knowhow-check 환경변수로 우회 가능: SKIP_KNOWHOW_CHECK=1 bash finish-task.sh $TASK_ID"
                if [ "${SKIP_KNOWHOW_CHECK:-0}" != "1" ]; then
                    exit 1
                fi
                echo "[WARN] SKIP_KNOWHOW_CHECK=1로 우회합니다."
            fi
        fi
    fi
fi

# 0b. QC 프로세스 검증 (디자인/이미지 작업만 — IS_DESIGN_TASK 사용)
if [ -f "$TASK_FILE" ]; then
    if [ "${IS_DESIGN_TASK}" = "1" ]; then
        if [ ! -f "$REPORT_FILE" ]; then
            echo "[WARN] ⚠️ 보고서 파일 없음: $REPORT_FILE — QC 프로세스 검증을 건너뜁니다."
        else
            # 검증 2: 로키 소환 검증
            if [ "${SKIP_LOKI_CHECK:-0}" != "1" ]; then
                LOKI_CHECK=$(python3 -c "
import re, sys
report_path = '$REPORT_FILE'
try:
    with open(report_path) as f:
        content = f.read()
    model_section = re.search(r'## 모델 사용 기록.*?(?=\n## |\Z)', content, re.DOTALL)
    if model_section:
        section_text = model_section.group()
        has_loki_opus = bool(re.search(r'로키|Loki', section_text)) and bool(re.search(r'opus', section_text))
        if not has_loki_opus:
            print('FAIL')
        else:
            print('PASS')
    else:
        print('FAIL')
except Exception as e:
    print('FAIL', file=sys.stderr)
    print('FAIL')
" 2>/dev/null || echo "FAIL")
                if [ "$LOKI_CHECK" = "FAIL" ]; then
                    echo "[ERROR] ❌ 디자인 QC에 로키(opus) 참여 기록 없음."
                    echo "[ERROR] 디자인 QC는 로키(opus) 단독 필수. 팀장 자체 평가는 규칙 위반."
                    exit 1
                fi
            else
                echo "[WARN] ⚠️ SKIP_LOKI_CHECK=1 — 로키 소환 체크를 우회합니다."
            fi

            # 검증 3: "팀장 시각 검수" 패턴 차단 (WARNING만)
            if grep -qiE "팀장 시각 검수|팀장 검수|팀장 판단|자체 평가" "$REPORT_FILE" 2>/dev/null; then
                echo "[WARN] ⚠️ \"팀장 시각 검수\" 패턴 감지. 디자인 QC는 로키(opus)만 수행 가능."
            fi
        fi
    fi
fi

# 1. QC 실행 (멱등성: .qc-done 이미 있으면 스킵)
QC_RESULT_FILE="$EVENTS_DIR/${TASK_ID}.qc-result"
QC_DONE_FILE="$EVENTS_DIR/${TASK_ID}.qc-done"

if [ -f "$QC_DONE_FILE" ]; then
    echo "[INFO] .qc-done 이미 존재 — QC 단계 스킵."
else
    # 팀별 qc_verify.py 경로 결정
    if [ -n "$TEAM_SHORT" ] && [ -f "$WORKSPACE/teams/${TEAM_SHORT}/qc/qc_verify.py" ]; then
        QC_SCRIPT="$WORKSPACE/teams/${TEAM_SHORT}/qc/qc_verify.py"
    else
        QC_SCRIPT="$WORKSPACE/teams/shared/qc_verify.py"
    fi

    echo "[INFO] QC 실행: $QC_SCRIPT --gate --task-id $TASK_ID"
    if [ -n "$TEAM_SHORT" ]; then
        python3 "$QC_SCRIPT" --gate --task-id "$TASK_ID" --team "$TEAM_SHORT" 2>&1 || true
    else
        python3 "$QC_SCRIPT" --gate --task-id "$TASK_ID" 2>&1 || true
    fi

    # QC 결과 확인
    if [ ! -f "$QC_RESULT_FILE" ]; then
        echo "[ERROR] QC 실행 후 .qc-result 파일이 생성되지 않았습니다: $QC_RESULT_FILE"
        exit 1
    fi

    QC_STATUS=$(python3 -c "
import json, sys
try:
    with open('$QC_RESULT_FILE') as f:
        data = json.load(f)
    print(data.get('qc_result', 'FAIL'))
except Exception:
    print('FAIL')
" 2>/dev/null || echo "FAIL")

    if [ "$QC_STATUS" = "FAIL" ]; then
        # QC FAIL 시 .failed 이벤트 생성
        FAILED_FILE="$EVENTS_DIR/${TASK_ID}.failed"
        echo '{"task_id":"'"$TASK_ID"'","team":"'"$TEAM_SHORT"'","fail_reason":"QC FAIL","timestamp":"'"$(date -Iseconds)"'"}' > "$FAILED_FILE"
        echo "[FAIL] QC FAIL — .failed 생성: $FAILED_FILE"
        exit 1
    fi

    # QC 완료 표시
    echo '{"task_id":"'"$TASK_ID"'","qc_result":"'"$QC_STATUS"'","timestamp":"'"$(date -Iseconds)"'"}' > "$QC_DONE_FILE"
    echo "[INFO] QC $QC_STATUS — .qc-done 생성: $QC_DONE_FILE"
fi

# QC 결과 읽기 (후속 단계용)
QC_STATUS=$(python3 -c "
import json, sys
try:
    with open('$QC_RESULT_FILE') as f:
        data = json.load(f)
    print(data.get('qc_result', 'PASS'))
except Exception:
    print('PASS')
" 2>/dev/null || echo "PASS")

# 1.6. Scope Guard 검증 (task-2364, 머지 직전 hard wall)
# non-code task 판별 (Step 2.5의 GIT_GATE_SKIP보다 먼저 자체 계산)
_SCOPE_GATE_SKIP=0
if [ -f "$TASK_FILE" ]; then
    _SCOPE_GATE_SKIP=$(python3 -c "
import re, sys
try:
    with open('$TASK_FILE', encoding='utf-8') as f:
        content = f.read()
    m = re.search(r'## 레벨\s*\n(.*?)(?=\n## |\Z)', content, re.DOTALL)
    if m:
        section = m.group(1)
        if re.search(r'코드 수정 없음|문서 업데이트만|문서만|리서치만', section):
            print('1')
        else:
            print('0')
    else:
        print('0')
except Exception:
    print('0')
" 2>/dev/null || echo "0")
fi

SCOPE_GUARD_DONE_FILE="$EVENTS_DIR/${TASK_ID}.scope-guard-done"
if [ -f "$SCOPE_GUARD_DONE_FILE" ]; then
    echo "[INFO] .scope-guard-done 이미 존재 — scope 검증 스킵."
elif [ "$_SCOPE_GATE_SKIP" -eq 1 ] 2>/dev/null; then
    echo "[SCOPE-GUARD] non-code task — scope 검증 스킵."
else
    # PROJECT_PATH 미지정 시 workspace 자체 사용 (시스템 task 보호 — Codex 지적 반영)
    SCOPE_PROJ_DIR="${PROJECT_PATH:-$WORKSPACE}"
    # workspace 자체가 git이 아닌 경우 SKIP
    if ! git -C "$SCOPE_PROJ_DIR" rev-parse --git-dir >/dev/null 2>&1; then
        echo "[SCOPE-GUARD] $SCOPE_PROJ_DIR not a git repo — 검증 스킵."
    else
        # diff 계산: worktree 브랜치 기준 main..HEAD
        SCOPE_DIFF_FILE="$EVENTS_DIR/${TASK_ID}.scope-diff.txt"
        # main 브랜치 이름 추정 (main 또는 master)
        MAIN_BRANCH=$(git -C "$SCOPE_PROJ_DIR" rev-parse --verify main >/dev/null 2>&1 && echo main || echo master)
        git -C "$SCOPE_PROJ_DIR" diff --name-only "${MAIN_BRANCH}..HEAD" > "$SCOPE_DIFF_FILE" 2>/dev/null \
            || git -C "$SCOPE_PROJ_DIR" diff --name-only HEAD~1 > "$SCOPE_DIFF_FILE" 2>/dev/null \
            || true

        if [ ! -s "$SCOPE_DIFF_FILE" ]; then
            echo "[SCOPE-GUARD] diff 비어있음 — 검증 스킵."
        else
            echo "[SCOPE-GUARD] task-scope-guard.sh 호출: $SCOPE_DIFF_FILE"
            set +e
            bash "$WORKSPACE/scripts/task-scope-guard.sh" "$TASK_ID" "$SCOPE_DIFF_FILE"
            SCOPE_EXIT=$?
            set -e
            if [ "$SCOPE_EXIT" -ne 0 ]; then
                echo "[SCOPE-GUARD] FAIL — 머지 차단 + .escalate 생성" >&2
                ESCALATE_FILE="$EVENTS_DIR/${TASK_ID}.escalate"
                echo '{"task_id":"'"$TASK_ID"'","reason":"scope_guard_violation","timestamp":"'"$(date -Iseconds)"'"}' > "$ESCALATE_FILE"
                exit 1
            fi
            echo '{"task_id":"'"$TASK_ID"'","timestamp":"'"$(date -Iseconds)"'"}' > "$SCOPE_GUARD_DONE_FILE"
            echo "[SCOPE-GUARD] PASS — .scope-guard-done 생성"
        fi
    fi
fi

# 2. 머지 (멱등성: .merge-done 이미 있으면 스킵, project_path 없으면 스킵)
MERGE_DONE_FILE="$EVENTS_DIR/${TASK_ID}.merge-done"

if [ -f "$MERGE_DONE_FILE" ]; then
    echo "[INFO] .merge-done 이미 존재 — 머지 단계 스킵."
elif [ -z "$PROJECT_PATH" ]; then
    echo "[INFO] PROJECT_PATH 미지정 — 머지 단계 스킵 (시스템 작업)."
else
    # ── Lock-in 1 First-line: cancelled + guard.sh가 else 블록 첫 statement ──
    if [[ -f "$EVENTS_DIR/${TASK_ID}.cancelled" ]]; then
        echo "[CANCELLED] merge step blocked — task $TASK_ID cancelled (.cancelled 마커 존재)" >&2
        exit 1
    fi
    # task-2472: state file missing 시 merge 차단
    STATE_FILE="$WORKSPACE/.tasks/state/${TASK_ID}.json"
    if [ ! -f "$STATE_FILE" ]; then
        echo "[ERROR] state file missing: $STATE_FILE — merge 차단 (task-2472)" >&2
        python3 "$WORKSPACE/scripts/escalation_marker.py" emit \
            --task-id "$TASK_ID" \
            --kind escalated \
            --reason "state file missing" \
            --source "finish-task.sh" \
            --blocking "state_file_missing" \
            --evidence "$STATE_FILE" 2>/dev/null || true
        exit 1
    fi

    # task-2467: BLOCKED/ESCALATED 시 .done 차단
    # task-2472: raw `: >` emit 제거 → escalation_marker.py JSON payload 발행으로 교체
    # Gemini 리뷰 medium: || echo '{}' fallback 제거 (taskctl 실패를 무시하면 corruption 시 merge 진행 위험).
    # taskctl status가 비-zero exit하면 fail-closed.
    if ! TASKCTL_STATUS_JSON=$(python3 "$WORKSPACE/scripts/taskctl.py" status "$TASK_ID" --machine 2>&1); then
        echo "[ERROR] taskctl status 실패 — merge 차단 ($TASK_ID): $TASKCTL_STATUS_JSON" >&2
        exit 1
    fi
    CURRENT_STATE=$(echo "$TASKCTL_STATUS_JSON" | python3 -c "import json,sys; d=json.load(sys.stdin); print(d.get('current_state','') if isinstance(d,dict) else '')" 2>/dev/null || echo "")
    case "$CURRENT_STATE" in
        BLOCKED)
            python3 "$WORKSPACE/scripts/escalation_marker.py" emit \
                --task-id "$TASK_ID" \
                --kind blocked \
                --reason "taskctl state=BLOCKED at finish-task.sh" \
                --source "finish-task.sh" \
                --blocking "taskctl_state_blocked" \
                --evidence "$EVENTS_DIR/${TASK_ID}.qc-result" || {
                echo "[ERROR] escalation_marker emit failed (BLOCKED) — fail-closed" >&2
                exit 1
            }
            echo "[BLOCKED] taskctl state=BLOCKED. .done.blocked JSON payload 발행."
            python3 "$WORKSPACE/memory/task-timer.py" end "$TASK_ID" --qc-result BLOCKED 2>&1 || true
            _timer_ended=1
            exit 0
            ;;
        ESCALATED)
            python3 "$WORKSPACE/scripts/escalation_marker.py" emit \
                --task-id "$TASK_ID" \
                --kind escalated \
                --reason "taskctl state=ESCALATED at finish-task.sh" \
                --source "finish-task.sh" \
                --blocking "taskctl_state_escalated" \
                --evidence "$EVENTS_DIR/${TASK_ID}.qc-result" || {
                echo "[ERROR] escalation_marker emit failed (ESCALATED) — fail-closed" >&2
                exit 1
            }
            echo "[ESCALATED] taskctl state=ESCALATED. .done.escalated JSON payload 발행."
            python3 "$WORKSPACE/memory/task-timer.py" end "$TASK_ID" --qc-result ESCALATED 2>&1 || true
            _timer_ended=1
            exit 0
            ;;
    esac
    if ! bash "$WORKSPACE/scripts/guard.sh" pre-push "$TASK_ID"; then
        echo "[GUARD-FAIL] pre-push guard 실패 — merge 차단 ($TASK_ID)" >&2
        exit 1
    fi
    if ! bash "$WORKSPACE/scripts/guard.sh" qc-check "$TASK_ID"; then
        echo "[GUARD-FAIL] qc-check guard 실패 — merge 차단 ($TASK_ID)" >&2
        exit 1
    fi
    echo "[INFO] 머지 실행: worktree_manager.py finish $PROJECT_PATH $TASK_ID $TEAM_SHORT --action auto"
    python3 "$WORKSPACE/scripts/worktree_manager.py" finish "$PROJECT_PATH" "$TASK_ID" "$TEAM_SHORT" --action auto 2>&1 || {
        echo "[WARN] worktree_manager.py finish 실패 — 계속 진행."
    }
    echo '{"task_id":"'"$TASK_ID"'","project_path":"'"$PROJECT_PATH"'","timestamp":"'"$(date -Iseconds)"'"}' > "$MERGE_DONE_FILE"
    echo "[INFO] 머지 완료 — .merge-done 생성: $MERGE_DONE_FILE"
    # task-2367 P1: post-merge health probe (5분 후 background 실행)
    if [ -f "$WORKSPACE/scripts/post_merge_probe.py" ]; then
        MERGE_SHA="${MERGE_SHA:-$(cd "${PROJECT_PATH:-$WORKSPACE}" 2>/dev/null && git rev-parse HEAD 2>/dev/null || echo "")}"
        if [ -n "$MERGE_SHA" ]; then
            # 멱등 마크가 있으면 스킵
            PROBE_MARK="$EVENTS_DIR/${TASK_ID}.probe-done"
            if [ ! -f "$PROBE_MARK" ]; then
                echo "[POST-PROBE] 5분 후 health probe 예약: ${TASK_ID} sha=${MERGE_SHA:0:8}"
                (
                    python3 "$WORKSPACE/scripts/post_merge_probe.py" \
                        --task-id "$TASK_ID" \
                        --merge-sha "$MERGE_SHA" \
                        --project-path "${PROJECT_PATH:-$WORKSPACE}" \
                        --delay 300 \
                        >> "$WORKSPACE/logs/post-merge-probe.log" 2>&1 &
                ) </dev/null
                echo "[POST-PROBE] background pid 분리됨"
            fi
        fi
    fi
fi

# 2.3. PR 머지 검증 (worktree PR 생성한 경우만)
if [ -n "$PROJECT_PATH" ] && [ -d "$PROJECT_PATH/.git" ]; then
    BRANCH_NAME="task/${TASK_ID}-${TEAM_SHORT}"
    OPEN_PR=$(gh pr list --state open --head "$BRANCH_NAME" --json number --jq length 2>/dev/null || echo "0")
    if [ "$OPEN_PR" -gt 0 ]; then
        echo "[PR-GATE] BLOCKED: PR이 아직 머지되지 않았습니다 (branch: $BRANCH_NAME)"
        exit 1
    fi
fi

# 2.5. Git 커밋 검증 게이트 (task-2031)
# non-code task 판별: 코드 수정 없는 작업은 git 게이트 SKIP
GIT_GATE_SKIP=0
if [ -f "$TASK_FILE" ]; then
    # ## 레벨 섹션 내에서만 non-code 키워드를 검사 (본문의 설명 텍스트 오탐 방지)
    GIT_GATE_SKIP=$(python3 -c "
import re, sys
try:
    with open('$TASK_FILE', encoding='utf-8') as f:
        content = f.read()
    # ## 레벨 섹션 추출
    m = re.search(r'## 레벨\s*\n(.*?)(?=\n## |\Z)', content, re.DOTALL)
    if m:
        section = m.group(1)
        if re.search(r'코드 수정 없음|문서 업데이트만|문서만|리서치만', section):
            print('1')
        else:
            print('0')
    else:
        print('0')
except Exception:
    print('0')
" 2>/dev/null || echo "0")
    if [ "$GIT_GATE_SKIP" -eq 1 ]; then
        echo "[GIT-GATE] non-code task 감지 — git 검증 SKIP."
    fi
fi

# 변수 기본값 초기화 (Git 게이트 skip 시에도 후속 게이트가 참조 가능하도록)
PROJ_DIR="${PROJECT_PATH:-$WORKSPACE}"
COMMIT_COUNT=0

if [ "$GIT_GATE_SKIP" -eq 0 ]; then
    # 작업 디렉토리 기반 프로젝트 루트 감지
    WORK_DIR="${PROJECT_PATH:-$WORKSPACE}"
    PROJ_DIR=$(git -C "$WORK_DIR" rev-parse --show-toplevel 2>/dev/null || echo "$WORK_DIR")

    # 1) task ID 커밋 최소 1건
    COMMIT_COUNT=$(git -C "$PROJ_DIR" log --oneline --all --grep="$TASK_ID" 2>/dev/null | wc -l)
    if [ "$COMMIT_COUNT" -eq 0 ]; then
        echo "[GIT-GATE] BLOCKED: $TASK_ID 커밋 0건. git commit 후 재실행." >&2
        exit 1
    fi
    echo "[GIT-GATE] 커밋 $COMMIT_COUNT건 확인."

    # 2) uncommitted 변경 없음 (시스템 자동 파일 제외)
    REAL_DIFF=$(git -C "$PROJ_DIR" diff --name-only 2>/dev/null | { grep -v -E '(memory/heartbeats/|memory/events/|memory/daily/|memory/logs/|memory/reports/|memory/tasks/|logs/|whisper/|bot-activity\.json|token-ledger\.json|memory/pipeline-status\.json|memory/preview-state\.json|memory/merge-log\.json|memory/bot_settings_sync\.json|memory/memory-check-log\.json|dashboard/data/refine-|dashboard/data/medium-comments-log\.json|\.heartbeat|memory/\.task-counter|memory/task-timers\.json|memory/canary-status\.json|scripts/gemini_rate_tracker\.json|tests/coverage-report\.txt|config/constants\.json)' || true; } | wc -l)
    REAL_CACHED=$(git -C "$PROJ_DIR" diff --cached --name-only 2>/dev/null | { grep -v -E '(memory/heartbeats/|memory/events/|memory/daily/|memory/logs/|memory/reports/|memory/tasks/|logs/|whisper/|bot-activity\.json|token-ledger\.json|memory/pipeline-status\.json|memory/preview-state\.json|memory/merge-log\.json|memory/bot_settings_sync\.json|memory/memory-check-log\.json|dashboard/data/refine-|dashboard/data/medium-comments-log\.json|\.heartbeat|memory/\.task-counter|memory/task-timers\.json|memory/canary-status\.json|scripts/gemini_rate_tracker\.json|tests/coverage-report\.txt|config/constants\.json)' || true; } | wc -l)
    if [ "$REAL_DIFF" -gt 0 ] || [ "$REAL_CACHED" -gt 0 ]; then
        echo "[GIT-GATE] BLOCKED: uncommitted 변경 존재 (${REAL_DIFF} unstaged, ${REAL_CACHED} staged)." >&2
        exit 1
    fi
    echo "[GIT-GATE] uncommitted 변경 없음 확인 (시스템 자동 파일 제외)."

    # 3) 빈 커밋 방어
    LAST_HASH=$(git -C "$PROJ_DIR" log --all --grep="$TASK_ID" --format="%H" -1 2>/dev/null)
    if [ -n "$LAST_HASH" ]; then
        DIFF_FILES=$(git -C "$PROJ_DIR" diff --name-only "${LAST_HASH}^..${LAST_HASH}" 2>/dev/null | wc -l)
        if [ "$DIFF_FILES" -eq 0 ]; then
            echo "[GIT-GATE] BLOCKED: 빈 커밋(변경 파일 0건)." >&2
            exit 1
        fi
        echo "[GIT-GATE] 마지막 커밋 변경 파일 ${DIFF_FILES}건 확인."
    fi

    echo "[GIT-GATE] PASS — git 검증 통과."
fi

# 2.6. Impact Scanner Gate
IMPACT_RESULT_VAL="PASS"
IMPACT_ENABLED=$(python3 -c "from utils.gate_config_loader import is_gate_enabled; print(is_gate_enabled('impact_scanner'))" 2>/dev/null || echo "false")
if [ "$IMPACT_ENABLED" = "True" ]; then
    IMPACT_MODE=$(python3 -c "from utils.gate_config_loader import get_gate_mode; print(get_gate_mode('impact_scanner'))" 2>/dev/null || echo "warn")
    set +e
    IMPACT_RAW=$(python3 scripts/impact_scanner.py --project-root "$PROJ_DIR" --task-id "$TASK_ID" 2>/dev/null | tail -1)
    IMPACT_EXIT=$?
    set -e
    IMPACT_GATE=$(echo "$IMPACT_RAW" | python3 -c "import sys,json; print(json.load(sys.stdin).get('gate_result','PASS'))" 2>/dev/null || echo "PASS")
    if [ $IMPACT_EXIT -eq 2 ] || [ "$IMPACT_GATE" = "BLOCK" ]; then
        IMPACT_RESULT_VAL="BLOCK"
    elif [ $IMPACT_EXIT -eq 1 ] || [ "$IMPACT_GATE" = "WARN" ]; then
        IMPACT_RESULT_VAL="WARN"
    fi
    echo "[IMPACT-GATE] result=$IMPACT_RESULT_VAL mode=$IMPACT_MODE"
    if [ "$IMPACT_RESULT_VAL" = "BLOCK" ] && [ "$IMPACT_MODE" = "fail" ]; then
        echo "[IMPACT-GATE] BLOCKED: 수정되지 않은 참조 파일 6건 이상"
        exit 1
    fi
else
    echo "[IMPACT-GATE] disabled — 스킵."
fi

# 2.6.5. CI Preflight Gate
CI_RESULT_VAL="PASS"
CI_ENABLED=$(python3 -c "from utils.gate_config_loader import is_gate_enabled; print(is_gate_enabled('ci_preflight'))" 2>/dev/null || echo "false")
if [ "$CI_ENABLED" = "True" ]; then
    CI_MODE=$(python3 -c "from utils.gate_config_loader import get_gate_mode; print(get_gate_mode('ci_preflight'))" 2>/dev/null || echo "warn")
    set +e
    bash scripts/ci_preflight.sh "$PROJ_DIR" --affected-only "$COMMIT_COUNT"
    CI_EXIT=$?
    set -e
    if [ $CI_EXIT -eq 0 ]; then
        CI_RESULT_VAL="PASS"
    elif [ $CI_EXIT -eq 2 ]; then
        CI_RESULT_VAL="WARN"
    else
        CI_RESULT_VAL="FAIL"
    fi
    echo "[CI-PREFLIGHT] exit=$CI_EXIT result=$CI_RESULT_VAL mode=$CI_MODE"
    if [ "$CI_RESULT_VAL" = "FAIL" ] && [ "$CI_MODE" = "fail" ]; then
        echo "[CI-PREFLIGHT] BLOCKED: CI 테스트 실패"
        exit 1
    fi
else
    echo "[CI-PREFLIGHT] disabled — 스킵."
fi

# 2.6.9. Playwright Chrome 좀비 정리 (task-2324)
echo "[cleanup] Playwright Chrome 프로세스 정리..."
pkill -f "ms-playwright.*chromium.*chrome" 2>/dev/null || true
pkill -f "ms-playwright.*chrome-headless-shell" 2>/dev/null || true
sleep 1
# 강제 kill (SIGKILL) - graceful shutdown 실패 시
pkill -9 -f "ms-playwright.*chromium.*chrome" 2>/dev/null || true
pkill -9 -f "ms-playwright.*chrome-headless-shell" 2>/dev/null || true
echo "[cleanup] Playwright Chrome 정리 완료"

# 2.6.10. 워크트리 백그라운드 프로세스 cleanup (task-2350)
# 봇이 워크트리에서 띄운 dev 서버(npm run dev, vite, uvicorn, playwright 등) 자동 종료
# 안전 가드: /.worktrees/<task-id>-<team>/ 부분 문자열 매칭으로만 종료 (시스템 전역 dev 서버 보존)
WORKTREE_SUBSTR=""
if [ -n "$PROJECT_PATH" ] && [[ "$PROJECT_PATH" == *"/.worktrees/"* ]]; then
    # 1차: PROJECT_PATH 자체가 워크트리 경로
    WORKTREE_SUBSTR="$PROJECT_PATH"
elif [ -n "$TASK_ID" ] && [ -n "$TEAM_SHORT" ]; then
    # 2차: 패턴 매칭 (.worktrees/<task-id>-<team>)
    WORKTREE_SUBSTR=".worktrees/${TASK_ID}-${TEAM_SHORT}"
fi

if [ -n "$WORKTREE_SUBSTR" ]; then
    echo "[bg-cleanup] 워크트리 백그라운드 프로세스 정리 시작: $WORKTREE_SUBSTR"

    # 자기 자신/조상 PID 수집 (kill 제외)
    SELF_PID=$$
    EXCLUDE_PIDS=" $SELF_PID "
    _cur_pid=$SELF_PID
    while [ "$_cur_pid" -gt 1 ]; do
        _ppid=$(awk '/^PPid:/ {print $2}' /proc/$_cur_pid/status 2>/dev/null || echo "")
        if [ -z "$_ppid" ] || [ "$_ppid" = "0" ] || [ "$_ppid" = "1" ]; then
            break
        fi
        EXCLUDE_PIDS="$EXCLUDE_PIDS$_ppid "
        _cur_pid=$_ppid
    done

    # /proc/<pid>/cwd 심볼릭 링크에서 워크트리 경로 매칭하는 PID 수집
    BG_PIDS=""
    for pid_dir in /proc/[0-9]*; do
        pid=$(basename "$pid_dir")
        # 자기 자신/조상 제외
        case "$EXCLUDE_PIDS" in
            *" $pid "*) continue ;;
        esac
        cwd=$(readlink "$pid_dir/cwd" 2>/dev/null || true)
        if [ -n "$cwd" ] && [[ "$cwd" == *"$WORKTREE_SUBSTR"* ]]; then
            BG_PIDS="$BG_PIDS $pid"
        fi
    done

    BG_COUNT=0
    for _ in $BG_PIDS; do BG_COUNT=$((BG_COUNT + 1)); done

    if [ "$BG_COUNT" -gt 0 ]; then
        echo "[bg-cleanup] 종료 대상 PID:$BG_PIDS (총 ${BG_COUNT}개)"
        # 자식 프로세스 트리 SIGTERM
        for pid in $BG_PIDS; do
            pkill -TERM -P "$pid" 2>/dev/null || true
            kill -TERM "$pid" 2>/dev/null || true
        done
        # graceful 대기
        sleep 5
        # SIGKILL 폴백
        KILLED_HARD=0
        for pid in $BG_PIDS; do
            if kill -0 "$pid" 2>/dev/null; then
                pkill -KILL -P "$pid" 2>/dev/null || true
                kill -KILL "$pid" 2>/dev/null || true
                KILLED_HARD=$((KILLED_HARD + 1))
            fi
        done
        echo "[bg-cleanup] 워크트리 백그라운드 프로세스 ${BG_COUNT}개 종료 완료 (SIGKILL 폴백: ${KILLED_HARD}개)"
    else
        echo "[bg-cleanup] 워크트리에서 실행 중인 백그라운드 프로세스 없음"
    fi
else
    echo "[bg-cleanup] WORKTREE_SUBSTR 미감지 — 스킵 (시스템 작업 또는 비-워크트리)"
fi

# 2.7. 팀원 전원 idle 복원 (member-status.json)
python3 -c "
import json
from datetime import datetime, timezone
status_file = '$WORKSPACE/memory/events/member-status.json'
try:
    with open(status_file) as f:
        data = json.load(f)
    now = datetime.now(timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ')
    changed = False
    for name, info in data.get('members', {}).items():
        if info.get('status') in ('working', 'standby'):
            task_desc = info.get('task', '') or ''
            if '$TASK_ID' in task_desc or not task_desc or info.get('status') == 'standby':
                info['status'] = 'idle'
                info['since'] = now
                info['task'] = None
                changed = True
    if changed:
        data['updated_at'] = now
        with open(status_file, 'w') as f:
            json.dump(data, f, indent=2, ensure_ascii=False)
        print('[INFO] member-status.json: working/standby → idle 복원 완료')
    else:
        print('[INFO] member-status.json: 복원 대상 없음')
except Exception as e:
    print(f'[WARN] member-status idle 복원 실패: {e}')
" 2>&1 || echo "[WARN] member-status idle 복원 스킵"

# 2.8. G3 독립 검증 (Lv.3+ 작업에만 적용)
TASK_LEVEL=$(python3 -c "
import json, re
workspace = '$WORKSPACE'
task_id = '$TASK_ID'

# 1차: task-timers.json work_level
level = ''
try:
    with open(f'{workspace}/memory/task-timers.json') as f:
        data = json.load(f)
    tasks = data.get('tasks', data)
    t = tasks.get(task_id, {})
    level = (t.get('work_level', '') or '').strip().lower()
except Exception:
    pass

# 2차: task 파일 ## 레벨 섹션 파싱
if not level:
    task_file = f'{workspace}/memory/tasks/{task_id}.md'
    try:
        with open(task_file) as f:
            content = f.read()
        m = re.search(r'## 레벨\s*\n(.*?)(?=\n## |\Z)', content, re.DOTALL)
        if m:
            section = m.group(1).strip().lower()
            if 'critical' in section:
                level = 'critical'
            elif 'security' in section:
                level = 'security'
            elif re.search(r'lv\.?4|level\s*4', section):
                level = 'lv4'
            elif re.search(r'lv\.?3|level\s*3', section):
                level = 'lv3'
    except Exception:
        pass

# Lv.3+ 판정
lv3_keywords = ('critical', 'security', 'lv3', 'lv4', 'lv.3', 'lv.4')
is_lv3 = any(kw in level for kw in lv3_keywords)
if not is_lv3 and level:
    import re as _re
    is_lv3 = bool(_re.search(r'level\s*[34]', level))
print('lv3plus' if is_lv3 else 'below')
" 2>/dev/null || echo "below")

if [ "$TASK_LEVEL" = "lv3plus" ]; then
    echo "[G3-GATE] Lv.3+ 감지 — g3_independent_verifier 실행."
    set +e
    python3 "$WORKSPACE/scripts/g3_independent_verifier.py" --task-id "$TASK_ID" 2>&1
    G3_EXIT=$?
    set -e
    if [ "$G3_EXIT" -ne 0 ]; then
        echo "[G3-GATE] FAIL — .done 생성 차단. g3_verifier exit code=$G3_EXIT"
        # .g3-failed 이벤트 생성 (QC FAIL 패턴과 동일)
        echo '{"task_id":"'"$TASK_ID"'","team":"'"$TEAM_SHORT"'","fail_reason":"G3 FAIL","timestamp":"'"$(date -Iseconds)"'"}' > "$EVENTS_DIR/${TASK_ID}.g3-failed"
        exit 1
    fi
    echo "[G3-GATE] PASS — g3 검증 통과."
else
    echo "[G3-GATE] Lv.2 이하 — g3_verifier 스킵."
fi

# 2.8.5. [G4-GATE] Pre-PR Gemini CLI 단발 gate (task-2562, OAuth-personal)
# 회장 §명시: G3 PASS 직후, .done 이전, Lv.4 보안 감사 이전.
# Lv.1~2 soft (warning + PR open 허용) / Lv.2 risk-trigger hard / Lv.3+ hard
# fix_loop_count max=2 / scope_violation 즉시 ESCALATED / OAuth-personal 강제.
if [ "${G4_GATE_ENABLED:-1}" = "1" ] && [ -f "$WORKSPACE/scripts/gemini_cli_gate_check.py" ]; then
    echo "[G4-GATE] Pre-PR Gemini CLI 단발 gate 실행 (task=$TASK_ID, level=$TASK_LEVEL)"
    # gemini comment(2026-05-12): --affected-files / --expected-files 전달.
    # ACTUAL_CHANGED_FILES / EXPECTED_FILES 가 상위 step에서 export 되었으면 사용.
    G4_AFFECTED_ARGS=()
    if [ -n "${ACTUAL_CHANGED_FILES:-}" ]; then
        # shellcheck disable=SC2206
        G4_AFFECTED_ARGS=(--affected-files ${ACTUAL_CHANGED_FILES})
    fi
    G4_EXPECTED_ARGS=()
    if [ -n "${EXPECTED_FILES:-}" ]; then
        # shellcheck disable=SC2206
        G4_EXPECTED_ARGS=(--expected-files ${EXPECTED_FILES})
    fi
    set +e
    python3 "$WORKSPACE/scripts/gemini_cli_gate_check.py" \
        --task-id "$TASK_ID" \
        --workspace-root "$WORKSPACE" \
        "${G4_AFFECTED_ARGS[@]}" \
        "${G4_EXPECTED_ARGS[@]}" \
        2>&1
    G4_EXIT=$?
    set -e
    if [ "$G4_EXIT" -eq 2 ]; then
        echo "[G4-GATE] ESCALATED — scope_violation 또는 fix_loop_cap. .done 차단."
        exit 1
    elif [ "$G4_EXIT" -eq 1 ]; then
        echo "[G4-GATE] hard FAIL — .done 생성 차단 + PR open 금지."
        exit 1
    else
        echo "[G4-GATE] PASS 또는 soft (PR open 허용)."
    fi
else
    echo "[G4-GATE] disabled or gate script missing — skipped."
fi

# 2.9. Lv.4 보안 감사 검증 (WARNING only — 첫 단계)
if [ "$TASK_LEVEL" = "lv3plus" ]; then
    IS_LV4=$(python3 -c "
import re
task_file = '$WORKSPACE/memory/tasks/$TASK_ID.md'
try:
    with open(task_file) as f:
        content = f.read()
    m = re.search(r'## 레벨\s*\n(.*?)(?=\n## |\Z)', content, re.DOTALL)
    if m:
        section = m.group(1).strip().lower()
        if 'critical' in section or 'security' in section or re.search(r'lv\.?4|level\s*4', section):
            print('yes')
        else:
            print('no')
    else:
        print('no')
except Exception:
    print('no')
" 2>/dev/null || echo "no")

    if [ "$IS_LV4" = "yes" ]; then
        echo "[LV4-AUDIT] Lv.4 감지 — 보안 감사 검증 시작."

        # 2.9.1 security-audit 이벤트 파일 체크
        SECURITY_AUDIT_FILE="$EVENTS_DIR/${TASK_ID}.security-audit"
        if [ ! -f "$SECURITY_AUDIT_FILE" ]; then
            echo "[LV4-AUDIT] ⚠️ WARNING: security-audit 파일 없음 ($SECURITY_AUDIT_FILE)"
            echo "[LV4-AUDIT] Lv.4 작업은 보안 감사 결과 파일이 필수입니다. .done 차단."
            exit 1
        else
            echo "[LV4-AUDIT] ✅ security-audit 파일 존재 확인."
        fi

        # 2.9.2 로키 보안 감사 참여 기록 체크 (보고서에서)
        REPORT_FILE="$WORKSPACE/memory/reports/${TASK_ID}.md"
        if [ -f "$REPORT_FILE" ]; then
            LOKI_SECURITY=$(python3 -c "
import re
report_path = '$REPORT_FILE'
try:
    with open(report_path) as f:
        content = f.read()
    has_loki = bool(re.search(r'로키|Loki|loki', content))
    has_security = bool(re.search(r'보안 감사|security audit|레드팀|red.?team', content, re.IGNORECASE))
    if has_loki and has_security:
        print('PASS')
    elif has_loki:
        print('PARTIAL')
    else:
        print('MISSING')
except Exception:
    print('ERROR')
" 2>/dev/null || echo "ERROR")

            case "$LOKI_SECURITY" in
                PASS)
                    echo "[LV4-AUDIT] ✅ 로키 보안 감사 참여 기록 확인."
                    ;;
                PARTIAL)
                    echo "[LV4-AUDIT] ⚠️ WARNING: 로키 참여 기록은 있으나 보안 감사 명시 없음."
                    ;;
                MISSING)
                    echo "[LV4-AUDIT] ⚠️ WARNING: 로키 보안 감사 참여 기록 없음."
                    echo "[LV4-AUDIT] Lv.4 작업은 로키 레드팀 보안 감사가 필수입니다. .done 차단."
                    exit 1
                    ;;
                *)
                    echo "[LV4-AUDIT] ⚠️ WARNING: 보고서 파싱 실패."
                    ;;
            esac
        else
            echo "[LV4-AUDIT] ⚠️ WARNING: 보고서 파일 없음 — 로키 참여 확인 불가."
        fi

        echo "[LV4-AUDIT] Lv.4 보안 감사 검증 완료 (FAIL 게이트 적용)."
    fi
fi

# 2.10. Codex 사전검증 결과 파일 체크 (Lv.3+ 작업에만 적용)
if [ "$TASK_LEVEL" = "lv3plus" ]; then
    CODEX_GATE_FILE="$EVENTS_DIR/${TASK_ID}.codex-gate"
    if [ -f "$CODEX_GATE_FILE" ]; then
        CODEX_PASS=$(python3 -c "
import json
try:
    with open('$CODEX_GATE_FILE') as f:
        data = json.load(f)
    print('PASS' if data.get('pass', False) else 'FAIL')
except Exception:
    print('UNKNOWN')
" 2>/dev/null || echo "UNKNOWN")
        echo "[CODEX-GATE] Codex 사전검증 결과 파일 존재: $CODEX_PASS"
    else
        echo "[CODEX-GATE] WARNING: Codex 사전검증 결과 파일 없음 ($CODEX_GATE_FILE). Codex 게이트가 실행되지 않았습니다."
    fi
else
    echo "[CODEX-GATE] Lv.2 이하 — Codex 게이트 체크 스킵."
fi

# 2.11. Unresolved Issue Gate
UNRESOLVED_RESULT_VAL="PASS"
UNRESOLVED_ENABLED=$(python3 -c "from utils.gate_config_loader import is_gate_enabled; print(is_gate_enabled('unresolved_gate'))" 2>/dev/null || echo "false")
if [ "$UNRESOLVED_ENABLED" = "True" ]; then
    UNRESOLVED_MODE=$(python3 -c "from utils.gate_config_loader import get_gate_mode; print(get_gate_mode('unresolved_gate'))" 2>/dev/null || echo "warn")
    if [ -f "$REPORT_FILE" ]; then
        UNRESOLVED_COUNT=$(grep -ciE "범위 내 미해결|in.scope.*unresolved" "$REPORT_FILE" 2>/dev/null | tail -1 || echo "0")
        UNRESOLVED_COUNT="${UNRESOLVED_COUNT:-0}"
        MAX_UNRESOLVED=$(python3 -c "from utils.gate_config_loader import load_gate_config; print(load_gate_config('unresolved_gate').get('max_in_scope_unresolved', 3))" 2>/dev/null || echo "3")
        echo "[UNRESOLVED-GATE] count=$UNRESOLVED_COUNT max=$MAX_UNRESOLVED mode=$UNRESOLVED_MODE"
        if [ "$UNRESOLVED_COUNT" -gt "$MAX_UNRESOLVED" ]; then
            UNRESOLVED_RESULT_VAL="BLOCK"
            if [ "$UNRESOLVED_MODE" = "fail" ]; then
                echo "[UNRESOLVED-GATE] BLOCKED"
                exit 1
            fi
        elif [ "$UNRESOLVED_COUNT" -gt 0 ]; then
            UNRESOLVED_RESULT_VAL="WARN"
        fi
    fi
else
    echo "[UNRESOLVED-GATE] disabled — 스킵."
fi

# 2.12. Goal Assertions Gate
GOAL_RESULT_VAL="PASS"
GOAL_ENABLED=$(python3 -c "from utils.gate_config_loader import is_gate_enabled; print(is_gate_enabled('goal_assertions'))" 2>/dev/null || echo "false")
if [ "$GOAL_ENABLED" = "True" ]; then
    GOAL_MODE=$(python3 -c "from utils.gate_config_loader import get_gate_mode; print(get_gate_mode('goal_assertions'))" 2>/dev/null || echo "fail")
    if [ -f "$TASK_FILE" ]; then
        GOALS=$(python3 -c "
import re
with open('$TASK_FILE') as f:
    content = f.read()
m = re.search(r'## goal_assertions.*?\n(.*?)(?=\n##|\Z)', content, re.S)
if m:
    for line in m.group(1).strip().split('\n'):
        cmd = re.search(r'\x60([^\x60]+)\x60', line)
        if cmd:
            print(cmd.group(1))
" 2>/dev/null)
        # allowed_commands 화이트리스트 로드
        ALLOWED_CMDS=$(python3 -c "from utils.gate_config_loader import load_gate_config; print(' '.join(load_gate_config('goal_assertions').get('allowed_commands', [])))" 2>/dev/null || echo "grep curl pytest python3 tsc cat jq npx npm")
        GOAL_FAIL=0
        while IFS= read -r cmd; do
            [ -z "$cmd" ] && continue
            # 명령의 첫 단어 추출 및 화이트리스트 검증
            FIRST_WORD=$(echo "$cmd" | awk '{print $1}')
            ALLOWED=0
            for acmd in $ALLOWED_CMDS; do
                if [ "$FIRST_WORD" = "$acmd" ]; then
                    ALLOWED=1
                    break
                fi
            done
            if [ $ALLOWED -eq 0 ]; then
                echo "[GOAL-GATE] SKIP (not in allowed_commands): $cmd"
                continue
            fi
            set +e
            eval "$cmd" > /dev/null 2>&1
            CMD_EXIT=$?
            set -e
            if [ $CMD_EXIT -ne 0 ]; then
                echo "[GOAL-GATE] FAIL: $cmd"
                GOAL_FAIL=1
            fi
        done <<< "$GOALS"
        if [ $GOAL_FAIL -eq 1 ]; then
            GOAL_RESULT_VAL="FAIL"
            if [ "$GOAL_MODE" = "fail" ]; then
                echo "[GOAL-GATE] BLOCKED: goal_assertions 미충족"
                exit 1
            fi
        fi
    fi
else
    echo "[GOAL-GATE] disabled — 스킵."
fi

# l1_smoketest 결과 수집 (.qc-result에서)
L1_RESULT_VAL="PASS"
if [ -f "$QC_RESULT_FILE" ]; then
    L1_FROM_QC=$(python3 -c "
import json
try:
    with open('$QC_RESULT_FILE') as f:
        data = json.load(f)
    checks = data.get('checks_summary', {})
    l1 = checks.get('l1_smoketest_check', checks.get('l1_smoketest', 'PASS'))
    print(l1)
except Exception:
    print('PASS')
" 2>/dev/null || echo "PASS")
    if [ "$L1_FROM_QC" = "FAIL" ]; then
        L1_RESULT_VAL="FAIL"
    fi
fi

# task-2468 P0-1: .g3-fail 마커 차단 (G3 gate와 별개로 .done 차단 한 번 더)
python3 -c "
import sys
sys.path.insert(0, '$WORKSPACE/scripts')
from lifecycle_guards import check_g3_fail_blocks_done
r = check_g3_fail_blocks_done('$TASK_ID')
if not r.ok:
    print(f'[GUARD] .done 차단 (P0-1): {r.reason}')
    sys.exit(1)
" || { echo "[GUARD] G3 fail marker 검출 — .done 차단"; exit 1; }

# 3. .done 원자적 생성 (qc_result 포함)
if ! (set -C; python3 -c "
import json, sys
data = {
    'task_id': '$TASK_ID',
    'team': '$TEAM_SHORT',
    'qc_result': '$QC_STATUS',
    'completed_at': '$(date -Iseconds)',
    'status': 'done',
    'gate_results': {
        'impact_scanner': '$IMPACT_RESULT_VAL',
        'ci_preflight': '$CI_RESULT_VAL',
        'l1_smoketest': '$L1_RESULT_VAL',
        'goal_assertions': '$GOAL_RESULT_VAL',
        'unresolved_gate': '$UNRESOLVED_RESULT_VAL',
    },
}
with open('$DONE_FILE', 'x') as f:
    json.dump(data, f, ensure_ascii=False, indent=2)
") 2>/dev/null; then
    echo "[WARN] .done already exists: $DONE_FILE"
fi

# ── task-2569 RC-3: stash lifecycle audit (종료) ──
_STASH_AUDIT_AFTER=$(git stash list 2>/dev/null | wc -l)
mkdir -p "$WORKSPACE/memory/logs"
python3 - "$_STASH_AUDIT_AFTER" "${TASK_ID:-unknown}" <<'PYEOF' >> "$WORKSPACE/memory/logs/cleanup-audit.jsonl" 2>/dev/null || true
import json, sys, time
print(json.dumps({"ts": time.time(), "audit": "finish-task-end", "task_id": sys.argv[2], "stash_count": int(sys.argv[1])}, ensure_ascii=False))
PYEOF

# 4. task-timer end에 qc_result 전달
_timer_ended=1
if [ -n "$QC_STATUS" ]; then
    python3 "$WORKSPACE/memory/task-timer.py" end "$TASK_ID" --qc-result "$QC_STATUS" 2>&1 || echo "[WARN] task-timer end failed"
else
    python3 "$WORKSPACE/memory/task-timer.py" end "$TASK_ID" 2>&1 || echo "[WARN] task-timer end failed"
fi

# 4.5. token-tracker scan + enrich (토큰 사용량 task-timers.json에 기록)
python3 "$WORKSPACE/scripts/token-tracker.py" scan 2>&1 | tail -1 || echo "[WARN] token-tracker scan failed"
python3 "$WORKSPACE/scripts/token-tracker.py" enrich 2>&1 | tail -1 || echo "[WARN] token-tracker enrich failed"

# 5. notify-completion.py
source "$WORKSPACE/.env.keys" 2>/dev/null || true
python3 "$WORKSPACE/scripts/notify-completion.py" "$TASK_ID" 2>&1 || echo "[WARN] notify-completion failed"

# 6. 봇 → 아누 후속 알림 (task-2360)
# 보고서 + .followup.txt 기반 회장결정/머지/미해결/다음단계 cron 발송.
# .anu-notified 마커로 중복 방지. 환경변수 DISABLE_ANU_FOLLOWUP=1 시 스킵.
if [ "${DISABLE_ANU_FOLLOWUP:-0}" != "1" ]; then
    python3 "$WORKSPACE/scripts/extract_followup.py" send "$TASK_ID" 2>&1 \
        | tail -2 \
        || echo "[WARN] anu-followup send failed (non-fatal)"
fi

echo "[OK] finish-task.sh completed for $TASK_ID"
