# taskctl-takeover-spec.md — taskctl takeover 명령 설계 명세

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

---

## 1. 목적 (Why)

### 1.1 설계 배경

task-2452 사고에서 봇이 handoff 없이 이전 봇의 작업 상태를 이어받으려 시도하여
silent corruption이 발생했다. 이 경험에서 두 가지 교훈을 얻었다:

- 문서 안내만으로는 봇 간 인계 절차 누락을 막을 수 없다.
- handoff evidence 검증을 코드가 강제하지 않으면 언제든 우회된다.

### 1.2 설계 목표

- 봇 간 인계 시 silent corruption 방지
- handoff evidence 검증을 코드로 강제 (수동 점검 제거)
- start_task_guard와 연계하여 takeover도 9개 검증을 통과해야만 작업 시작 가능

### 1.3 회장 명시 원칙 (직접 인용)

> "이어받기는 taskctl takeover만 허용. 봇은 다른 봇의 폴더를 이어받지 않고,
> handoff evidence만 이어받는다."

이 원칙이 본 명세 전체의 설계 기준이다. 새 봇은 이전 봇의 worktree를 재사용하지 않고,
반드시 origin/main에서 새 worktree를 분기하여 시작한다.

---

## 2. 인터페이스

### 2.1 명령 형식

```bash
./scripts/taskctl.py takeover <task-id> --from <branch> --bot <new-bot>
```

**인자 설명:**

| 인자 | 필수 | 설명 | 예시 |
|---|---|---|---|
| `<task-id>` | 필수 | 인계받을 task 식별자 | `task-2458` |
| `--from <branch>` | 필수 | 이전 봇의 브랜치명 | `task/task-2458-dev3` |
| `--bot <new-bot>` | 필수 | 새 봇 식별자 | `dev4` |

**실행 예시:**

```bash
./scripts/taskctl.py takeover task-2458 --from task/task-2458-dev3 --bot dev4
```

### 2.2 환경변수

| 변수 | 설명 | 기본값 |
|---|---|---|
| `WORKSPACE_ROOT` | 워크스페이스 루트 절대경로 | `/home/jay/workspace` |

### 2.3 종료 코드

| 코드 | 의미 | 봇 행동 |
|---|---|---|
| `0` | PASS — takeover 완료, 작업 시작 가능 | 새 worktree에서 작업 진행 |
| `1` | FAIL — 검증 실패 또는 evidence 문제 | 즉시 STOP + 회장에게 stderr 보고 |
| `2` | internal error — 예상치 못한 예외 | 즉시 STOP + stderr 전체 보고 |

---

## 3. 검증 흐름 다이어그램 (ASCII)

```
[start: taskctl takeover <task-id> --from <branch> --bot <new-bot>]
   │
   ▼
[1. handoff 파일 존재?]
   │  memory/handoffs/<task-id>.json 확인
   ├─ NO ──→ FAIL evidence 저장 + exit 1
   │         stderr: "handoff JSON 미존재: memory/handoffs/<task-id>.json"
   │ YES
   ▼
[2. validate_handoff: schema / task_id / changed_paths 검증]
   │  jsonschema Draft 2020-12 기준
   │  task_id 필드가 인자 <task-id>와 일치 확인
   │  changed_paths 항목이 allowed_paths 내에 있는지 확인
   ├─ FAIL ─→ FAIL evidence 저장 + exit 1
   │ PASS
   ▼
[3. from-branch 존재? (git ls-remote / git branch -r)]
   ├─ NO ──→ exit 1
   │         stderr: "from-branch 미존재: <branch>"
   │ YES
   ▼
[4. handoff.head_sha == from-branch HEAD?]
   │  git rev-parse origin/<branch> 또는 로컬 브랜치 HEAD 비교
   ├─ NO ──→ exit 1
   │         stderr: "head_sha mismatch: handoff=<sha1> branch=<sha2>"
   │ YES
   ▼
[5. 새 worktree 경로 충돌?]
   │  .worktrees/<task-id>-<new-bot> 디렉토리 존재 확인
   ├─ YES ──→ exit 1
   │          stderr: "worktree 경로 충돌: .worktrees/<task-id>-<new-bot>"
   │ NO
   ▼
[6. git worktree add]
   │  git worktree add .worktrees/<task-id>-<new-bot>
   │                   -b task/<task-id>-<new-bot>
   │                   origin/main
   │
   ▼
[7. cherry-pick / patch 적용 — v1.0 미수행]
   │  v1.0: 이 단계 skip
   │  v1.1+: cherry_pick_commits 또는 patch_path 처리
   │
   ▼
[8. start_task_guard 자동 호출]
   │  ./scripts/start_task_guard.py --task <task-id> --bot <new-bot>
   │  (cwd: .worktrees/<task-id>-<new-bot>)
   ├─ exit 1 ──→ worktree 자동 제거 시도 + takeover FAIL + exit 1
   │ exit 0
   ▼
[9. evidence atomic write]
   │  .tasks/evidence/<task-id>/takeover-<UTC-ts>.json
   │  tempfile + os.replace (원자성 보장)
   │
   ▼
[end: exit 0 — 새 worktree에서 작업 시작 가능]
```

---

## 4. cherry-pick vs patch 정책

### 4.1 v1.0 정책 (본 task 범위)

handoff JSON v1.0 스키마(`handoff-schema.json`)는 `cherry_pick_commits`, `patch_path` 필드를
정의하지 않는다. 스키마에 `additionalProperties: false`가 선언되어 있으므로,
이 필드를 포함한 handoff JSON은 스키마 검증에서 즉시 실패한다.

**v1.0 takeover 동작:**
- 새 branch를 origin/main에서 분기만 한다.
- 이전 봇의 commit은 자동 이전하지 않는다.
- 새 봇은 handoff의 `changed_paths`와 `pending_work`를 참고하여
  필요 시 수동으로 작업을 재수행한다.

### 4.2 v1.1 미래 확장 (out of scope — 명세 차원 기록만)

> 이하 내용은 본 task(v1.0)의 구현 범위 밖이다. 설계 연속성을 위해 기록한다.

v1.1에서 추가될 선택 필드:

| 필드 | 타입 | 설명 |
|---|---|---|
| `cherry_pick_commits` | `list[str]` | 이전 봇의 커밋 SHA 목록. takeover 시 cherry-pick 적용 |
| `patch_path` | `str` | 이전 봇이 생성한 patch 파일 경로. takeover 시 git apply |

- 두 필드는 oneOf로 배타적 (둘 다 존재하면 스키마 실패).
- 둘 다 없으면 v1.0 동작과 동일.
- 추가 시 `handoff-schema.json` 버전을 1.1로 올리고 `schema_version` enum에 `"1.1"` 추가.

### 4.3 선택 기준 (v1.1 도입 시 참고)

| 시나리오 | 권장 방식 | 이유 |
|---|---|---|
| 단일 atomic 변경 (1~3 커밋) | `cherry_pick_commits` | 커밋 의도와 메시지 보존 |
| 복잡한 다중 커밋 + 충돌 우려 | `patch_path` | 충돌 처리 책임을 새 봇에게 명시적 이전 |

---

## 5. evidence 포맷

### 5.1 파일 위치

```
.tasks/evidence/<task-id>/takeover-<UTC-timestamp>.json
```

파일명 타임스탬프 포맷: `YYYYMMDDTHHMMSSZ` (파일시스템 안전 문자만 사용)
JSON 내부 `started_at` 필드: ISO 8601 UTC `YYYY-MM-DDTHH:MM:SSZ`
JSON 내부 `ts_filename` 필드: 파일명과 동일한 압축 포맷 (역참조용)

예시: `.tasks/evidence/task-2458/takeover-20260505T093012Z.json`

### 5.2 성공 시 필수 필드

```json
{
  "task_id": "task-2458",
  "previous_bot": "dev3",
  "new_bot": "dev4",
  "base_sha": "f4151eeb",
  "head_sha": "a1b2c3d4",
  "handoff_path": "/home/jay/workspace/memory/handoffs/task-2458.json",
  "from_branch": "task/task-2458-dev3",
  "new_branch": "task/task-2458-dev4",
  "new_worktree_path": "/home/jay/workspace/.worktrees/task-2458-dev4",
  "started_at": "2026-05-05T09:30:12Z",
  "takeover_status": "success"
}
```

| 필드 | 타입 | 설명 |
|---|---|---|
| `task_id` | string | 인계 대상 task 식별자 |
| `previous_bot` | string | handoff JSON에서 추출한 이전 봇 ID |
| `new_bot` | string | `--bot` 인자로 전달된 새 봇 ID |
| `base_sha` | string | origin/main HEAD (새 branch 분기 기준점) |
| `head_sha` | string | handoff JSON에서 추출한 이전 봇 작업 HEAD |
| `handoff_path` | string | handoff JSON 절대경로 |
| `from_branch` | string | `--from` 인자로 전달된 이전 봇 브랜치 |
| `new_branch` | string | 생성된 새 브랜치 `task/<id>-<new-bot>` |
| `new_worktree_path` | string | 생성된 새 worktree 절대경로 |
| `started_at` | string | ISO 8601 UTC 타임스탬프 |
| `takeover_status` | string | `"success"` 또는 `"failed"` |

### 5.3 실패 시 추가 필드

```json
{
  "takeover_status": "failed",
  "failure_reason": "head_sha mismatch: handoff=a1b2c3d4, branch=9f8e7d6c",
  "failure_check": "4_head_sha_match"
}
```

| 필드 | 타입 | 설명 |
|---|---|---|
| `failure_reason` | string | 사람이 읽을 수 있는 실패 사유 |
| `failure_check` | string | 단계 식별자: `<번호>_<짧은이름>` (예: `1_handoff_exists`, `4_head_sha_match`) |

조기 실패(handoff 부재, schema invalid, sha mismatch 등) 시에도 evidence가 저장되어 사후 감사가 가능하다. 단, base_sha/handoff_path 등 일부 필드는 단계에 따라 누락될 수 있다.

### 5.4 원자성 보장

```python
import tempfile, os, json

def write_evidence_atomic(path: str, data: dict) -> None:
    dir_path = os.path.dirname(path)
    os.makedirs(dir_path, exist_ok=True)
    with tempfile.NamedTemporaryFile(
        mode='w', dir=dir_path, delete=False, suffix='.tmp'
    ) as f:
        json.dump(data, f, ensure_ascii=False, indent=2)
        tmp_path = f.name
    os.replace(tmp_path, path)  # 원자적 이동 (부분 쓰기 방지)
```

---

## 6. start_task_guard 통합

### 6.1 자동 호출 규칙

takeover의 8번째 단계로 start_task_guard를 자동 호출한다.

```bash
# takeover 내부에서 실행 (subprocess)
cd .worktrees/<task-id>-<new-bot>
./scripts/start_task_guard.py --task <task-id> --bot <new-bot>
```

start_task_guard는 9개 검증 + lock 생성을 수행하므로, takeover는 그 결과를 신뢰한다.
takeover가 독자적으로 worktree/branch/clean-tree를 재검증하지 않는다.

### 6.2 start_task_guard 실패 시 처리

| 단계 | 동작 |
|---|---|
| exit 1 수신 | takeover FAIL 처리 |
| worktree 제거 | `git worktree remove --force .worktrees/<task-id>-<new-bot>` 시도 |
| 브랜치 제거 | `git branch -D task/<task-id>-<new-bot>` 시도 |
| evidence 기록 | `takeover_status: "failed"`, `failure_check: 8` |
| exit 1 | takeover 자체도 exit 1 |

제거 시도가 실패해도 takeover는 exit 1을 반환한다.
제거 실패 사유는 stderr에 경고로 출력하되, 회장에게 수동 정리를 요청한다.

---

## 7. Phase 2-D 통합 인터페이스 (예고)

dispatch.py가 STEP 0을 자동 주입할 때 takeover_request 모드:

- handoff JSON이 존재하고 `handoff_reason == "takeover_request"`이면
  dispatch가 봇 프롬프트에 takeover 명령을 자동 삽입
- 봇은 일반 `start_task_guard` 호출이 아닌 `taskctl takeover` 호출로 작업 시작

**자동 삽입 템플릿 (Phase 2-D 예고):**

```markdown
## STEP 0 (takeover 모드 — silent corruption 방지)

이 작업은 이전 봇으로부터 인계받는 작업입니다. 반드시 아래를 먼저 실행하세요:

```bash
cd /home/jay/workspace
./scripts/taskctl.py takeover <task-id> --from <from-branch> --bot <new-bot>
# exit 0이면 새 worktree에서 작업 진행
# exit 1이면 즉시 STOP + 회장에게 stderr 보고
```

일반 `git worktree add` + `start_task_guard` 수동 호출은 takeover 시 금지입니다.
```

---

## 8. 비동기 안전성

### 8.1 동시 takeover 시도

복수의 봇이 동시에 동일 task-id로 takeover를 시도하는 경우:

- start_task_guard의 lock(`.tasks/locks/<task-id>.lock`) 생성 단계에서 자동 차단
- 먼저 lock을 획득한 봇만 exit 0을 받음
- 나머지는 exit 1 + "lock 이미 존재" 메시지

### 8.2 evidence 동시 쓰기

- 파일명에 UTC 타임스탬프(나노초 단위 권장)를 포함하여 충돌 회피
- tempfile + os.replace 조합으로 부분 쓰기 방지
- 동일 타임스탬프 충돌 발생 시 파일명에 4자리 난수 suffix 추가

---

## 9. 차단 케이스 (회귀 테스트 대상)

| # | 케이스 | 입력 조건 | 기대 결과 |
|---|---|---|---|
| 1 | handoff 없이 takeover | `memory/handoffs/<id>.json` 미존재 | exit 1, "handoff JSON 미존재" |
| 2 | schema invalid | handoff JSON 스키마 불일치 | exit 1, validate_handoff 실패 메시지 |
| 3 | task_id 불일치 | handoff.task_id != 인자 task-id | exit 1, "task_id mismatch" |
| 4 | head_sha 불일치 | handoff.head_sha != from-branch HEAD | exit 1, "head_sha mismatch" |
| 5 | from-branch 미존재 | git에 해당 브랜치 없음 | exit 1, "from-branch invalid" |
| 6 | changed_paths 초과 | changed_paths에 allowed_paths 외 경로 포함 | exit 1, "changed_paths not in allowed" |
| 7 | worktree 경로 충돌 | `.worktrees/<id>-<bot>` 이미 존재 | exit 1, "worktree 경로 충돌" |
| 8 | start_task_guard 실패 | 9개 검증 중 1개 이상 실패 | exit 1, worktree 자동 제거 시도 |
| 9 | (PASS) 정상 takeover | 모든 조건 충족 | exit 0, 새 worktree+branch+evidence 생성 |

---

## 10. Phase 1 산출물 read-only 원칙

본 task(task-2458)에서 아래 파일은 **수정 불가**이며, 호출/import만 허용한다.

| 파일 | 제약 |
|---|---|
| `scripts/start_task_guard.py` | read-only. subprocess 호출만 허용 |
| `scripts/create_handoff.py` | read-only. 참조만 허용 |
| `memory/specs/handoff-schema.json` | read-only. jsonschema 검증 대상으로만 사용 |
| `memory/specs/start-guard-spec.md` | read-only. 설계 참조용 |

`scripts/taskctl.py`는 본 task의 신규 산출물이므로 수정 가능.

---

## 11. 변경 이력

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

---

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

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

| 항목 | 경로 |
|---|---|
| handoff JSON | `memory/handoffs/<task-id>.json` |
| handoff 스키마 | `memory/specs/handoff-schema.json` |
| 새 worktree | `.worktrees/<task-id>-<new-bot>/` |
| 새 브랜치 | `task/<task-id>-<new-bot>` |
| takeover evidence | `.tasks/evidence/<task-id>/takeover-<ts>.json` |
| lock 파일 | `.tasks/locks/<task-id>.lock` |
| taskctl 스크립트 | `scripts/taskctl.py` |
| start_task_guard | `scripts/start_task_guard.py` |

## 부록 B: exit 코드 규약 (전체 가드 체인 공통)

| exit 코드 | 의미 | 봇 행동 |
|---|---|---|
| 0 | 모든 검증 PASS | 작업 진행 |
| 1 | 검증 실패 | 즉시 STOP + 회장에게 stderr 그대로 보고 |
| 2 | internal error | 즉시 STOP + stderr 전체 보고 |

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