# GOAL-GATE Placeholder Hardening 설계 (task-2729+17)

작성일: 2026-06-08
작성자: 스바로그 (개발6팀 백엔드)
대상 파일: `scripts/finish-task.sh` (worktree: task-2729+17-dev6)

---

## 문제

`finish-task.sh`의 GOAL-GATE(섹션 2.12)는 task md의 `## goal_assertions` 블록에서 backtick으로 감싸진 명령을 추출해 `eval`로 직접 실행한다.

자동생성된 placeholder 명령이 포함될 경우:

- 미확장 `$QC_SCRIPT` → `python3 $QC_SCRIPT --gate --task-id ...` 형태로 추출
- `eval "python3 $QC_SCRIPT --gate --task-id ..."` → 쉘 변수 미정의 시 `python3 --gate --task-id ...`로 해석
- 또는 실제로 `dispatch.py`나 유사 스크립트가 `--gate` 모드로 실행되어 **무한 행(hang)** 발생

결과: finalize 프로세스가 정지 → task-2729+14 / +15 / +16 의 callback miss 직접 원인.

---

## 해결 (B안: finish-task.sh GOAL-GATE 실행부 단독 수정)

생성측(dispatch.py) 무수정. `finish-task.sh` 내 GOAL-GATE 실행 루프만 강화.

### (1) placeholder default-deny 탐지 3종 — 실행 전 차단

`goal_assertion_eval()` 함수 내부에서 명령 실행 전 검사:

| 패턴 | 예시 | 탐지 방법 |
|------|------|-----------|
| 미확장 `$VAR` / `${VAR}` | `python3 $QC_SCRIPT ...` | `grep -qE '\$\{?[A-Za-z_]'` |
| 리터럴 `...` (3점) | `cat ...` | `case "$cmd" in *'...'*)` |
| `<...>` 꺾쇠 placeholder | `grep <foo> x` | `grep -qE '<[^>]+'` |

위 3가지 중 하나라도 해당하면 `SKIP_PLACEHOLDER` 반환 → 실행 생략.

### (2) timeout guard — hang fail-closed

```bash
timeout "${tmo}s" bash -c "$cmd" > /dev/null 2>&1
```

- 기본값: `GOAL_CMD_TIMEOUT=30` (환경변수로 외부 조정 가능)
- exit code 124 → `TIMEOUT` 반환 → 무한 행 방지

### (3) 4-state 판정

| 상태 | 조건 | 후속 동작 |
|------|------|-----------|
| `PASS` | 명령 성공 | finalize 계속 |
| `SKIP` | placeholder만 존재, PASS 0건 | finalize 계속 (block 안 함) |
| `FAIL` | 정상 명령이 비0 반환 | mode=fail 시 BLOCKED |
| `TIMEOUT` | timeout 초과 | mode=fail 시 BLOCKED |

우선순위: `TIMEOUT > FAIL > SKIP > PASS`

### (4) placeholder SKIP ≠ PASS — marker 기록

- 로그: `[GOAL-GATE] GOAL_ASSERTION_PLACEHOLDER_SKIPPED: <cmd>`
- 요약: `[GOAL-GATE] note: GOAL_ASSERTION_PLACEHOLDER_SKIPPED recorded (skip is NOT pass)`
- `GOAL_RESULT_VAL="SKIP"` (PASS와 명확히 구분)

### (5) placeholder SKIP은 block 안 함 — finalize 계속

- `TIMEOUT` / `FAIL` 만 `mode=fail` 시 `exit 1` (fail-closed BLOCKED)
- `SKIP` 은 `exit 1` 없이 finalize 계속 → callback 정상 도달

---

## 구현 구조

### 변경 1: `goal_assertion_eval()` 함수 + sourcing guard (line 4 직후 삽입)

```bash
goal_assertion_eval() {
    local cmd="$1" allowed="$2" tmo="${3:-30}"
    # placeholder 탐지 3종
    # allowlist 검증
    # timeout guard 실행
    # 4-state 반환: SKIP_PLACEHOLDER | SKIP_NOT_ALLOWED | PASS | FAIL | TIMEOUT
}

if [ "${FINISH_TASK_LIB_ONLY:-0}" = "1" ]; then
    return 0 2>/dev/null || exit 0
fi
```

### 변경 2: GOAL-GATE 루프 재작성 (섹션 2.12 전체 교체)

- `eval "$cmd"` 제거 → `goal_assertion_eval "$cmd" ...` 호출
- 4-state case 분기
- `GOAL_TIMEOUT_HIT`, `GOAL_PLACEHOLDER_SKIPPED`, `GOAL_PASS_COUNT` 카운터
- fail-closed 조건: `TIMEOUT || FAIL` only

---

## backward-compat

- `## goal_assertions` 섹션 없는 task md → `GOALS` 빈 문자열 → 루프 실행 없음 → 기존 동작 동일
- 정상 literal 명령 (placeholder 없음) → 기존대로 실행 (allowlist + timeout 추가됨)
- `GOAL_ENABLED` = "True" 아닐 때 → disabled 메시지 출력 후 스킵 (기존 동작 동일)

---

## 테스트 전략

### 단위 테스트 격리 방법

```bash
FINISH_TASK_LIB_ONLY=1 source scripts/finish-task.sh
# → 함수만 정의, main 로직 실행 없음 (side-effect 0)
```

### 회귀 케이스 10건

| # | 입력 cmd | allowed | timeout_s | 기대 출력 |
|---|----------|---------|-----------|-----------|
| 1 | `python3 $QC_SCRIPT --gate --task-id ...` | `python3 grep` | 5 | `SKIP_PLACEHOLDER` |
| 2 | `cat ...` | `cat` | 5 | `SKIP_PLACEHOLDER` |
| 3 | `grep <foo> x` | `grep` | 5 | `SKIP_PLACEHOLDER` |
| 4 | `grep -q a /etc/hostname` | `grep` | 5 | `PASS` |
| 5 | `grep -q ZZZNOPE /etc/hostname` | `grep` | 5 | `FAIL` |
| 6 | `rm -rf /tmp/x` | `grep cat` | 5 | `SKIP_NOT_ALLOWED` |
| 7 | `python3 -c "import time; time.sleep(10)"` | `python3` | 2 | `TIMEOUT` |
| 8 | `cat /etc/hostname` | `cat` | 5 | `PASS` |
| 9 | `python3 ${QC_SCRIPT} --gate` | `python3` | 5 | `SKIP_PLACEHOLDER` |
| 10 | `curl <url>` | `curl` | 5 | `SKIP_PLACEHOLDER` |

---

## 범위 밖 (무수정)

| 컴포넌트 | 이유 |
|----------|------|
| `dispatch.py` | 생성측 — 별도 task에서 처리 |
| `utils/gate_config_loader.py` | gate 설정 로더 — 무관 |
| callback prereg 로직 | 별도 섹션 |
| `git_evidence` 수집 | 별도 섹션 |
| canonical `scripts/finish-task.sh` | worktree 작업만 허용 |

---

## 검증 결과 (2026-06-08)

```
bash -n scripts/finish-task.sh  →  exit_code=0 (문법 오류 없음)

placeholder1: SKIP_PLACEHOLDER  ✓
placeholder2: SKIP_PLACEHOLDER  ✓
placeholder3: SKIP_PLACEHOLDER  ✓
normal_pass:  PASS              ✓
normal_fail:  FAIL              ✓
notallowed:   SKIP_NOT_ALLOWED  ✓
timeout:      TIMEOUT           ✓
```
