"""utils/repository_policy_adapter.py — GitHub repository policy → runtime capability layer.

회장 §명시: GitHub repository ruleset / branch protection / merge capability를
runtime capability layer로 표준화. admin override 없이 deterministic merge path 선택.
"회장 직접 머지 fallback" 완전 제거.

회장 §명시 11개 금지 행위:
  1.  admin override 호출 (--admin flag 정적 차단)
  2.  branch protection 우회 코드
  3.  canonical_workspace_resolver 수정
  4.  automation_contracts 수정
  5.  merge_queue_executor 본체 수정
  6.  replacement_pr_runner 본체 수정
  7.  auto_gemini_triage 본체 수정
  8.  post_merge_smoke_runner 본체 수정
  9.  critical_escalation_reporter 본체 수정
  10. expected_files 외 파일 수정
  11. AUTOMATION_CAPABILITY_GAP을 CriticalEscalationType에 추가 (Critical 7종 외 보고 금지)
"""
from __future__ import annotations

import argparse
import json
import subprocess
import sys
from dataclasses import dataclass
from enum import Enum
from typing import Any, Callable, Optional

from utils.automation_contracts import (  # pyright: ignore[reportMissingImports]
    CriticalEscalationType,
    EscalationPacket,
    RiskLevel,
)
from utils.canonical_workspace_resolver import (  # pyright: ignore[reportMissingImports]
    resolve_canonical_workspace,
)

__all__ = [
    "RepositoryCapability",
    "BlockedReason",
    "MergePathPlan",
    "probe_capability",
    "classify_blocked_reason",
    "select_merge_path",
    "assert_no_admin_override",
    "invoke_triage_hook",
    # Re-exported for downstream wiring (회장 §명시 CanonicalWorkspace + automation_contracts 연동)
    "CriticalEscalationType",
    "EscalationPacket",
    "RiskLevel",
    "resolve_canonical_workspace",
    "build_capability_gap_packet",
]

# ---------------------------------------------------------------------------
# Type alias for injectable runner (enables test stubbing)
# ---------------------------------------------------------------------------
RunnerType = Callable[..., "subprocess.CompletedProcess[str]"]


# ---------------------------------------------------------------------------
# 1. RepositoryCapability — frozen dataclass, 6 probe fields
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class RepositoryCapability:
    """GitHub repository의 머지 관련 capability snapshot.

    모든 필드는 probe 시점에 고정(frozen). admin override 없이 deterministic.
    """

    can_squash_merge: bool
    requires_approval: bool                # required_approving_review_count > 0
    requires_thread_resolution: bool       # required_review_thread_resolution
    auto_merge_enabled: bool               # repo settings.allowAutoMerge
    bot_can_merge: bool                    # bot token이 머지 가능한지
    admin_override_required: bool          # 머지에 admin이 필요한지


# ---------------------------------------------------------------------------
# 2. BlockedReason — 정확히 7종 (회장 §명시)
# ---------------------------------------------------------------------------

class BlockedReason(str, Enum):
    """PR 머지 차단 이유. 정확히 7종."""

    UNRESOLVED_REVIEW_THREAD = "UNRESOLVED_REVIEW_THREAD"
    REQUIRED_APPROVAL = "REQUIRED_APPROVAL"
    STALE_BASE = "STALE_BASE"
    MISSING_CI_CHECK = "MISSING_CI_CHECK"
    BRANCH_PROTECTION = "BRANCH_PROTECTION"
    PERMISSION_ISSUE = "PERMISSION_ISSUE"
    AUTO_MERGE_UNSUPPORTED = "AUTO_MERGE_UNSUPPORTED"


# ---------------------------------------------------------------------------
# 3. MergePathPlan — frozen dataclass
# ---------------------------------------------------------------------------

@dataclass(frozen=True)
class MergePathPlan:
    """Deterministic merge path 선택 결과.

    회장 §명시: admin override 없이 선택된 경로만 허용.
    capability_gap=True 시 CriticalEscalationType에 추가하지 않고
    ops 채널 보고 마커로만 사용 (Critical 7종 외 회장 보고 0건).
    """

    action: str
    # "squash_merge" | "auto_gemini_triage" | "base_sync" |
    # "escalate_capability_gap" | "wait_ci" | "manual_merge_required"

    reason: Optional[BlockedReason]        # None → 정상 squash merge path
    requires_chair: bool                   # True 시 회장 직접 보고. 현 구현은 모든 경로에서 False (ops 채널만 — 회장 §명시 "회장 직접 머지 fallback 제거")
    capability_gap: bool                   # AUTOMATION_CAPABILITY_GAP marker
    triage_hook: Optional[str]             # "auto_gemini_triage.triage_pr" | None
    base_sync_command: Optional[str]       # "git merge origin/main" | None
    description: str


# ---------------------------------------------------------------------------
# 4. Internal runner helper
# ---------------------------------------------------------------------------

def _default_runner(
    args: list[str],
    *,
    cwd: Optional[str] = None,
) -> "subprocess.CompletedProcess[str]":
    return subprocess.run(
        args,
        cwd=cwd,
        capture_output=True,
        text=True,
        timeout=30,
    )


def _run_gh(
    args: list[str],
    *,
    runner: Optional[RunnerType] = None,
) -> Optional[Any]:
    """gh api 호출 후 JSON dict 반환. 실패 시 None (safe fallback).

    예외 발생 시 stderr에 짧은 trace 기록 후 None 반환 — silent fallback이지만
    디버깅 가능하도록 원인을 남긴다 (Gemini high 권고 수용).
    """
    assert_no_admin_override(args)
    fn = runner if runner is not None else _default_runner
    try:
        result = fn(args, cwd=None)
    except Exception as exc:
        # 네트워크/타임아웃/permission 등 호출 자체 실패
        print(
            f"[repository_policy_adapter] gh call failed ({type(exc).__name__}): "
            f"{' '.join(args[:3])}... → fallback None",
            file=sys.stderr,
        )
        return None

    if result.returncode != 0:
        # 비정상 종료 (404/403 등) — fallback이 정상 흐름이지만 trace는 남김
        return None
    if not result.stdout.strip():
        return None

    try:
        parsed = json.loads(result.stdout)
        return parsed if isinstance(parsed, (dict, list)) else None
    except json.JSONDecodeError as exc:
        print(
            f"[repository_policy_adapter] JSON parse failed for "
            f"{' '.join(args[:3])}: {exc.msg}",
            file=sys.stderr,
        )
        return None


# ---------------------------------------------------------------------------
# 5. assert_no_admin_override — admin override 정적 차단
# ---------------------------------------------------------------------------

def assert_no_admin_override(args: list[str]) -> None:
    """gh 호출 args에 --admin 포함 시 RuntimeError. 어디서든 gh 호출 전에 호출.

    회장 §명시: admin override는 항상 금지.
    """
    if any(a == "--admin" for a in args):
        raise RuntimeError(
            "admin override is forbidden by repository_policy_adapter "
            "(회장 §명시: admin override 항상 금지)"
        )


# ---------------------------------------------------------------------------
# 6. probe_capability
# ---------------------------------------------------------------------------

def probe_capability(
    owner: str,
    repo: str,
    branch: str = "main",
    *,
    runner: Optional[RunnerType] = None,
) -> RepositoryCapability:
    """GitHub API 4회 호출로 RepositoryCapability 6 field를 probe.

    각 호출은 실패 허용 — fallback False 처리.
    runner: subprocess.run 주입 가능 (테스트 stubbing).
    """
    # -----------------------------------------------------------------------
    # Call 1: Repository ruleset — required_review_thread_resolution,
    #         required_approving_review_count
    # -----------------------------------------------------------------------
    requires_thread_resolution = False
    requires_approval = False
    admin_override_required = False

    ruleset_data = _run_gh(
        ["gh", "api", f"repos/{owner}/{repo}/rules/branches/{branch}"],
        runner=runner,
    )
    if ruleset_data is not None:
        rules: list[dict] = []
        if isinstance(ruleset_data, list):
            rules = ruleset_data
        elif isinstance(ruleset_data, dict):
            rules = ruleset_data.get("rules", [])

        for rule in rules:
            rtype = rule.get("type", "")
            params = rule.get("parameters", {}) or {}

            if rtype == "pull_request":
                count = params.get("required_approving_review_count", 0) or 0
                if count > 0:
                    requires_approval = True
                if params.get("required_review_thread_resolution", False):
                    requires_thread_resolution = True
                # bypass_actors — admin only check
                bypass_actors = rule.get("bypass_actors", []) or []
                if bypass_actors and all(
                    (a.get("actor_type", "") == "RepositoryRole"
                     and a.get("role_name", "") == "admin")
                    for a in bypass_actors
                    if isinstance(a, dict)
                ):
                    admin_override_required = True

            if rtype == "restrictions":
                admin_override_required = True

    # -----------------------------------------------------------------------
    # Call 2: Classic branch protection (fallback when ruleset unavailable)
    # -----------------------------------------------------------------------
    protection_data = _run_gh(
        ["gh", "api", f"repos/{owner}/{repo}/branches/{branch}/protection"],
        runner=runner,
    )
    if protection_data is not None and isinstance(protection_data, dict):
        prc = protection_data.get("required_pull_request_reviews", {}) or {}
        if prc:
            count = prc.get("required_approving_review_count", 0) or 0
            if count > 0:
                requires_approval = True
            if prc.get("require_code_owner_reviews", False):
                requires_approval = True
            if protection_data.get("required_conversation_resolution", {}).get("enabled", False):
                requires_thread_resolution = True

        restrictions = protection_data.get("restrictions", None)
        if restrictions is not None:
            admin_override_required = True

    # -----------------------------------------------------------------------
    # Call 3: Repo settings — allow_squash_merge, allow_auto_merge
    # -----------------------------------------------------------------------
    can_squash_merge = False
    auto_merge_enabled = False

    repo_data = _run_gh(
        ["gh", "api", f"repos/{owner}/{repo}"],
        runner=runner,
    )
    if repo_data is not None and isinstance(repo_data, dict):
        can_squash_merge = bool(repo_data.get("allow_squash_merge", False))
        auto_merge_enabled = bool(repo_data.get("allow_auto_merge", False))

    # -----------------------------------------------------------------------
    # Call 4: Bot permission — "write" or "admin" → bot_can_merge=True
    # -----------------------------------------------------------------------
    bot_can_merge = False

    perm_data = _run_gh(
        ["gh", "api",
         f"repos/{owner}/{repo}/collaborators/github-actions[bot]/permission"],
        runner=runner,
    )
    if perm_data is not None and isinstance(perm_data, dict):
        perm = perm_data.get("permission", "") or ""
        if perm in ("write", "admin"):
            bot_can_merge = True

    return RepositoryCapability(
        can_squash_merge=can_squash_merge,
        requires_approval=requires_approval,
        requires_thread_resolution=requires_thread_resolution,
        auto_merge_enabled=auto_merge_enabled,
        bot_can_merge=bot_can_merge,
        admin_override_required=admin_override_required,
    )


# ---------------------------------------------------------------------------
# 7. classify_blocked_reason
# ---------------------------------------------------------------------------

def classify_blocked_reason(
    pr: dict,
    capability: RepositoryCapability,
) -> Optional[BlockedReason]:
    """PR dict + RepositoryCapability → BlockedReason | None.

    우선순위 순차 체크 (회장 §명시 순서 고정):
      1. UNRESOLVED_REVIEW_THREAD
      2. STALE_BASE
      3. REQUIRED_APPROVAL
      4. MISSING_CI_CHECK
      5. PERMISSION_ISSUE
      6. AUTO_MERGE_UNSUPPORTED
      7. BRANCH_PROTECTION
    """
    # ------------------------------------------------------------------
    # Unresolved thread count 계산 (두 가지 형태 지원)
    # ------------------------------------------------------------------
    unresolved_thread_count: int = 0

    raw_threads = pr.get("reviewThreads", None)
    if isinstance(raw_threads, list):
        unresolved_thread_count = sum(
            1 for t in raw_threads
            if isinstance(t, dict) and not t.get("isResolved", True)
        )
    elif isinstance(raw_threads, dict):
        # GraphQL 응답: { "nodes": [...] }
        nodes = raw_threads.get("nodes", []) or []
        unresolved_thread_count = sum(
            1 for t in nodes
            if isinstance(t, dict) and not t.get("isResolved", True)
        )
    else:
        # 직접 int 형태로 전달된 경우
        direct_count = pr.get("unresolved_thread_count", 0)
        if isinstance(direct_count, int):
            unresolved_thread_count = direct_count

    merge_state: str = pr.get("mergeStateStatus", "") or ""
    review_decision: str = pr.get("reviewDecision", "") or ""

    # statusCheckRollup
    rollup = pr.get("statusCheckRollup", None)
    ci_state: str = ""
    if isinstance(rollup, dict):
        ci_state = rollup.get("state", "") or ""
    elif isinstance(rollup, str):
        ci_state = rollup

    # ------------------------------------------------------------------
    # 우선순위 1: UNRESOLVED_REVIEW_THREAD
    # ------------------------------------------------------------------
    if capability.requires_thread_resolution and unresolved_thread_count > 0:
        return BlockedReason.UNRESOLVED_REVIEW_THREAD

    # ------------------------------------------------------------------
    # 우선순위 2: STALE_BASE
    # ------------------------------------------------------------------
    if merge_state == "BEHIND":
        return BlockedReason.STALE_BASE

    # ------------------------------------------------------------------
    # 우선순위 3: REQUIRED_APPROVAL
    # ------------------------------------------------------------------
    if capability.requires_approval and review_decision != "APPROVED":
        return BlockedReason.REQUIRED_APPROVAL

    # ------------------------------------------------------------------
    # 우선순위 4: MISSING_CI_CHECK
    # ------------------------------------------------------------------
    if ci_state in ("PENDING", "EXPECTED", "FAILURE"):
        return BlockedReason.MISSING_CI_CHECK

    # ------------------------------------------------------------------
    # 우선순위 5: PERMISSION_ISSUE
    # ------------------------------------------------------------------
    if not capability.bot_can_merge:
        return BlockedReason.PERMISSION_ISSUE

    # ------------------------------------------------------------------
    # 우선순위 6: AUTO_MERGE_UNSUPPORTED
    # ------------------------------------------------------------------
    if not capability.auto_merge_enabled:
        return BlockedReason.AUTO_MERGE_UNSUPPORTED

    # ------------------------------------------------------------------
    # 우선순위 7: BRANCH_PROTECTION (그 외 BLOCKED)
    # ------------------------------------------------------------------
    if merge_state == "BLOCKED":
        return BlockedReason.BRANCH_PROTECTION

    # 모두 false → None (정상 merge 가능)
    return None


# ---------------------------------------------------------------------------
# 8. select_merge_path
# ---------------------------------------------------------------------------

def select_merge_path(
    pr: dict,
    capability: RepositoryCapability,
    blocked_reason: Optional[BlockedReason],
) -> MergePathPlan:
    """BlockedReason → deterministic MergePathPlan 선택.

    회장 §명시: admin override 없이 deterministic. "회장 직접 머지 fallback" 없음.
    AUTOMATION_CAPABILITY_GAP은 ops 채널만 (requires_chair=False).
    """
    pr_num: int | str = pr.get("number", "?")
    bot_mergeable: bool = capability.bot_can_merge
    needs_approval: bool = capability.requires_approval

    if blocked_reason is None:
        return MergePathPlan(
            action="squash_merge",
            reason=None,
            requires_chair=False,
            capability_gap=False,
            triage_hook=None,
            base_sync_command=None,
            description=(
                f"PR #{pr_num}: 정상 squash merge path. 모든 capability 충족. "
                f"(bot_can_merge={bot_mergeable})"
            ),
        )

    if blocked_reason == BlockedReason.UNRESOLVED_REVIEW_THREAD:
        return MergePathPlan(
            action="auto_gemini_triage",
            reason=blocked_reason,
            requires_chair=False,
            capability_gap=False,
            triage_hook="auto_gemini_triage.triage_pr",
            base_sync_command=None,
            description=(
                f"PR #{pr_num}: Unresolved review thread 존재. "
                "auto_gemini_triage hook으로 자동 분류/resolve."
            ),
        )

    if blocked_reason == BlockedReason.STALE_BASE:
        return MergePathPlan(
            action="base_sync",
            reason=blocked_reason,
            requires_chair=False,
            capability_gap=False,
            triage_hook=None,
            base_sync_command="git merge origin/main",
            description=(
                f"PR #{pr_num}: Base branch가 stale. "
                "git merge origin/main으로 동기화. (force push 금지)"
            ),
        )

    if blocked_reason in (
        BlockedReason.REQUIRED_APPROVAL,
        BlockedReason.BRANCH_PROTECTION,
        BlockedReason.PERMISSION_ISSUE,
    ):
        return MergePathPlan(
            action="escalate_capability_gap",
            reason=blocked_reason,
            requires_chair=False,          # 회장 직접 머지 X — ops 채널만
            capability_gap=True,
            triage_hook=None,
            base_sync_command=None,
            description=(
                f"PR #{pr_num}: Capability gap 감지: {blocked_reason.value}. "
                f"requires_approval={needs_approval}, bot_can_merge={bot_mergeable}. "
                "자동화 불가 — ops 채널로 보고. (회장 직접 머지 fallback 없음)"
            ),
        )

    if blocked_reason == BlockedReason.MISSING_CI_CHECK:
        return MergePathPlan(
            action="wait_ci",
            reason=blocked_reason,
            requires_chair=False,
            capability_gap=False,
            triage_hook=None,
            base_sync_command=None,
            description=f"PR #{pr_num}: CI 체크 미완료/실패. CI 통과 대기.",
        )

    # BlockedReason.AUTO_MERGE_UNSUPPORTED — 마지막 case (7종 완전 처리)
    else:
        return MergePathPlan(
            action="manual_merge_required",
            reason=blocked_reason,
            requires_chair=False,
            capability_gap=True,
            triage_hook=None,
            base_sync_command=None,
            description=(
                f"PR #{pr_num}: Repo auto_merge 비활성화. "
                "manual merge 필요. ops 채널 보고."
            ),
        )


# ---------------------------------------------------------------------------
# 9. invoke_triage_hook — lazy import 인터페이스
# ---------------------------------------------------------------------------

def invoke_triage_hook(pr_number: int) -> dict:
    """auto_gemini_triage 연동 hook. 실제 wiring은 후속 task.

    본 task에서는 인터페이스만 정의. 호출 시 lazy import로 circular 방지.
    실제로 triage_pr을 호출하지 않고 callable만 반환 (테스트에서 인터페이스 검증).
    """
    from utils.auto_gemini_triage import triage_pr  # lazy import  # pyright: ignore[reportMissingImports]
    return {
        "hook": "auto_gemini_triage.triage_pr",
        "pr_number": pr_number,
        "callable": triage_pr,
    }


# ---------------------------------------------------------------------------
# 10. build_capability_gap_packet — automation_contracts 연동 헬퍼
# ---------------------------------------------------------------------------

def build_capability_gap_packet(
    plan: MergePathPlan,
    pr_number: int,
    task_id: str,
) -> "EscalationPacket | None":
    """capability_gap=True인 MergePathPlan을 EscalationPacket으로 변환.

    회장 §명시: AUTOMATION_CAPABILITY_GAP은 CriticalEscalationType 7종 외이므로
    Critical 보고 대상이 아님. 가장 가까운 기존 타입 사용 (ops 채널 marker만).
    RiskLevel은 plan.requires_chair 기반으로 결정 (HIGH vs MEDIUM).

    Returns:
        EscalationPacket (ops 채널용) or None (capability_gap=False인 경우)
    """
    if not plan.capability_gap:
        return None

    risk = RiskLevel.HIGH if plan.requires_chair else RiskLevel.MEDIUM
    return EscalationPacket(
        task_id=task_id,
        pr_number=pr_number,
        escalation_type=CriticalEscalationType.BLOCK_OVERRIDE_REQUIRED_OR_REASON_INSUFFICIENT,
        reason=plan.description,
        why_auto_cannot_continue=(
            f"capability_gap=True: action={plan.action}, "
            f"blocked_reason={plan.reason.value if plan.reason else 'None'}"
        ),
        safe_options=["ops_channel_report", "manual_merge_review"],
        recommended_option="ops_channel_report",
        evidence={"risk_level": risk.value, "action": plan.action},
    )


# ---------------------------------------------------------------------------
# 11. CLI helpers
# ---------------------------------------------------------------------------

def _extract_owner_repo_from_remote(
    *,
    runner: Optional[RunnerType] = None,
) -> tuple[str, str]:
    """git remote get-url origin에서 owner/repo 추출. 실패 시 기본값 반환."""
    fn = runner if runner is not None else _default_runner
    try:
        result = fn(["git", "remote", "get-url", "origin"], cwd=None)
        if result.returncode == 0:
            url = result.stdout.strip()
            # ssh: git@github.com:owner/repo.git
            # https: https://github.com/owner/repo.git
            import re
            m = re.search(r"[:/]([^/]+)/([^/]+?)(?:\.git)?$", url)
            if m:
                return m.group(1), m.group(2)
    except Exception:
        pass
    return "Jeon-Jonghyuk", "dev_workspace"


def _fetch_pr_info(
    pr_number: int,
    *,
    runner: Optional[RunnerType] = None,
) -> dict:
    """gh pr view로 PR 정보 조회."""
    fn = runner if runner is not None else _default_runner
    try:
        result = fn(
            [
                "gh", "pr", "view", str(pr_number),
                "--json",
                "mergeStateStatus,reviewThreads,reviewDecision,statusCheckRollup",
            ],
            cwd=None,
        )
        if result.returncode == 0 and result.stdout.strip():
            return json.loads(result.stdout)
    except Exception:
        pass
    return {}


def _build_parser() -> argparse.ArgumentParser:
    p = argparse.ArgumentParser(
        description=(
            "repository_policy_adapter — GitHub repository capability probe & "
            "deterministic merge path selector. (회장 §명시: admin override 항상 금지)"
        ),
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Examples:
  # capability probe (JSON 출력)
  python3 utils/repository_policy_adapter.py --probe-capability --owner Jeon-Jonghyuk --repo dev_workspace --branch main --json

  # PR 분석 (probe + classify + select_merge_path)
  python3 utils/repository_policy_adapter.py --pr 42 --json

  # PR 차단 이유만 분류
  python3 utils/repository_policy_adapter.py --pr 42 --classify-blocked

금지 사항 (회장 §명시):
  - admin override (--admin flag 정적 차단)
  - branch protection 우회 코드
  - canonical_workspace_resolver / automation_contracts 수정
  - 5 모듈 본체 수정
  - expected_files 외 파일 수정
  - AUTOMATION_CAPABILITY_GAP을 CriticalEscalationType에 추가
        """,
    )
    p.add_argument(
        "--probe-capability",
        action="store_true",
        help="Repository capability를 probe하여 출력",
    )
    p.add_argument("--owner", default=None, help="GitHub owner (미지정 시 remote에서 추출)")
    p.add_argument("--repo", default=None, help="GitHub repo (미지정 시 remote에서 추출)")
    p.add_argument("--branch", default="main", help="대상 branch (기본: main)")
    p.add_argument(
        "--pr",
        type=int,
        metavar="N",
        default=None,
        help="PR 번호. probe + classify + select_merge_path 실행",
    )
    p.add_argument(
        "--classify-blocked",
        action="store_true",
        help="--pr N과 함께 사용: 차단 이유만 출력",
    )
    p.add_argument("--json", action="store_true", dest="output_json", help="JSON 출력")
    return p


def _capability_to_dict(cap: RepositoryCapability) -> dict:
    return {
        "can_squash_merge": cap.can_squash_merge,
        "requires_approval": cap.requires_approval,
        "requires_thread_resolution": cap.requires_thread_resolution,
        "auto_merge_enabled": cap.auto_merge_enabled,
        "bot_can_merge": cap.bot_can_merge,
        "admin_override_required": cap.admin_override_required,
    }


def _plan_to_dict(plan: MergePathPlan) -> dict:
    return {
        "action": plan.action,
        "reason": plan.reason.value if plan.reason is not None else None,
        "requires_chair": plan.requires_chair,
        "capability_gap": plan.capability_gap,
        "triage_hook": plan.triage_hook,
        "base_sync_command": plan.base_sync_command,
        "description": plan.description,
    }


# ---------------------------------------------------------------------------
# 11. CLI entrypoint
# ---------------------------------------------------------------------------

if __name__ == "__main__":
    parser = _build_parser()
    args = parser.parse_args()

    # owner/repo 결정
    owner: str
    repo: str
    if args.owner and args.repo:
        owner, repo = args.owner, args.repo
    else:
        owner, repo = _extract_owner_repo_from_remote()
        if args.owner:
            owner = args.owner
        if args.repo:
            repo = args.repo

    # ------------------------------------------------------------------
    # --probe-capability
    # ------------------------------------------------------------------
    if args.probe_capability:
        try:
            cap = probe_capability(owner, repo, args.branch)
        except Exception as exc:
            print(f"ERROR: {exc}", file=sys.stderr)
            sys.exit(1)

        if args.output_json:
            cap_dict = _capability_to_dict(cap)
            # workspace_meta: resolve_canonical_workspace 결과 포함
            # (회장 §명시 CanonicalWorkspace 연동)
            try:
                # task_id는 현재 브랜치/디렉터리에서 추론 (probe 시점 best-effort)
                import re as _re
                _branch_guess = ""
                try:
                    _br = _default_runner(
                        ["git", "rev-parse", "--abbrev-ref", "HEAD"],
                        cwd=None,
                    )
                    if _br.returncode == 0:
                        _branch_guess = _br.stdout.strip()
                except Exception:
                    pass
                # branch 이름에서 task-NNNN 추출 (없으면 "probe" 사용)
                _task_match = _re.search(r"task-\d+(?:[+\-]\w+)?", _branch_guess)
                _inferred_task_id = _task_match.group(0) if _task_match else "probe"
                workspace = resolve_canonical_workspace(
                    _inferred_task_id,
                    fetch=False,
                )
                cap_dict["workspace_meta"] = {
                    "task_id": workspace.task_id,
                    "branch_name": workspace.branch_name,
                }
            except Exception:
                cap_dict["workspace_meta"] = None
            print(json.dumps(cap_dict, indent=2))
        else:
            for k, v in _capability_to_dict(cap).items():
                print(f"{k}: {v}")
        sys.exit(0)

    # ------------------------------------------------------------------
    # --pr N
    # ------------------------------------------------------------------
    if args.pr is not None:
        try:
            cap = probe_capability(owner, repo, args.branch)
            pr_info = _fetch_pr_info(args.pr)
            blocked = classify_blocked_reason(pr_info, cap)
            plan = select_merge_path(pr_info, cap, blocked)
        except Exception as exc:
            print(f"ERROR: {exc}", file=sys.stderr)
            sys.exit(1)

        if args.classify_blocked:
            # --classify-blocked: 차단 이유만 출력
            result = {
                "pr_number": args.pr,
                "blocked_reason": blocked.value if blocked is not None else None,
            }
            if args.output_json:
                print(json.dumps(result, indent=2))
            else:
                val = blocked.value if blocked is not None else "None (merge 가능)"
                print(f"pr #{args.pr} blocked_reason: {val}")
            sys.exit(0)

        # --json: probe + classify + select 통합 출력
        combined = {
            "pr_number": args.pr,
            "capability": _capability_to_dict(cap),
            "blocked_reason": blocked.value if blocked is not None else None,
            "merge_path_plan": _plan_to_dict(plan),
        }
        if args.output_json:
            print(json.dumps(combined, indent=2))
        else:
            print(f"pr              : #{args.pr}")
            print(f"blocked_reason  : {blocked.value if blocked else 'None'}")
            print(f"action          : {plan.action}")
            print(f"capability_gap  : {plan.capability_gap}")
            print(f"requires_chair  : {plan.requires_chair}")
            print(f"description     : {plan.description}")
        sys.exit(0)

    # 인수 없음
    parser.print_help()
    sys.exit(0)
