# Git Hooks Spec — task-2457 Phase 2-A

## 개요

본 문서는 `task-2457` Phase 2-A 산출물인 클라이언트 측 Git 훅(`pre-commit`, `pre-push`)의 동작 명세이다.
훅의 책임은 **start_task_guard / dispatch 거버넌스 흐름을 우회하는 임의 commit/push를 차단**하는 것이며, 서버측 정책(`auto_merge`, `taskctl`)을 보완한다.

- 위치: `scripts/git-hooks/{pre-commit,pre-push}`
- 설치: `scripts/install-git-hooks.sh` (core.hooksPath = scripts/git-hooks)
- 제거: `scripts/uninstall-git-hooks.sh`
- 정책 우회: `TASKCTL_BYPASS=1 TASKCTL_BYPASS_REASON="..."` (4필드 evidence 강제)

훅이 다루는 검증은 사후가 아닌 **사전 차단(pre-)** 이며, FAIL 시 exit 1로 commit/push 자체를 무효화한다.

---

## 7대 차단 케이스 (task-2457 task md 2장 요약)

아래 케이스는 task-2457 task md 2장에 정의된 차단 시나리오를 본 hook 구현으로 매핑한 표이다.
원문이 의도한 7가지 차단 시나리오는 다음과 같다.

| # | 차단 케이스 | 트리거 조건 | 담당 hook | 매핑된 검증 단계 | FAIL 메시지 |
|---|-------------|-------------|-----------|-----------------|-------------|
| 1 | main 직접 commit | `git rev-parse --abbrev-ref HEAD == main` 상태에서 `git commit` | pre-commit | 검증 2 | `[BLOCKED] main direct commit prohibited` |
| 2 | main 직접 push (현재 브랜치) | 현재 branch가 `main`인 채로 `git push` | pre-push | 검증 3 | `[BLOCKED] main direct push prohibited` |
| 3 | main 직접 push (refspec) | 다른 branch에서 `git push origin HEAD:main` 시도 | pre-push | 검증 2 | `[BLOCKED] main direct push prohibited (refspec=refs/heads/main)` |
| 4 | task 패턴 미준수 branch | `^task/(task-[0-9]+(\.[0-9]+)?)-` 정규식 매칭 실패 | pre-commit / pre-push | 검증 3 / 검증 4 | `[BLOCKED] branch does not match task pattern: task/task-N-bot` |
| 5 | start_task_guard 미통과 (lock 부재) | `.tasks/locks/<task-id>.lock` 파일 없음 | pre-commit / pre-push | 검증 4 / 검증 6 | `[BLOCKED] start_task_guard not passed: .tasks/locks/<task-id>.lock missing` |
| 6 | branch / lock task-id mismatch (mixed task) | branch에서 추출한 task-id ≠ lock 파일의 `task_id` | pre-commit / pre-push | 검증 5 / 검증 6 | `[BLOCKED] branch/lock task-id mismatch (branch=..., lock=...)` |
| 7 | scope 위반 push (allowed_resources 외 파일) | `git diff --name-only origin/main..HEAD` 결과 중 `memory/capabilities/<task-id>.json` 의 `allowed_resources.paths` 글롭과 매칭되지 않는 파일 존재 | pre-push | 검증 7 | `[BLOCKED] scope violation: <files>` |

추가로 hook은 다음 두 사이드 차단을 수행한다.

- cancelled marker 차단: `memory/events/<task-id>.cancelled` 존재 시 push 거부 (pre-push 검증 5)
- taskctl state CANCELLED 차단: best-effort로 `taskctl status --machine` 호출 결과 `current_state == CANCELLED` 시 거부 (pre-push 검증 9)

> **★ 커밋 메시지 fallback은 절대 수행하지 않는다.** Codex 리뷰 high 권고 사항으로,
> 커밋 메시지에서 `task-N`을 추출하는 방식은 위조 가능하므로 branch 정규식 추출만 신뢰원으로 인정한다.

### mixed task commit 감지에 관한 주석

순수한 "한 commit에 여러 task 파일이 섞이는" 감지는 commit-msg 단계에서만 정확히 가능하다 (pre-commit 시점에는 메시지 미확정).
본 Phase 2-A 구현에서는 별도 commit-msg hook을 두지 않으며, **검증 5 (branch/lock task-id 일치)** 가 mixed task의 1차 방어선 역할을 한다.
즉, 어떤 task의 worktree에서 commit이 일어나는 한, 그 worktree의 lock과 branch가 합치되어야 하므로 다른 task 흐름과의 혼입은 거버넌스 단계에서 분리된다.

---

## 3 PASS 케이스

| # | PASS 케이스 | 조건 |
|---|-------------|------|
| P1 | 정상 task commit | branch `task/task-N-<bot>`, lock 파일 존재 + task-id 일치 → `[OK] pre-commit guard PASS (task-id=task-N)` |
| P2 | 정상 task push (capability 있음) | 위 P1 조건 + capability snapshot의 `allowed_resources.paths` 글롭에 변경 파일이 모두 매칭 → `[OK] pre-push guard PASS (task-id=task-N, scope=PASS)` |
| P3 | 정상 task push (legacy grace) | capability snapshot 부재 시 scope 검사 SKIP, 그 외 전 검증 통과 → `[OK] pre-push guard PASS (task-id=task-N, scope=SKIPPED)` |

---

## Bypass 정책

긴급 hotfix·인프라 장애 등 거버넌스를 일시 우회해야 할 때 다음 환경변수로만 우회 허용한다.

```bash
TASKCTL_BYPASS=1 TASKCTL_BYPASS_REASON="hotfix: prod outage #1234" git commit -m "..."
```

### 규칙

- `TASKCTL_BYPASS=1` 만 설정하고 `TASKCTL_BYPASS_REASON`이 비어있거나 누락이면 가드 자체가 FAIL (`[BLOCKED] TASKCTL_BYPASS reason missing`)
- bypass 시 4필드 evidence를 atomic 기록한다.

### Evidence 포맷

- 경로: `.tasks/evidence/<task-id>/bypass-<UTC-ISO8601>.json`
- task-id 추출 실패 시 `unknown` 디렉토리 사용
- atomic 쓰기: `tmp + mv` (race 방지)

```json
{
  "bypass": true,
  "timestamp": "2026-05-05T08:30:00Z",
  "actor": "jay",
  "reason": "hotfix: prod outage #1234"
}
```

4필드: `bypass` (true), `timestamp` (UTC ISO8601), `actor` (`$USER`), `reason` (`$TASKCTL_BYPASS_REASON`).
이 evidence는 사후 회계(post-hoc audit) 용으로, 어떤 actor가 어떤 사유로 가드를 건너뛰었는지 영구 보존한다.

---

## FAIL 메시지 가이드

- 모든 FAIL은 `[BLOCKED] <reason>` 포맷으로 stderr에 출력한 뒤 `exit 1`
- 모든 PASS는 `[OK] <hook-name> guard PASS (task-id=<x>[, scope=<status>])` 로 stderr에 출력하고 `exit 0`
- bypass는 `[BYPASS] <hook-name> guard skipped (evidence=<path>)`로 출력
- exit code는 git이 commit/push를 abort하는 신호이므로 1로 고정

---

## Phase 2-D 인터페이스 명세 (taskctl_verify CLI)

Phase 2-D에서 작성될 `scripts/taskctl_verify.py`의 호출 계약을 사전 명세한다.

- **CLI**: `python3 scripts/taskctl_verify.py <task-id>`
- **exit code**:
  - `0` = 검증 PASS (push 허용)
  - `1` = 검증 FAIL (`[BLOCKED] taskctl_verify FAIL` 으로 push 차단)
- **호출 위치**: pre-push 검증 8
- **fallback**: 스크립트 부재 시 (Phase 2-A 현 상태) pre-push는 검증 6+7의 통과를 기준으로 PASS 판정하고,
  `.tasks/evidence/<task-id>/verify-fallback-<UTC-ISO8601>.json` 에 다음 evidence를 기록한다:

```json
{
  "taskctl_verify_status": "fallback",
  "reason": "taskctl_verify.py not present in Phase 2-A",
  "lock_check": "PASS",
  "scope_check": "PASS",
  "timestamp": "2026-05-05T08:30:00Z"
}
```

`scope_check` 값은 capability 부재 시 `SKIPPED`로 기록된다.

---

## 설치 / 제거 절차

### 설치

```bash
# worktree root에서 실행
bash scripts/install-git-hooks.sh
```

수행 동작:
1. `git config core.hooksPath scripts/git-hooks`
2. `chmod +x scripts/git-hooks/pre-commit scripts/git-hooks/pre-push`
3. 확인 메시지 출력

### 제거

```bash
bash scripts/uninstall-git-hooks.sh
```

수행 동작:
1. `git config --unset core.hooksPath`

> 주의: worktree마다 `core.hooksPath`는 별도 설정이 필요하다. 새 worktree를 만들면 install 스크립트를 재실행하라.
> 또한 mode 비트는 git에 의해 보존되지 않는 환경이 있을 수 있으므로 신규 clone 후에는 `git update-index --chmod=+x scripts/git-hooks/pre-commit scripts/git-hooks/pre-push` 로 +x 비트를 명시 commit 한다.

---

## 변경 이력

- 2026-05-05 (Phase 2-A): 루(dev3) 초기 작성. pre-commit / pre-push / install / uninstall 스크립트와 함께 작성.
