---
id: taskctl-state-machine-spec
version: 1.0
created: 2026-05-06
authority: chairman (task-2467)
scope: scripts/taskctl.py + .tasks/state/ + .tasks/evidence/
---

# taskctl State Machine 명세 (v1.0)

> 회장 절대 기준 (인용):
> "taskctl을 거치지 않고는 main을 절대 변경할 수 없다."
> "모든 PR은 bot이 생성하고, 인간은 승인만 하며, main 반영은 taskctl만 수행한다."

## 1. 상태 enum (14 정상 + 5 예외 = 19종)

### 1.1 정상 상태 (14)

| # | 상태 | 의미 | 진입 명령 | 직전 상태 |
|---|------|------|----------|-----------|
| 1 | CREATED | 상태 파일 생성됨 | `taskctl init` | (없음) |
| 2 | WORKTREE_READY | worktree + branch 준비 완료 | `taskctl worktree-ready` (또는 ack/dispatch alias) | CREATED |
| 3 | RUNNING | 봇이 작업 중 | `taskctl run` | WORKTREE_READY |
| 4 | HANDOFF_READY | (선택) 다음 봇으로 인계 가능 | `taskctl handoff` | RUNNING |
| 5 | COMMITTED | 코드 커밋 + push 완료 | `taskctl commit` | RUNNING / HANDOFF_READY |
| 6 | PR_OPEN | PR 생성됨 (bot author) | `taskctl pr-open` | COMMITTED |
| 7 | CI_PENDING | CI workflow 진행 중 | `taskctl ci-check` | PR_OPEN |
| 8 | GEMINI_PENDING | Gemini App review 대기 | `taskctl gemini-evidence` | CI_PENDING |
| 9 | REVIEW_READY | Gemini 리뷰 도착 (HOLD/PASS 판정 대기) | `taskctl review-ready` | GEMINI_PENDING |
| 10 | VERIFIED | guard.sh + qc + gemini PASS | `taskctl verify` | REVIEW_READY |
| 11 | HUMAN_APPROVED | 인간 승인 (PR author != approver) | `taskctl approve --by <human>` | VERIFIED |
| 12 | MERGING | merge 진행 중 (gh pr merge 호출 직전) | `taskctl merge` 내부 | HUMAN_APPROVED |
| 13 | MERGED | origin/main에 merge commit 확인됨 | `taskctl merge` 내부 | MERGING |
| 14 | DONE | `.done` 생성 + 모든 evidence 박제 | `taskctl done` | MERGED |

### 1.2 예외 상태 (5)

| 상태 | 의미 | 트리거 |
|------|------|--------|
| BLOCKED | 일시 차단 (HOLD evidence 등) | gemini=HOLD, mergeable=CONFLICT 등 |
| CANCELLED | 작업 취소 (회복 불가) | `taskctl cancel`, `.cancelled` 마커 |
| FAILED | 작업 실패 (회복 불가) | `taskctl fail --reason` |
| ESCALATED | 회장 검토 필요 | self-approve 시도, admin cap 초과 등 |
| ADMIN_OVERRIDE_USED | chairman admin override 1회 사용됨 | `taskctl merge --admin` |

### 1.3 Backwards Compatibility (호환 alias)

기존 11상태와의 매핑:
- `DISPATCHED` ≡ `WORKTREE_READY` (dispatch.py 호출 호환)
- `ACKED` ≡ `WORKTREE_READY` (봇 ack 호환)
- `GUARD_PASS` ≡ `VERIFIED` (verify 명령은 두 상태 모두 진입 허용)

기존 task의 state 파일은 그대로 유지되며, `verify`/`approve`/`merge` 명령이 새 14+5 상태로 자동 전이.

## 2. 정규 전이 (ALLOWED_TRANSITIONS)

```
CREATED → WORKTREE_READY (DISPATCHED, ACKED 호환)
WORKTREE_READY → RUNNING
RUNNING → HANDOFF_READY (선택) | COMMITTED
HANDOFF_READY → RUNNING (재개) | COMMITTED
COMMITTED → PR_OPEN
PR_OPEN → CI_PENDING | PR_OPEN (재시도)
CI_PENDING → GEMINI_PENDING | PR_OPEN (실패 강등)
GEMINI_PENDING → REVIEW_READY | BLOCKED | PR_OPEN
REVIEW_READY → VERIFIED | BLOCKED | PR_OPEN
VERIFIED → HUMAN_APPROVED | PR_OPEN (재verify 강등)
HUMAN_APPROVED → MERGING | VERIFIED (재verify)
MERGING → MERGED | FAILED
MERGED → DONE
DONE → (terminal)
```

예외 진입:
- 모든 비-terminal 상태 → BLOCKED / CANCELLED / FAILED / ESCALATED
- `MERGED` 후 admin override 박제 → ADMIN_OVERRIDE_USED (terminal sibling of DONE)

Terminal 상태 (전이 불가):
- DONE / CANCELLED / FAILED / ADMIN_OVERRIDE_USED

## 3. 6개 금지 전이 (단위테스트 강제)

회장 명령 §3.9 인용:

1. `PR_OPEN` 없이 `VERIFIED` (반드시 CI_PENDING → GEMINI_PENDING → REVIEW_READY 경유)
2. `VERIFIED` 없이 `HUMAN_APPROVED` (verify 미통과 승인 차단)
3. `HUMAN_APPROVED` 없이 `MERGING` (인간 승인 없이 머지 차단)
4. `MERGED` 없이 `DONE` (origin/main merge 미확인 시 .done 차단)
5. `CANCELLED`에서 다른 상태로 복귀 (terminal)
6. `BLOCKED` 상태에서 `MERGING` 진입 (HOLD/CONFLICT 시 머지 차단)

각각 `tests/state_machine/test_transitions.py`에 명시적 케이스로 박제.

## 4. 상태 파일 (`.tasks/state/<task-id>.json`)

기존 스키마 유지 + 신규 필드:

```json
{
  "task_id": "task-2467",
  "current_state": "MERGED",
  "transitions": [
    {
      "from": "HUMAN_APPROVED",
      "to": "MERGING",
      "ts": "2026-05-06T05:30:00Z",
      "actor": "chairman <jonghyuk.jeon@gmail.com>",
      "command": "taskctl merge",
      "exit_code": 0,
      "evidence_path": ".tasks/evidence/task-2467/merge.json"
    }
  ],
  "evidence": { ... 기존 단일 dict ... },
  "human_approved": true,
  "bypass": {"used": false, ...},
  "admin_override": {
    "used": false,
    "ts": null,
    "actor": null,
    "reason": null,
    "audit_log_offset": null
  },
  "_checksum": "sha256(...)"
}
```

각 transition entry 필수 필드 (회장 §3.9):
- `from`, `to`, `ts`, `actor`, `command`, `exit_code`, `evidence_path`

## 5. Evidence 9종 (`.tasks/evidence/<task-id>/`)

회장 §3.10 인용: "필수 9종"

| # | 파일 | 진입 명령 | 핵심 필드 |
|---|------|----------|----------|
| 1 | `start.json` | init/run | command, actor, ts, branch, head_sha |
| 2 | `commit.json` | commit | git_diff_sha, changed_paths, commit_hash, branch |
| 3 | `pr-open.json` | pr-open | pr_number, pr_author (bot 강제), base_sha, head_sha |
| 4 | `ci.json` | ci-check | pr_number, ci_checks (8 required), all_pass, head_sha |
| 5 | `gemini.json` | gemini-evidence | pr_number, head_sha, app_slug, severity_count, hold_block_pass |
| 6 | `verify.json` | verify | guard_sh_result, qc_report_guard_result, hidden_path_audit |
| 7 | `approval.json` | approve | pr_author, approver (≠ pr_author), ts, ci_pass, gemini_pass |
| 8 | `merge.json` | merge | command, exit_code, merge_commit_sha, pr_state, head_sha (consistent) |
| 9 | `done.json` | done | merge_commit_sha (origin/main 확인), .done_path, ts |

각 파일 필수 필드 (회장 §3.10):
- `command`, `stdout`, `stderr`, `exit_code`, `timestamp`, `actor`, `sha`, `pr_number`

자동 기록: 모든 transition 함수에서 `_save_evidence(name, payload)` 헬퍼 호출. 누락 시 transition 실패.

## 6. 명령 사양

### 6.1 `taskctl pr-open <task-id>` (회장 §3.2)

**유일한 PR 생성 명령**. 다른 모든 경로 차단.

```python
# 동작
1. state == COMMITTED 검증
2. bot token 로드: BOT_GITHUB_TOKEN env / .env.keys
3. bot token으로 gh pr create (gh auth token override)
4. PR author == bot 검증 (author != bot이면 FAIL + ESCALATED)
5. evidence/pr-open.json 박제 (created_by=bot, head_sha, base_sha, pr_number, pr_author)
6. PR_OPEN 전이
```

**합격 기준**: PR author == bot. 100%.

### 6.2 `taskctl approve <task-id> --by <human>` (회장 §3.3)

**유일한 승인 명령**.

```python
# 조건 (모두 충족)
- state == VERIFIED
- PR author != approver (self-approve 차단)
- ci_checks 모두 SUCCESS
- gemini evidence PASS (high_severity == 0, head_sha 일치)
- taskctl verify PASS
- not CANCELLED, not BLOCKED
```

**self-approve 차단**: PR author 조회 → approver 비교 → 같으면 FAIL + ESCALATED 전이 + 텔레그램 알림 (선택).

### 6.3 `taskctl merge <task-id> [--admin]` (회장 §3.4)

**유일한 merge 명령**.

```python
# 정상 경로 조건 (--admin 없이)
- state == HUMAN_APPROVED
- PR state == OPEN
- mergeable == CLEAN
- ci_checks 모두 SUCCESS
- gemini PASS
- taskctl verify PASS
- hidden_path_audit PASS
- branch head_sha == state head_sha (no force-push)
- PR author != approver (재검증)
- not CANCELLED

# 동작
1. HUMAN_APPROVED → MERGING 전이
2. taskctl 내부에서만 `gh pr merge --merge --delete-branch` 호출
3. origin/main에서 merge commit 확인 (gh api로 sha 검증)
4. evidence/merge.json (merge_commit_sha, pr_state=MERGED)
5. MERGING → MERGED 전이
6. (그 후 taskctl done 호출하여 DONE)
```

### 6.4 `taskctl merge --admin` (회장 §3.5 admin override)

```python
# 조건
- whoami / git config user.email == chairman 검증
- 월 3회 soft limit, 분기 5회 hard cap (rolling window)
- audit log 자동 기록: memory/orchestration-audit/admin-override.jsonl

# audit log 필드 (필수)
{
  "ts": ISO8601,
  "task_id": "...",
  "pr_number": N,
  "actor": "chairman",
  "reason": "...",
  "head_sha": "...",
  "bypassed_checks": ["..."],
  "soft_count_this_month": N,
  "hard_count_this_quarter": N
}

# 결과
- ADMIN_OVERRIDE_USED 별도 상태로 전이 (DONE 대신)
```

`gh pr merge --admin` 직접 호출은 코드베이스 어디에서도 금지. taskctl 내부 1곳만 허용.

### 6.5 `taskctl done <task-id>` (회장 §3.11)

```python
# 조건
- state == MERGED
- merge commit SHA가 origin/main에 존재 (gh api로 재확인)
- done.json 생성 (merge_commit_sha, ts)
- .done 파일 생성: memory/events/<task-id>.done

# 금지
- PR open 상태 → done 진입 불가
- .merge-done 만으로 done 인정 불가
- 봇 보고만으로 done 인정 불가
```

## 7. Hidden Path Audit (회장 §3.8)

```python
# repo 전체 grep (단위테스트 강제)
def hidden_path_audit() -> bool:
    forbidden_patterns = [
        "gh pr create",
        "gh pr merge",
        "git push origin main",
        "git push --force origin main",
        "worktree_manager finish --action merge",
    ]
    excludes = [".git", "tests", "fixtures", ".worktrees", "node_modules", "__pycache__"]

    for pattern in forbidden_patterns:
        result = grep_repo(pattern, excludes=excludes)
        # taskctl.py 내부 1곳만 허용 (gh pr merge)
        violations = [hit for hit in result if "scripts/taskctl.py" not in hit.path]
        if violations:
            return False
    return True
```

`tests/taskctl/test_hidden_path_audit.py`에 박제. CI workflow에도 통합 (별도 task-2468).

## 8. Bypass / TASKCTL_BYPASS 환경변수

기존 `TASKCTL_BYPASS=1`은 backwards-compat으로 유지하되, 새 admin override (`--admin`)가 권장 경로. bypass 사용 시 evidence에 `bypass.used=true` 강제 박제 + audit log.

## 9. 외부 호출자 (라우팅 변경)

| 파일 | 기존 | 변경 후 |
|------|------|---------|
| `scripts/worktree_manager.py` action="pr" | gh pr create + gh pr merge 직접 | taskctl pr-open + (인간 승인 대기) |
| `scripts/finish-task.sh` | worktree_manager.finish --action auto | taskctl done (직접) — BLOCKED 시 .done.blocked |
| `scripts/anu_confirm_bot/main.py` | (이미 라우팅됨) taskctl merge | taskctl approve + taskctl merge 2단계 |

## 10. 전이 다이어그램

```
                    +-----------+
                    |  CREATED  |
                    +-----+-----+
                          |
                          v
              +-----------+-----------+
              |    WORKTREE_READY     |
              | (= DISPATCHED/ACKED)  |
              +-----------+-----------+
                          |
                          v
                    +-----+-----+
                    |  RUNNING  |  <----+
                    +-----+-----+       |
                          |             | (자기 → HANDOFF_READY → RUNNING)
                          v             |
                    +-----+-----+       |
                    | COMMITTED |  -----+
                    +-----+-----+
                          |
                          v
                    +-----+-----+   <----+ (재시도)
                    |  PR_OPEN  |        |
                    +-----+-----+        |
                          |              |
                          v              |
                    +-----+-----+        |
                    | CI_PENDING| -------+
                    +-----+-----+
                          |
                          v
                    +-----+-----+
                    |  GEMINI   |        +---------+
                    |  PENDING  | -----> | BLOCKED |
                    +-----+-----+        +---------+
                          |
                          v
                    +-----+-----+
                    |  REVIEW   |
                    |  READY    |
                    +-----+-----+
                          |
                          v
                    +-----+-----+
                    | VERIFIED  |  (= GUARD_PASS)
                    +-----+-----+
                          |
                          v (--by != PR author)
                    +-----+-----+
                    |  HUMAN_   |
                    | APPROVED  |
                    +-----+-----+
                          |
                          v
                    +-----+-----+
                    |  MERGING  |
                    +-----+-----+
                          |
                          v
                    +-----+-----+
                    |  MERGED   |
                    +-----+-----+
                          |
                          v
                    +-----+-----+
                    |   DONE    |
                    +-----------+

   예외 진입 (모든 비-terminal에서):
       BLOCKED / CANCELLED / FAILED / ESCALATED / ADMIN_OVERRIDE_USED
```

## 11. 청사진 §10/§11/§16 매핑

- **§10 한정승인 스키마**: `taskctl approve --by <human>`이 (1) HOLD 자동머지 차단, (2) PR 코어파일 unchanged, (3) live evidence 1건+를 검증한 후만 HUMAN_APPROVED 진입.
- **§11 merge_policy**: `taskctl merge`가 evaluate_gate (gemini_evidence_verify.py) PASS + 8 required CI checks SUCCESS + mergeable=CLEAN을 필수로 강제.
- **§16 시스템 강제 전환**: 본 명세 자체가 §16의 코드화. 한정승인은 ADMIN_OVERRIDE_USED로 시스템 audit log에 박제.

---

(끝)
