# start-guard-spec.md — 호출 강제 설계 문서 (Start Guard / Handoff Guard)

> 버전: 1.0 | 작성일: 2026-05-05 | 작성자: 락슈미 (dev4팀 UX/UI 설계자)
> Task: task-2454 Phase 1 MVP

---

## 1. 문제 정의 (Why)

### 1.1 task-2452 사고 raw 증거

task-2452 사고는 봇이 **메인 워크스페이스(`/home/jay/workspace/`)에서 직접 작업**을 시작하여 발생한
silent corruption 사례다. 구체적인 증거는 다음과 같다:

- **봇이 fresh worktree를 생성하지 않고** 메인 워크스페이스에서 파일을 수정
- 브랜치 확인 없이 작업 진행 → `origin/main`에 직접 커밋 또는 잘못된 브랜치에 커밋
- handoff 없이 다음 봇이 이어받기(takeover) 시도 → 이전 작업 상태 불명
- `start_task_guard.py` 호출 자체가 없었으므로, 위 모든 조건에 대한 **코드 레벨 검증이 전혀 없었음**

이 결과, 변경사항이 어느 브랜치에 얼마나 반영되었는지 추적 불가능한 상태(silent corruption)가 발생했다.

### 1.2 회장 발화 직접 인용

> "이번 사고의 root cause는 merge 단계가 아니라 '작업 시작 단계'와 'handoff/이어받기 단계'의 코드 가드
> 부재다. 문서로 안내하지 말고, 코드가 자동으로 막고 자동으로 안내하게 만들어라."

이 발화가 task-2454 전체의 설계 원칙을 결정한다.
문서(`.md`) 안내만으로는 봇 호출 누락을 막을 수 없다.
dispatch.py가 봇 프롬프트에 STEP 0을 **자동 삽입**함으로써,
봇이 가드를 호출하지 않으면 **작업 자체가 시작되지 않는 구조**를 만들어야 한다.

> "start_task_guard 호출 누락 = 시스템 실패."

### 1.3 task-2440 (server-side merge gate)과의 보완 관계

task-2440은 GitHub 브랜치 보호 규칙 + Required Checks + Gemini Gate + Merge Queue를 통해
**머지 단계**의 강제 게이트를 구현했다.

본 task-2454는 그보다 **앞선 단계** — 작업 시작(Start)과 이어받기(Handoff) — 를 코드로 강제한다.

| 가드 위치 | 담당 task | 구현 형태 |
|---|---|---|
| 작업 시작 / 이어받기 | task-2454 (본 task) | start_task_guard.py |
| 머지 전 최종 검증 | task-2440 | GitHub Required Checks + Gemini Gate |

두 가드는 서로 보완 관계다. task-2440이 머지 단계 최후 방어선이라면,
task-2454는 **최초 진입 단계** 방어선이다.

---

## 2. 가드 체인 전체 그림

회장 명시 기준 한 줄 가드 체인:

```
Start Guard → Handoff Guard → Commit Guard → Finish Guard → Merge Guard
```

### 단계별 구분

| Phase | 산출물 | 담당 |
|---|---|---|
| Phase 1 (본 task) | Start Guard + Handoff Guard | task-2454 |
| Phase 2 | dispatch STEP 0 자동 주입 + taskctl takeover | dispatch.py 수정 (별도 task) |
| Phase 3 | git pre-commit hook (Commit Guard) | `.git/hooks/pre-commit` 또는 husky |
| Phase 4 | mixed commit recovery workflow | 별도 도구, 회장 수동 승인만 |

**Phase 1(본 task) 범위:**

- `scripts/start_task_guard.py` — 작업 시작 전 8개 조건 검증 + lock 생성
- `scripts/create_handoff.py` — 작업 종료 시 handoff JSON 생성
- `memory/specs/handoff-schema.json` — 핸드오프 스키마 (v1.0, 이미 존재)
- 본 문서(`memory/specs/start-guard-spec.md`) — 설계 명세

Phase 2에서 dispatch.py 수정을 통해 STEP 0이 자동 주입된다.
Phase 1에서는 봇이 수동으로 STEP 0을 따라야 한다.

---

## 3. 호출 진입점 (How to Enforce)

### 3.1 dispatch.py STEP 0 자동 주입 템플릿 (★ 핵심)

> "dispatch.py가 cron prompt 첫 줄에 자동 삽입할 STEP 0 템플릿 명시."
> — 회장 직접 명시 (task-2454 배경)

dispatch.py는 Phase 2에서 봇 프롬프트의 **최상단 첫 줄**에 아래 템플릿을 자동 삽입해야 한다.
본 Phase 1에서는 봇이 이 템플릿을 수동으로 준수해야 한다.

```markdown
## STEP 0 (★ 시작 전 필수 — silent corruption 방지)

작업 시작 전 반드시 다음을 실행하세요. 미실행 시 작업 무효:

1. fresh worktree 생성:
   ```bash
   cd /home/jay/workspace
   git worktree add .worktrees/<task-id>-<bot> -b task/<task-id>-<bot> origin/main
   cd .worktrees/<task-id>-<bot>
   ```

2. start_task_guard 실행:
   ```bash
   ./scripts/start_task_guard.py --task <task-id> --bot <bot>
   # exit 0이면 진행, exit 1이면 즉시 STOP + 회장 알림
   ```

3. exit 1 발생 시: stderr 메시지 그대로 회장에게 보고 + 작업 중단.
```

#### 변수 치환 규칙

| 변수 | 치환값 | 예시 |
|---|---|---|
| `<task-id>` | 실제 task 식별자 | `task-2454` |
| `<bot>` | 봇 식별자 | `dev1` ~ `dev9`, `design`, `qc` |

치환 예시:
- `<task-id>` → `task-2454`
- `<bot>` → `dev4`
- 결과 브랜치: `task/task-2454-dev4`
- 결과 worktree 경로: `.worktrees/task-2454-dev4`

### 3.2 dispatch.py 삽입 위치 (Phase 2 가이드)

dispatch.py는 본 Phase에서 **forbidden_paths**이므로 코드 수정을 하지 않는다.
Phase 2 적용 시 참고용으로 삽입 위치만 명시한다.

- **대상 함수**: dispatch.py 내 cron prompt를 조합하는 함수
  (read-only 확인 결과: `_build_prompt(...)` 또는 이에 상응하는 함수)
- **삽입 위치**: 해당 함수의 최상단 — prompt 문자열의 첫 번째 라인
- **적용 방법**:

```python
# dispatch.py Phase 2 적용 예시 (현재 미적용, 설계 참조용)
STEP0_TEMPLATE = """\
## STEP 0 (★ 시작 전 필수 — silent corruption 방지)
...
"""

def _build_prompt(task_id: str, bot: str, ...) -> str:
    step0_block = STEP0_TEMPLATE.format(task_id=task_id, bot=bot)
    return step0_block + "\n\n" + original_prompt_body
```

jinja2 사용 시: 템플릿 파일 `prompts/step0_block.j2` 에 템플릿 관리,
`{{ step0_block }}` 변수를 prompt 템플릿 최상단에 배치.

### 3.3 강제 검증 (Phase 2 추가 검증)

Phase 2 통합 후 dispatch.py가 아래 검증을 추가로 수행한다:

- 봇 응답(첫 번째 도구 호출) 추적 시, 첫 도구 호출이 `start_task_guard.py` 실행인지 확인
- 미호출 시 봇에게 즉시 재지시:

```
[STEP 0 미완료] start_task_guard.py 호출이 감지되지 않았습니다.
STEP 0을 먼저 실행한 후 작업을 계속하세요.
미실행 시 이 세션의 모든 작업은 무효 처리됩니다.
```

---

## 4. Handoff Guard 동작 흐름

### 4.1 전체 흐름도

```
[봇 A 작업 종료] → create_handoff.py 호출 (interrupt/complete)
                ↓
        memory/handoffs/<task-id>.json 생성
        (handoff-schema.json v1.0 준수)
                ↓
[봇 B takeover] → start_task_guard.py --takeover-from <task-id>
                ↓
        handoff JSON 부재 시 → exit 1 + "handoff JSON 없음, takeover 불가" 출력
        존재 시 → 8개 검증 수행 → lock 생성 → exit 0
```

### 4.2 create_handoff.py 호출 규칙

| 상황 | handoff_reason 값 | 비고 |
|---|---|---|
| 작업 도중 중단 (세션 종료, 강제 이관) | `interrupt` | 잔여 작업 기술 필수 |
| 작업 완료 (정상 종료) | `complete` | pending_work 없어도 됨 |
| 이관 요청 | `takeover_request` | 이관 사유 명시 |

호출 예시:

```bash
# 작업 도중 중단 시
./scripts/create_handoff.py \
  --task task-2454 \
  --bot dev4 \
  --reason interrupt \
  --pending-work "3.1절 STEP 0 템플릿 검증 미완"

# 작업 정상 완료 시
./scripts/create_handoff.py \
  --task task-2454 \
  --bot dev4 \
  --reason complete
```

### 4.3 start_task_guard.py --takeover-from 동작

takeover 시 start_task_guard.py는 기존 8개 검증에 추가로:

1. `memory/handoffs/<task-id>.json` 존재 확인
2. handoff JSON의 schema_version = "1.0" 확인
3. handoff의 `current_branch`가 현재 worktree 브랜치와 일치하는지 확인
4. handoff의 `head_sha`가 현재 브랜치 HEAD와 일치하는지 확인 (diverge 감지)

모든 검증 통과 시 lock을 생성하고 exit 0.

---

## 5. Mixed Commit 정책 (★ 회장 절대 금지)

### 5.1 정의

mixed commit = 하나의 커밋에 두 개 이상의 task-id에 해당하는 파일 변경이 섞인 커밋.

예: `task-2454`와 `task-2455`의 변경사항이 같은 commit SHA에 포함된 경우.

### 5.2 자동 복구 절대 금지

> **자동 복구 절대 금지** — rebase/cherry-pick을 LLM이 시도하는 것은
> 더 큰 silent corruption 위험을 초래한다.

mixed commit 감지 시 허용되는 동작:

| 단계 | 동작 | 결과 |
|---|---|---|
| 감지 | mixed commit 패턴 탐지 | 즉시 freeze |
| freeze | `memory/events/<task-id>.mixed-commit.freeze` 마커 생성 | 추가 작업 차단 |
| evidence | diff, git log, 관련 commit SHA 기록 | `memory/events/<task-id>.mixed-commit.json` |
| escalation | 회장에게 즉시 보고 + 작업 중단 | stderr 출력 |

**회복은 회장 수동 판단.** Phase 4 recovery workflow에서 도구를 제공할 예정이며,
그 이전에는 LLM이 임의로 rebase/cherry-pick을 시도해서는 안 된다.

### 5.3 mixed commit 감지 조건 (Phase 3 pre-commit hook 구현 예정)

```bash
# 커밋 대상 파일 목록에서 task-id 추출
CHANGED=$(git diff --cached --name-only)
TASK_IDS=$(echo "$CHANGED" | grep -oE 'task-[0-9]+' | sort -u | wc -l)
if [ "$TASK_IDS" -gt 1 ]; then
  echo "[ERROR] mixed commit 감지: 2개 이상 task 변경사항이 섞여 있습니다."
  echo "각 task 변경사항을 별도 커밋으로 분리하세요."
  exit 1
fi
```

---

## 6. Orphaned Lock 정책

### 6.1 lock JSON 필수 필드

`.tasks/locks/<task-id>.lock` 파일은 다음 필드를 반드시 포함해야 한다:

```json
{
  "task_id": "task-2454",
  "bot": "dev4",
  "pid": 12345,
  "worktree": ".worktrees/task-2454-dev4",
  "branch": "task/task-2454-dev4",
  "created_at": "2026-05-05T03:00:00Z",
  "heartbeat_timestamp": "2026-05-05T03:05:00Z"
}
```

`pid`와 `heartbeat_timestamp` 필드는 stale lock 판별의 핵심이다.

### 6.2 heartbeat 갱신 책임

| Phase | 갱신 주체 | 갱신 방법 |
|---|---|---|
| Phase 1 (현재) | 봇 자체 | 5분마다 `./scripts/start_task_guard.py --update-heartbeat <task-id>` 호출 (또는 외부 cron) |
| Phase 2 통합 후 | cron (`*/5 * * * *`) | active task 목록 추출 → 자동 갱신 |

Phase 1에서 봇이 heartbeat를 갱신하지 않으면 30분 후 stale 처리된다.
장시간 작업 시 반드시 heartbeat를 갱신해야 한다.

### 6.3 stale lock 처리 규칙

| 항목 | 값 |
|---|---|
| stale 임계값 | 30분 (1800초) |
| 판별 기준 | `now - heartbeat_timestamp > 1800s` AND `pid` 프로세스 미존재 |
| cleanup 명령 | `./scripts/start_task_guard.py --cleanup-stale` |
| cleanup 실행 주기 | cron `*/10 * * * *` |
| cleanup 증거 | `memory/events/<task-id>.lock-cleanup.json` |

### 6.4 stale lock cleanup 증거 포맷

```json
{
  "task_id": "task-2454",
  "bot": "dev4",
  "lock_path": ".tasks/locks/task-2454.lock",
  "stale_reason": "heartbeat expired (last: 2026-05-05T02:30:00Z, now: 2026-05-05T03:05:00Z)",
  "pid_alive": false,
  "cleaned_at": "2026-05-05T03:05:00Z"
}
```

---

## 7. Session-Watchdog 통합 계획 (Phase 2)

### 7.1 현재 구조 (Phase 1 분리 운영)

현재 session-watchdog과 lock은 분리 운영 중이다.

- `scripts/session-watchdog.sh` — 봇 세션 활성 감지
- `memory/heartbeats/*.heartbeat` — 세션별 heartbeat 파일 (별도 관리)
- `.tasks/locks/*.lock` — start_task_guard 생성 lock (별도 관리)

Phase 1에서는 두 시스템을 **분리 운영**한다. 통합 시 split-brain 위험이 있기 때문이다.

> split-brain 위험: session-watchdog이 heartbeat를 갱신하는 사이 lock이 stale 처리되거나,
> 반대로 lock이 stale인데 session-watchdog이 "활성"으로 판단하는 상황.

### 7.2 Phase 2 통합 절차

Phase 2에서 아래 절차로 통합한다:

1. `.tasks/locks/<task-id>.lock`을 **single source of truth**로 승격
2. `scripts/session-watchdog.sh`가 heartbeat 파일 대신 lock의 `heartbeat_timestamp` 필드를 읽도록 수정
3. `memory/heartbeats/` 폐기 또는 lock으로 마이그레이션 (이전 데이터 보존 후 폐기)
4. 봇은 heartbeat 갱신 시 lock JSON의 `heartbeat_timestamp`만 갱신 (heartbeat 파일 별도 생성 불필요)

### 7.3 통합 후 단일 갱신 인터페이스

```bash
# Phase 2 통합 후 봇 heartbeat 갱신 명령 (단일)
./scripts/start_task_guard.py --update-heartbeat task-2454
# → .tasks/locks/task-2454.lock의 heartbeat_timestamp 갱신
# → session-watchdog도 이 값을 읽음
```

---

## 8. Phase 별 마이그레이션 표

| Phase | 산출물 | 적용 방법 | 위험 |
|---|---|---|---|
| Phase 1 (본 task) | start_task_guard.py, create_handoff.py, handoff-schema.json, 본 설계 문서 | 봇 매뉴얼 호출 | 호출 누락 시 우회 가능 |
| Phase 2 | dispatch STEP 0 자동 주입, taskctl takeover, session-watchdog 통합 | dispatch.py 수정 | 기존 봇 prompt 호환 검증 필요 |
| Phase 3 | git pre-commit hook (Commit Guard) | `.git/hooks/pre-commit` 또는 husky | 우회는 `--no-verify` |
| Phase 4 | mixed commit recovery workflow | 별도 도구, 회장 수동 승인만 | 자동화 절대 금지 |

---

## 9. 8개 합격 기준 매핑

start_task_guard.py 내부 검증 항목과 합격 기준의 매핑 표.

| # | 합격 기준 | 검증 위치 |
|---|---|---|
| 1 | 메인 워크스페이스(`/home/jay/workspace/`) 시작 차단 | start_task_guard 검증 #2: `cwd != /home/jay/workspace` |
| 2 | 메인 브랜치가 main이 아님 차단 | start_task_guard 검증 #7: `origin/main HEAD` 일치 여부 확인 |
| 3 | task-N을 task-M 브랜치에서 시도 차단 | 검증 #3: 브랜치명에 `task-id` 포함 여부 / 검증 #4: 기존 lock task-id 일치 |
| 4 | handoff 없는 takeover 차단 | `--takeover-from` 진입점: `memory/handoffs/<task-id>.json` 존재 확인 |
| 5 | dirty worktree(uncommitted 변경) 차단 | 검증 #6: `git status --porcelain` 결과가 비어있는지 확인 |
| 6 | cancelled task 시작 차단 | 검증 #9: `memory/events/<task-id>.cancelled` 마커 존재 시 exit 1 |
| 7 | 정상 worktree + branch + clean tree 허용 | 모든 검증(#1~#9) PASS 시 lock 생성 + exit 0 |
| 8 | handoff JSON 스키마 유효성 | create_handoff.py self-validation: `handoff-schema.json` 대비 jsonschema 검증 |

### 9.1 검증 항목 전체 목록 (start_task_guard.py)

| 검증 번호 | 검증 내용 | 실패 시 exit 코드 |
|---|---|---|
| #1 | `--task`, `--bot` 인자 존재 | exit 1 |
| #2 | 현재 디렉토리가 `/home/jay/workspace`가 아닌 worktree 경로 | exit 1 |
| #3 | 현재 브랜치명이 `task/<task-id>-<bot>` 패턴 일치 | exit 1 |
| #4 | 브랜치에 포함된 task-id와 `--task` 인자 일치 | exit 1 |
| #5 | `.tasks/locks/<task-id>.lock` 미존재 또는 stale | exit 1 (신규 시작) / exit 0 (자신의 stale lock 갱신) |
| #6 | `git status --porcelain` = 비어있음 (clean tree) | exit 1 |
| #7 | `origin/main` HEAD가 예상 base_sha와 일치 (takeover 시) | exit 1 |
| #8 | worktree가 `origin/main` 기준으로 생성됨 (base_sha 검증) | exit 1 |
| #9 | `memory/events/<task-id>.cancelled` 마커 미존재 | exit 1 |

---

## 10. 변경 이력

| 날짜 | 버전 | 내용 | 작성자 |
|---|---|---|---|
| 2026-05-05 | v1.0 | Phase 1 MVP 초안 | 락슈미 (dev4팀 UX/UI 설계자) |

---

## 부록 A: 경로 규칙 요약

모든 경로는 `/home/jay/workspace` 기준 상대경로.

| 항목 | 경로 |
|---|---|
| 메인 워크스페이스 | `/home/jay/workspace/` (작업 금지) |
| worktree 루트 | `.worktrees/<task-id>-<bot>/` |
| lock 파일 | `.tasks/locks/<task-id>.lock` |
| handoff JSON | `memory/handoffs/<task-id>.json` |
| handoff 스키마 | `memory/specs/handoff-schema.json` |
| cancelled 마커 | `memory/events/<task-id>.cancelled` |
| freeze 마커 | `memory/events/<task-id>.mixed-commit.freeze` |
| lock cleanup 증거 | `memory/events/<task-id>.lock-cleanup.json` |
| mixed commit 증거 | `memory/events/<task-id>.mixed-commit.json` |
| session heartbeat (Phase 1) | `memory/heartbeats/<task-id>.heartbeat` |
| session heartbeat (Phase 2+) | `.tasks/locks/<task-id>.lock` (heartbeat_timestamp 필드) |
| start_task_guard | `scripts/start_task_guard.py` |
| create_handoff | `scripts/create_handoff.py` |
| session-watchdog | `scripts/session-watchdog.sh` |

## 부록 B: exit 코드 규약

| exit 코드 | 의미 | 봇 행동 |
|---|---|---|
| 0 | 모든 검증 PASS, 작업 시작 허용 | 작업 진행 |
| 1 | 검증 실패 — stderr 메시지 확인 | 즉시 STOP + 회장에게 stderr 그대로 보고 |

봇은 exit 코드 1 발생 시 **작업을 절대 계속하지 않는다.**
임의로 검증을 우회하거나 lock을 수동 생성하는 것은 금지다.
