# taskctl-verify-spec.md — `scripts/taskctl_verify.py` 명세

> 버전: 1.0 | 작성일: 2026-05-05 | 작성자: 엔키 (dev5팀 백엔드)
> Task: task-2459 Phase 2-C
> 상태: 명세 (구현은 Phase 2-D 통합 시)

---

## 0. 문서 목적과 범위

본 문서는 **`scripts/taskctl_verify.py`** 의 동작을 명세한다.
이 도구는 `taskctl.py`(Phase 2-B)가 `start`, `commit`, `finish`, `merge` 등 상태 전이를
시도하기 직전·직후에 호출되어 **작업 환경의 정합성**을 11개 검사로 검증하고,
`evidence` JSON과 `exit code`로 결과를 보고하는 **subprocess 가드**다.

본 문서는 다음을 다룬다.

- 11 검사 항목의 정의·의미·임계치
- evidence JSON 포맷
- exit code 규약
- CLI 인터페이스
- Phase 2-D Integration (`taskctl.py` ↔ `taskctl_verify.py`) 호출 계약
- ★ **자동 복구 금지 정책**

본 문서는 **명세만** 다루며, 구현(코드)은 Phase 2-D 통합 단계에서 작성한다.
구현자(Phase 2-D owner)는 본 문서를 단일 진실원으로 삼는다.

---

## 1. 배경 (Why)

### 1.1 task-2452 silent corruption의 재현 가능성

task-2452 사고 이후 다음 가드가 차례로 도입되었다.

| 단계 | 가드 | 담당 |
|---|---|---|
| 작업 시작 (Start) | `start_task_guard.py` | task-2454 / Phase 1 |
| 머지 전 검증 | GitHub Required Checks | task-2440 |
| 상태 전이 enforcement | `taskctl.py` | task-2449 / Phase 2-B |
| 커밋/푸시 직전 | git hooks | Phase 2-A |

그러나 **상태 전이 직전(혹은 직후) 작업 환경이 여전히 깨끗한가** 를 확인하는
일반화된 검증 게이트가 없었다. start_task_guard는 작업 시작 시 1회만 동작하며,
그 이후 봇이 worktree를 떠나거나, 다른 task의 commit이 섞이거나,
`.cancelled` 마커가 떨어졌을 때 이를 감지할 수 있는 코드가 부재했다.

### 1.2 본 도구가 갖는 위치

`taskctl_verify.py`는 **상태 전이마다 반복 호출 가능한 stateless 검증기**다.

```
┌──────────────┐    subprocess     ┌────────────────────┐
│  taskctl.py  │ ───────────────▶  │ taskctl_verify.py  │
│ (state mgr)  │ ◀───────────────  │  (11 checks)       │
└──────────────┘   exit / evidence └────────────────────┘
                                            │
                                            ▼
                              .tasks/evidence/<task>/verify-*.json
```

- `taskctl.py`는 **호출자**다. 결과를 받아 분기한다.
- `taskctl_verify.py`는 **순수 검사기**다. 어떠한 상태 변경·복구 시도도 하지 않는다.
- 두 도구의 분리는 의도적이다. taskctl은 transition을 적용하지만, verify는 진단만 한다.

### 1.3 자동 복구 금지의 이유

task-2452 사고의 사후 분석에서 도출된 합의:

> "스크립트가 git history를 자동 조작하면 silent corruption을 detect 할 수 없다.
> rebase·cherry-pick·LLM 보정은 사람·아누만이 수행한다."

따라서 `taskctl_verify.py`는 **읽기 전용**이다.
실패를 감지하면 evidence를 남기고 0이 아닌 exit code로 보고만 한다.
복구는 회장/아누의 수동 결정으로만 이루어진다.

---

## 2. 11개 검사 항목 (What)

각 항목은 **상태**(PASS / FAIL / WARN / N/A)를 갖는다.

| #  | 검사 ID            | 카테고리 | 의미                          | FAIL 조건                                                         | N/A 조건       |
|----|--------------------|----------|-------------------------------|-------------------------------------------------------------------|---------------|
| 1  | `start_lock`       | 필수     | start_task_guard가 lock 생성? | `.tasks/locks/<task-id>.lock` 부재                                | 없음          |
| 2  | `branch_match`     | 필수     | 브랜치 ≡ task/<id>-<bot>       | 현재 브랜치 ≠ 기대 브랜치                                          | 없음          |
| 3  | `worktree_path`    | 필수     | cwd가 worktree 안?            | cwd가 `.worktrees/<id>-<bot>`로 끝나지 않음                        | 없음          |
| 4  | `cancelled_check`  | 필수     | cancel 마커 부재?             | `memory/events/<id>.cancelled` 존재                                | 없음          |
| 5  | `dirty_tree`       | 필수     | working tree clean?           | untracked + modified ≥ 1                                          | 없음          |
| 6  | `changed_paths`    | 정보     | 변경 경로 추출                | (단독 FAIL 없음 — 항상 PASS)                                       | 없음          |
| 7  | `scope_matrix`     | 필수     | 변경이 allowed_resources 내?  | scope 위반 commit/path ≥ 1                                         | 없음          |
| 8  | `handoff_chain`    | 조건부   | handoff 무결성                | handoff JSON 있고 schema invalid 또는 previous_bot 추적 불가      | handoff 없음  |
| 9  | `mixed_commit`     | 필수     | 다른 task commit 섞임 여부    | `mixed_commit_detector.py` exit 1                                  | 없음          |
| 10 | `qc_report_guard`  | 옵션     | QC 보고서 verdict             | qc_verdict == FAIL                                                 | 보고서 없음   |
| 11 | `guard_sh`         | 옵션     | repo guard.sh 통과?           | `guard.sh` 존재하고 exit ≠ 0                                       | guard.sh 부재 |

### 2.1 `start_lock` — start_task_guard 호출 흔적

**의미:** `start_task_guard.py`(task-2454)가 작업 시작 시 lock 파일을 생성한다.
lock 파일이 부재하면 **봇이 가드를 우회하고 직접 작업 시작**한 silent corruption 시나리오로 간주.

**임계치:**

- PASS: `<workspace_root>/.tasks/locks/<task-id>.lock` 존재
- FAIL: 부재
- (N/A 없음 — 모든 task는 lock이 있어야 함)

**evidence detail 항목:** `lock_path`(절대경로), `lock_mtime`(ISO8601, lock 존재 시).

### 2.2 `branch_match` — 브랜치 정합성

**의미:** 현재 브랜치가 기대된 task 브랜치와 일치하는가.

**기대 브랜치 형식:**

```
task/<task-id>-<bot>
```

**임계치:**

- `--bot <bot>` 옵션이 주어진 경우: 정확히 `task/<id>-<bot>` 와 일치해야 PASS, 아니면 FAIL
- `--bot` 미지정 시: `task/<id>-` 로 시작하는 브랜치면 PASS, 아니면 FAIL
- `HEAD` 가 detached 상태면 FAIL

**evidence detail 항목:** `current_branch`, `expected_branch`, `bot_hint`.

### 2.3 `worktree_path` — 메인 워크스페이스 차단

**의미:** 봇이 메인 워크스페이스 `/home/jay/workspace/`에서 직접 작업하지 않도록 강제.

**임계치:**

- PASS: cwd 가 `.worktrees/<task-id>-<bot>` 디렉토리거나 그 하위
- FAIL: cwd 가 메인 워크스페이스 루트 또는 다른 worktree

**evidence detail 항목:** `cwd`, `expected_worktree_pattern`.

### 2.4 `cancelled_check` — 취소 마커 부재

**의미:** task가 진행 중에 회장/아누에 의해 취소된 경우, `memory/events/<id>.cancelled` 마커가
생성된다. 이 마커가 있으면 더 이상 commit/finish/merge를 진행해서는 안 된다.

**임계치:**

- PASS: 마커 부재
- FAIL: 마커 존재 (cwd, worktree 양쪽 모두 검사 — 둘 중 하나라도 있으면 FAIL)

**evidence detail 항목:** `cancelled_marker_path`(존재 시), `cancelled_at`(파일 mtime).

### 2.5 `dirty_tree` — 깨끗한 작업 트리

**의미:** verify 시점에 untracked·modified 파일이 없어야 한다.
이는 `commit`/`finish` 등 전이 직전에는 항상 깨끗한 상태여야 한다는 invariant.

**임계치:**

- PASS: `git status --porcelain` 출력 0줄
- FAIL: 1줄 이상

**evidence detail 항목:** `dirty_files`(리스트, 최대 50개로 truncate).

### 2.6 `changed_paths` — 변경 경로 수집 (정보용)

**의미:** `git diff --name-only origin/main..HEAD` 결과를 수집한다.
**단독 FAIL 없음** — 검사 7(scope_matrix)의 입력으로 사용되며, 본 항목은 항상 PASS.

**임계치:**

- 항상 PASS (origin/main..HEAD가 비어 있어도 빈 리스트로 PASS)

**evidence detail 항목:** `changed_paths_list`(전체 리스트, 정렬됨, dedup).

### 2.7 `scope_matrix` — 허용 경로 매트릭스 검증

**의미:** 변경된 경로가 task 파일(`memory/tasks/task-<id>.md`)의 `allowed_resources.paths`
패턴 중 하나와 매치되어야 한다.

**임계치:**

- PASS: 모든 changed_paths 가 적어도 하나의 패턴에 매치
- FAIL: 하나 이상의 path 가 어떤 패턴에도 매치되지 않음

**임계치 세부:**

- 패턴 매칭은 `fnmatch.fnmatchcase` 또는 동등한 glob (`**` 미지원이면 명세에 명시)
- task 파일 자체가 없으면 **FAIL** (task-id 부정확 또는 메타데이터 누락 — 차단)
- task 파일 읽기 실패 (권한/IO 오류 등) → **FAIL** (안전 측 차단)
- `allowed_resources.paths` 가 빈 리스트 → **FAIL** (스펙 위반 — 차단)
- task 파일에 `allowed_resources` 블록 자체가 없으면 → **WARN** (구버전 task md 호환)
  - 구버전 task md 가 점진 마이그레이션 중인 환경에서 차단으로 인한 운영 마비를 막기 위함.
  - 마이그레이션이 완료되면 본 절을 FAIL 로 격상할 수 있다 (회장 승인 + spec 새 버전).

**evidence detail 항목:** `allowed_patterns`(리스트), `scope_violations`(violating path 리스트).

### 2.8 `handoff_chain` — handoff 무결성 (조건부)

**의미:** `memory/handoffs/<task-id>.json` 이 존재하면 schema valid 여야 하고,
`previous_bot`(= JSON 의 `from_bot` 필드) 가 가리키는 봇이 추적 가능해야 한다.

**필수 필드 (HANDOFF_REQUIRED_FIELDS):**

- `handoff_id`, `from_bot`, `to_bot`, `task_id`, `timestamp`
- 키 부재 또는 빈 값(falsy) 모두 누락으로 간주.

**임계치:**

- N/A: handoff JSON 부재 (단독 봇 작업)
- PASS:
  - JSON 파싱 성공
  - 모든 필수 필드 존재 + 비어있지 않음
  - `task_id` 가 인자 task_id 와 일치
  - `from_bot` (= previous_bot) 이 비어있지 않아 추적 가능
- FAIL:
  - JSON 파싱 실패
  - 필수 필드 누락 또는 빈 값
  - `task_id` mismatch (handoff JSON 의 task_id ≠ 인자)
  - `from_bot` 비어있어 previous_bot 추적 불가

**evidence detail 항목:** `handoff_path`, `previous_bot`, `handoff_schema_errors`(있을 시).

### 2.9 `mixed_commit` — 타 task commit 혼입 검사

**의미:** `scripts/mixed_commit_detector.py <task-id>` subprocess 를 호출하여
exit code 로 결과를 받는다. 알고리즘 상세는 본 spec과 짝지어진
`mixed-commit-detector-spec.md` 참조.

**임계치:**

- PASS: detector exit 0
- FAIL: detector exit 1 (alien commit 감지)
- FAIL: detector 부재 — `scripts/mixed_commit_detector.py` 가 없으면 안전 검증 자체가
  불가능하므로 **차단**. (구버전 fallback 없음.)
- FAIL: detector exit 2 (internal_error) 또는 그 외 — 안전 측 차단
- FAIL: subprocess timeout 또는 OS 실행 오류 — 안전 측 차단

`taskctl_verify.py` 는 detector subprocess 호출 시 다음 인자를 명시한다:

- `--workspace <workspace>`: evidence/freeze 저장 경로
- `--git-dir <cwd>`: git 조회 cwd (task worktree). workspace 와 분리 필수
  (운영 환경의 main repo root 와 task worktree 가 다른 경로이기 때문).

**evidence detail 항목:** `mixed_commit_detector_exit`, `mixed_commit_evidence_path`(detector 가
저장한 evidence 경로, 있을 시).

### 2.10 `qc_report_guard` — QC 보고서 검증 (옵션)

**의미:** `memory/reports/<task-id>.md` 가 존재하면 본문에서 `qc_verdict` 를 추출하여 검사.

**임계치:**

- N/A: 보고서 부재 (RUNNING 상태 직후 등 보고서 작성 전)
- PASS: `qc_verdict: PASS` 또는 `qc_verdict: WARN`
- FAIL: `qc_verdict: FAIL`
- (verdict 라인을 파싱하지 못하면 WARN — 보고서 형식 문제는 차단보다는 경고)

**파싱 규칙:** 정규식 `^qc_verdict:\s*(PASS|WARN|FAIL)\s*$` 가 본문 어딘가에 매치.
첫 번째 매치만 사용.

**evidence detail 항목:** `qc_report_path`, `qc_verdict_value`.

### 2.11 `guard_sh` — repo guard.sh 통과 (옵션)

**의미:** repo 에 `scripts/guard.sh` 가 존재하면 호출하여 exit code 검사.

**임계치:**

- N/A: `scripts/guard.sh` 부재
- PASS: exit 0
- FAIL: exit ≠ 0
- (timeout 60초 — 초과 시 FAIL)

**evidence detail 항목:** `guard_sh_exit`, `guard_sh_stdout_tail`(마지막 20줄), `guard_sh_stderr_tail`.

---

## 3. evidence JSON 포맷

### 3.1 저장 위치

```
<workspace_root>/.tasks/evidence/<task-id>/verify-<YYYYMMDDTHHMMSSZ>.json
```

- `<workspace_root>`: `WORKSPACE_ROOT` 환경변수 또는 기본값 `/home/jay/workspace`
- `<task-id>`: `task-XXXX` 형식
- 타임스탬프: UTC ISO8601 compact (`20260505T180312Z`)

같은 task에 대해 여러번 호출 시 누적된다 (덮어쓰지 않음).

### 3.2 스키마

```json
{
  "task_id": "task-2459",
  "verified_at": "2026-05-05T18:03:12Z",
  "verifier_version": "1.0",
  "cwd": "/home/jay/workspace/.worktrees/task-2459-enki",
  "bot_hint": "enki",
  "results": {
    "start_lock": "PASS",
    "branch_match": "PASS",
    "worktree_path": "PASS",
    "cancelled_check": "PASS",
    "dirty_tree": "PASS",
    "changed_paths": "PASS",
    "scope_matrix": "PASS",
    "handoff_chain": "PASS",
    "mixed_commit": "PASS",
    "qc_report_guard": "N/A",
    "guard_sh": "PASS"
  },
  "details": {
    "lock_path": "/home/jay/workspace/.tasks/locks/task-2459.lock",
    "lock_mtime": "2026-05-05T17:50:01Z",
    "current_branch": "task/2459-enki",
    "expected_branch": "task/2459-enki",
    "cwd_match_pattern": ".worktrees/task-2459-enki",
    "cancelled_marker_path": null,
    "dirty_files": [],
    "changed_paths_list": [
      "memory/specs/taskctl-verify-spec.md",
      "memory/specs/mixed-commit-detector-spec.md"
    ],
    "allowed_patterns": [
      "memory/specs/taskctl-verify-spec.md",
      "memory/specs/mixed-commit-detector-spec.md"
    ],
    "scope_violations": [],
    "handoff_path": null,
    "previous_bot": null,
    "mixed_commit_detector_exit": 0,
    "mixed_commit_evidence_path": null,
    "qc_report_path": null,
    "qc_verdict_value": null,
    "guard_sh_exit": 0,
    "fail_reasons": []
  },
  "fail_reasons": [],
  "overall": "PASS",
  "exit_code": 0
}
```

★ **top-level `fail_reasons`** (필수): 모든 FAIL 사유를 사람이 읽을 메시지 리스트로
최상위에 노출한다. 호환성을 위해 동일 리스트가 `details.fail_reasons` 에도 그대로
유지된다. 소비자는 가능하면 top-level 을 우선 사용하고, 구버전 evidence 호환을 위해
`details.fail_reasons` 도 fallback 으로 읽을 수 있어야 한다.

### 3.3 details 필수 키

다음 키는 **항상** details 에 존재해야 한다 (값이 null 일 수 있음):

- `lock_path`, `lock_mtime`
- `current_branch`, `expected_branch`
- `cwd`, `cwd_match_pattern`
- `cancelled_marker_path`
- `dirty_files`(list)
- `changed_paths_list`(list)
- `allowed_patterns`(list), `scope_violations`(list)
- `handoff_path`, `previous_bot`
- `mixed_commit_detector_exit`, `mixed_commit_evidence_path`
- `qc_report_path`, `qc_verdict_value`
- `guard_sh_exit`
- `fail_reasons`(list of strings — 사람이 읽을 메시지)

### 3.4 FAIL 예시 (scope 위반 + dirty)

```json
{
  "task_id": "task-2459",
  "verified_at": "2026-05-05T18:10:44Z",
  "results": {
    "start_lock": "PASS",
    "branch_match": "PASS",
    "worktree_path": "PASS",
    "cancelled_check": "PASS",
    "dirty_tree": "FAIL",
    "changed_paths": "PASS",
    "scope_matrix": "FAIL",
    "handoff_chain": "N/A",
    "mixed_commit": "PASS",
    "qc_report_guard": "N/A",
    "guard_sh": "N/A"
  },
  "details": {
    "dirty_files": ["scripts/random_helper.py"],
    "changed_paths_list": [
      "memory/specs/taskctl-verify-spec.md",
      "scripts/random_helper.py"
    ],
    "allowed_patterns": [
      "memory/specs/taskctl-verify-spec.md",
      "memory/specs/mixed-commit-detector-spec.md"
    ],
    "scope_violations": ["scripts/random_helper.py"],
    "fail_reasons": [
      "dirty_tree: 1 untracked or modified file(s) detected",
      "scope_matrix: 1 path(s) outside allowed_resources"
    ]
  },
  "overall": "FAIL",
  "exit_code": 1
}
```

---

## 4. exit code 규약

| 코드 | 의미 | 조건                                                              |
|------|------|--------------------------------------------------------------------|
| 0    | PASS | 모든 검사가 PASS 또는 N/A                                          |
| 1    | FAIL | 1건 이상의 검사가 FAIL                                             |
| 2    | WARN | FAIL 0건, WARN 1건 이상 (옵션 검사의 N/A는 WARN으로 처리하지 않음) |

### 4.1 우선순위

1. FAIL 이 하나라도 있으면 → exit 1 (`overall: "FAIL"`)
2. FAIL 0 + WARN 1 이상 → exit 2 (`overall: "WARN"`)
3. 그 외 (전부 PASS or N/A) → exit 0 (`overall: "PASS"`)

### 4.2 WARN 의 발생 조건

본 spec의 검사 11개 중 **WARN 을 명시적으로 반환할 수 있는** 검사:

- `qc_report_guard`: 보고서 형식 파싱 실패 시 WARN
- (다른 검사들은 PASS / FAIL / N/A 만 반환)

옵션 검사가 N/A 인 경우(예: handoff 없음, qc 보고서 없음)는 WARN 으로 격상하지 **않는다**.

### 4.3 internal error

verify 자체가 실패한 경우 (subprocess 시간 초과, 권한 문제, JSON 직렬화 오류 등)
exit code **3** 으로 별도 보고. evidence 는 가능한 한 부분 저장.
호출자 `taskctl.py` 는 exit 3 을 "verify 자체 실패" 로 처리하고 차단.

---

## 5. CLI 인터페이스

### 5.1 기본 호출

```
python3 scripts/taskctl_verify.py <task-id>
```

- task 정보 + worktree(cwd) 정보 + 11개 검사
- evidence 저장: `.tasks/evidence/<task-id>/verify-<ts>.json`
- stdout: 단축 JSON 요약 (overall, results, exit_code)
- exit code: 위 규약대로

### 5.2 `--bot <bot>` (옵션)

```
python3 scripts/taskctl_verify.py task-2459 --bot enki
```

- `branch_match` 검사 정확도 향상 (기대 브랜치 = `task/<id>-<bot>`)
- 미지정 시 `task/<id>-` prefix 매칭만 검사

### 5.3 `--json` (옵션, dry-run 효과)

```
python3 scripts/taskctl_verify.py task-2459 --json
```

- evidence 파일 **저장하지 않음**
- stdout 에 전체 evidence JSON 출력 (`details` 포함)
- exit code 는 동일하게 보고
- 사용 사례: CI 미리보기, 디버깅, taskctl이 evidence 별도 저장하고 싶지 않을 때

### 5.4 `--workspace <path>` (옵션)

워크스페이스 루트를 명시. 기본값: `WORKSPACE_ROOT` env 또는 `/home/jay/workspace`.

### 5.5 `--quiet` (옵션)

stdout 출력 억제 (exit code 만 사용). evidence 저장은 유지.

### 5.6 종료 후 출력 예시

```
$ python3 scripts/taskctl_verify.py task-2459 --bot enki
{"task_id":"task-2459","overall":"PASS","exit_code":0,"results":{...},"evidence":".tasks/evidence/task-2459/verify-20260505T180312Z.json"}
$ echo $?
0
```

---

## 6. Phase 2-D Integration 인터페이스

### 6.1 호출 계약

`taskctl.py` 가 다음 시점에 `taskctl_verify.py` 를 subprocess 호출:

| taskctl 명령      | verify 호출 시점        | exit 처리                                  |
|-------------------|-------------------------|--------------------------------------------|
| `taskctl start`   | start_task_guard 직후   | FAIL 시 start 차단                          |
| `taskctl commit`  | git commit 직전          | FAIL 시 commit 차단, WARN 시 회장 승인 필요 |
| `taskctl finish`  | 보고서/PR 생성 직전      | FAIL 시 finish 차단                         |
| `taskctl merge`   | 병합 직전 최종 sanity    | FAIL 시 merge 차단                          |

### 6.2 호출 코드 예시 (Phase 2-D 구현 참고)

```python
import subprocess
import json
from pathlib import Path

def run_verify(task_id: str, bot: str | None = None) -> dict:
    """taskctl_verify.py 호출 후 evidence 파싱."""
    cmd = ["python3", "scripts/taskctl_verify.py", task_id]
    if bot:
        cmd += ["--bot", bot]
    result = subprocess.run(cmd, capture_output=True, text=True, timeout=180)
    summary = json.loads(result.stdout)

    if result.returncode == 0:
        return {"verdict": "PASS", "summary": summary}
    if result.returncode == 1:
        # evidence 파일 읽어 상세 분석
        ev_path = Path(summary["evidence"])
        evidence = json.loads(ev_path.read_text())
        return {"verdict": "FAIL", "summary": summary, "evidence": evidence}
    if result.returncode == 2:
        return {"verdict": "WARN", "summary": summary}
    # 3 = internal error
    return {"verdict": "ERROR", "stderr": result.stderr}


def transition_with_verify(task_id: str, bot: str, target_state: str) -> None:
    """상태 전이 직전 verify 실행."""
    v = run_verify(task_id, bot=bot)

    if v["verdict"] == "PASS":
        apply_transition(task_id, target_state)
        return
    if v["verdict"] == "WARN":
        # 회장 승인 마커 필요
        if not human_approval_marker_exists(task_id, target_state):
            print(f"[BLOCKED] verify WARN — 회장 승인 마커 필요")
            sys.exit(1)
        apply_transition(task_id, target_state)
        return
    if v["verdict"] == "FAIL":
        # ★ 자동 복구 금지 — 사람이 처리
        print(f"[BLOCKED] verify FAIL — 수동 처리 필요")
        print(f"  evidence: {v['summary'].get('evidence')}")
        sys.exit(1)
    if v["verdict"] == "ERROR":
        print(f"[BLOCKED] verify internal error", file=sys.stderr)
        sys.exit(1)
```

### 6.3 evidence 파일 활용

`taskctl.py` 는 verify FAIL 시 evidence 파일을 읽어:

- `details.fail_reasons` 를 사용자 친화적 메시지로 출력
- `details.scope_violations`, `details.dirty_files` 등을 작업 가이드로 변환
- evidence 경로를 dispatch 보고/inbox 메시지에 첨부

### 6.4 호출 빈도 / 캐시

verify는 stateless 이므로 매 전이마다 새로 호출한다. 캐시 없음.
evidence 파일은 누적된다 — 디스크 사용량은 운영 모니터링 대상.
회장이 별도 요청 시 오래된 evidence 정리 정책 도입 가능 (본 spec 범위 외).

### 6.5 부트스트랩 순서 (Phase 2-D 구현 시 주의)

1. `taskctl.py` 는 verify 호출 전에 자체적으로 task state 파일을 읽어
   현재 상태를 안다. verify 는 state 모름.
2. verify 결과 FAIL 시 taskctl 은 state 를 변경하지 **않는다**.
3. verify 가 evidence 저장에 실패해도(예: 디스크 가득) taskctl 은 안전 측으로 차단.

---

## 7. ★ 자동 복구 금지 정책

### 7.1 명문화

`taskctl_verify.py` 는 다음 행위를 **절대** 수행하지 않는다.

| 금지 행위                       | 사유                                                       |
|---------------------------------|------------------------------------------------------------|
| `git rebase` / `git rebase -i`  | history 자동 변경은 silent corruption 마스킹               |
| `git cherry-pick`               | 자동 cherry-pick 으로 commit 분리 시도 금지                |
| `git reset` (모든 형태)         | working tree·index·HEAD 자동 변경 금지                     |
| `git checkout -- <path>`        | 파일 자동 복원 금지                                        |
| `git stash` (apply 포함)        | 더러움(dirty)을 자동 숨기는 행위 금지                      |
| `git commit` / `git commit --amend` | verify는 read-only — 어떤 형태의 commit도 생성 금지     |
| `git push` / `git push --force` | 원격 변경 금지                                             |
| `.cancelled` 마커 자동 제거     | 회장/아누만 제거                                           |
| `.frozen` lock 자동 제거        | mixed-commit-detector spec 정책에 따라 회장/아누만 제거    |
| LLM API 호출 (Anthropic/GLM 등) | 결정의 자동화 금지 — verify 는 deterministic               |
| 외부 네트워크 호출              | git fetch 포함 — 시간 지연·실패 가능성 차단                |

### 7.2 verify 가 호출하는 외부 명령

다음 명령만 호출 허용:

- `git rev-parse`, `git symbolic-ref`, `git status --porcelain`,
  `git diff --name-only origin/main..HEAD`, `git log` (read-only)
- `python3 scripts/mixed_commit_detector.py <id>` (subprocess)
- `bash scripts/guard.sh` (있을 시, subprocess, read-only 가정)

`git fetch`, `git pull`, `git push` 등 네트워크·history-mutating 명령 금지.

### 7.3 실패 시 운영 절차

verify FAIL 시 사용자/봇이 수행할 절차:

1. evidence JSON 의 `fail_reasons` 확인
2. 검사별 운영 가이드:
   - `start_lock` FAIL → `start_task_guard.py` 부터 다시 시작
   - `branch_match` FAIL → 수동 `git checkout`, 단 메인 워크스페이스 작업이면 worktree 재생성
   - `worktree_path` FAIL → 메인에서 작업 중지, worktree로 이동
   - `cancelled_check` FAIL → 작업 중단, 회장 보고
   - `dirty_tree` FAIL → 수동 commit / stash / 정리 (스크립트 자동화 금지)
   - `scope_matrix` FAIL → 위반 파일을 task 범위로 옮기거나 task 분할 (회장 승인)
   - `handoff_chain` FAIL → handoff JSON 수동 점검
   - `mixed_commit` FAIL → `mixed-commit-detector-spec` 운영 절차 따라 회장/아누 처리
   - `qc_report_guard` FAIL → qc 항목 수정 후 보고서 재작성
   - `guard_sh` FAIL → guard.sh 출력 분석 후 수동 수정

### 7.4 정책 변경 시

본 spec은 **자동 복구 금지** 를 절대 기준으로 한다.
이를 변경하려면 회장 직접 승인 + 새 spec 버전 발행이 필요하다.
구현(Phase 2-D)에서 본 정책에 어긋나는 코드를 발견하면 즉시 reject.

---

## 8. 테스트 케이스 (Phase 2-D 구현용 참고)

본 명세를 구현·검증할 때 다음 시나리오가 모두 통과해야 한다.

| #  | 시나리오                                  | 기대 결과                                       |
|----|-------------------------------------------|------------------------------------------------|
| T1 | 정상 worktree, 변경 없음                  | overall PASS, exit 0                            |
| T2 | 정상 worktree, 범위 내 변경 1개           | PASS, exit 0                                    |
| T3 | 메인 워크스페이스에서 호출                | worktree_path FAIL, exit 1                      |
| T4 | 브랜치가 main / 다른 task                  | branch_match FAIL, exit 1                       |
| T5 | lock 부재                                  | start_lock FAIL, exit 1                         |
| T6 | dirty tree (untracked 1개)                | dirty_tree FAIL, exit 1                         |
| T7 | scope 위반 path 1개                       | scope_matrix FAIL, exit 1                       |
| T8 | mixed commit detector exit 1              | mixed_commit FAIL, exit 1                       |
| T9 | qc 보고서 verdict FAIL                    | qc_report_guard FAIL, exit 1                    |
| T10| guard.sh 부재                              | guard_sh N/A, 다른 검사 PASS면 exit 0           |
| T11| handoff JSON schema invalid               | handoff_chain FAIL, exit 1                      |
| T12| `.cancelled` 마커 존재                    | cancelled_check FAIL, exit 1                    |
| T13| `--json` 옵션, 모두 PASS                   | stdout JSON, evidence 파일 없음, exit 0         |
| T14| qc 보고서 verdict 파싱 실패               | qc_report_guard WARN, FAIL 없음 → exit 2        |

---

## 9. 변경 이력

| 버전 | 일자       | 작성자 | 변경                          |
|------|------------|--------|-------------------------------|
| 1.0  | 2026-05-05 | 엔키   | 초안 작성 (task-2459 Phase 2-C) |

---

## 10. 부록 — 11 검사 요약 카드

```
┌─────────────────────────────────────────────────────────────────────┐
│ taskctl_verify.py — 11 checks                                       │
├──┬─────────────────────┬──────────────────────────────────────────────┤
│ 1│ start_lock          │ .tasks/locks/<id>.lock 존재                 │
│ 2│ branch_match        │ HEAD ≡ task/<id>-<bot>                      │
│ 3│ worktree_path       │ cwd ⊂ .worktrees/<id>-<bot>                  │
│ 4│ cancelled_check     │ memory/events/<id>.cancelled 부재            │
│ 5│ dirty_tree          │ git status 0줄                               │
│ 6│ changed_paths       │ (정보) origin/main..HEAD diff                │
│ 7│ scope_matrix        │ changed ⊂ allowed_resources.paths            │
│ 8│ handoff_chain       │ handoff JSON valid (있을 시)                 │
│ 9│ mixed_commit        │ mixed_commit_detector exit 0                  │
│10│ qc_report_guard     │ qc_verdict ∈ {PASS, WARN}                    │
│11│ guard_sh            │ scripts/guard.sh exit 0 (있을 시)             │
└──┴─────────────────────┴──────────────────────────────────────────────┘
exit: 0=PASS, 1=FAIL, 2=WARN, 3=internal_error
정책: 자동 복구 금지 (rebase / cherry-pick / LLM 호출 0줄)
```
