# Worker reap

> lease 상태 issued→reaped, settled 검증, terminal close, worktree 정리, --dry-run·--ledger, 거부 exit 코드.

- Repository: local/mogui-ADE-orchestrator

- Human docs: https://grok-wiki.com/public/docs/local-mogui-ade-orchestrator-97afe791d5ac
- Complete Markdown: https://grok-wiki.com/public/docs/local-mogui-ade-orchestrator-97afe791d5ac/llms-full.txt

## Source Files

- `docs/runbooks/worker-reap.md`
- `scripts/worker-reap`
- `src/master_runtime/core/worker_reap.py`
- `tests/test_worker_reap.py`
- `docs/public/reference.md`
- `docs/public/delegation-and-review.md`

---

---
title: "Worker reap"
description: "lease 상태 issued→reaped, settled 검증, terminal close, worktree 정리, --dry-run·--ledger, 거부 exit 코드."
---

`scripts/worker-reap`는 settled 디스패치의 워커 자원을 회수하는 공개 CLI입니다. `orca orchestration dispatch-show`로 상태를 읽고, settled가 아니면 거부(exit 3)하며, settled이면 `orca terminal close`로 터미널을 닫고 안전할 때만 worktree를 제거합니다. 구현은 `WorkerReaper`(`src/master_runtime/core/worker_reap.py`)이고, 감사 기록은 `--ledger`로 넘긴 JSONL에 `event: "reap"` 한 줄로 남습니다. 자동 스윕·타이머 재프는 없습니다.

## 역할과 경계

완료 보고 처리의 끝은 검증·머지 결정만이 아니라 **reap + ledger 기록**입니다. settled 워커를 방치하면 harness debris가 됩니다. 잘못된 reap(열린 작업·미머지 브랜치 삭제) 비용이 높고 스킵 비용은 낮으므로, 애매한 경우는 항상 “남기고 사유를 보고”합니다.

| 구분 | 동작 |
|------|------|
| 공개 표면 | `scripts/worker-reap` |
| 코어 | `WorkerReaper`, `DispatchState`, `ReapRecord`, `ReapError` |
| 상태 조회 | `orca orchestration dispatch-show --json` (`--task` 또는 `--dispatch`) |
| 터미널 | `orca terminal close <terminal_id>` |
| worktree | `git worktree remove` (안전 조건 통과 시에만) |
| 감사 | `--ledger` JSONL append (`event: "reap"`) |
| 잔여 탐지 | `ReapObservability.unreaped_settled_leases()` (`work_ledger`) |

개념상 lease 수명은 `issued` → `running` → `submitted` → `accepted` → `reaped`입니다. 재프 게이트가 실제로 검사하는 Orca 디스패치 상태는 아래 settled 집합입니다.

## Lease·settled 판정

`DispatchState.is_settled()`는 status를 대문자로 정규화한 뒤 다음만 settled로 봅니다.

| status | settled | success (`is_success`) |
|--------|---------|------------------------|
| `COMPLETED` | 예 | 예 |
| `ACCEPTED` | 예 | 예 |
| `FAILED` | 예 | 아니오 |
| `ABANDONED` | 예 | 아니오 |
| `RUNNING` | 아니오 | 아니오 |
| `REGISTERED` | 아니오 | 아니오 |
| 그 외(예: `ISSUED`) | 아니오 | 아니오 |

`task_id`/`dispatch_id`가 둘 다 없으면 `ReapError` exit **2**. settled가 아니면 exit **3** — 메시지 형태:

```text
Dispatch <id> is not settled (status: RUNNING); refusing to reap open dispatch
```

## 실행 흐름

```mermaid
sequenceDiagram
  participant CLI as scripts/worker-reap
  participant WR as WorkerReaper
  participant O as orca
  participant G as git
  participant L as ledger JSONL

  CLI->>WR: reap(task_id|dispatch_id, execute)
  WR->>O: orchestration dispatch-show --json
  alt not settled
    WR-->>CLI: ReapError exit 3
  else settled
    opt terminal_id present and execute
      WR->>O: terminal close
    end
    opt worktree_path present
      WR->>G: status / branch / merge-tree
      alt clean and included in origin/main
        opt execute
          WR->>G: worktree remove
        end
      else unsafe or ambiguous
        Note over WR: worktree_left + reason
      end
    end
    opt execute and --ledger
      WR->>L: append event=reap
    end
    WR-->>CLI: ReapRecord JSON
  end
```

### 1. Settled 검증

`dispatch-show` JSON에서 `dispatch_id`, `task_id`, `terminal_id`, `status`, `worktree_path`를 읽습니다. stdout 파싱 실패 시 exit **4**. orca 자체 실패 시 runner가 반환한 code를 그대로 전파합니다.

### 2. Terminal close

`terminal_id`가 있으면:

- `execute=True`: `orca terminal close <terminal_id>` 호출. 실패 시 `ReapError` (runner code).
- 성공·dry-run 모두 `actions_taken`에 `terminal_closed:<terminal_id>` 기록.

터미널이 비어 있으면 이 단계는 생략됩니다.

### 3. Worktree 안전 검사 후 제거

`worktree_path`가 있을 때만 검사합니다. 제거 조건은 **둘 다** 참이어야 합니다: working tree clean **그리고** 브랜치 변경이 `origin/main`에 포함.

검사 순서:

1. 경로 존재 — 없으면 leave
2. `git status --porcelain` — 비어 있지 않으면 leave (`Worktree has uncommitted changes`)
3. `git branch --show-current` — 실패·detached HEAD면 leave
4. `git branch -a --merged origin/main` — 현재 브랜치가 목록에 있으면 포함으로 간주
5. 토폴로지 미머지 시 squash 대응: `origin/main^{tree}`와 `git merge-tree --write-tree origin/main HEAD` 결과가 같으면 “이미 포함”으로 제거 허용

통과 시 `git worktree remove <path>` → `worktree_removed:<path>`.  
실패·애매 시 제거하지 않고 `worktree_left:<path>:<reason>`. 검사 예외도 leave로 흡수합니다 (`Check failed: ...`).

제거 명령 자체가 실패하면 `ReapError`로 중단합니다 (exit = runner code, 기본 실패 경로 1).

### 4. 레코드·ledger

`ReapRecord` 필드:

| 필드 | 설명 |
|------|------|
| `task_id` | 디스패치의 task |
| `dispatch_id` | 디스패치 ID |
| `terminal_id` | 닫은(또는 닫을) 터미널 |
| `worktree_path` | 검사 대상 경로 또는 null |
| `actions_taken` | `;`로 이은 액션 문자열 |
| `timestamp` | Unix epoch float (`time.time()`) |

`--ledger`가 있고 `execute=True`일 때만 append. dry-run은 ledger에 쓰지 않습니다. 부모 디렉터리는 자동 생성됩니다.

Ledger 한 줄 스키마 (`sort_keys=True`, compact separators):

```json
{"actions_taken":"terminal_closed:term_123;worktree_left:/path/to/wt","dispatch_id":"dispatch_xyz","event":"reap","task_id":"task_abc123","terminal_id":"term_123","timestamp":1722787200.0,"ts":1722787200.0,"worktree_path":"/path/to/wt"}
```

`ts`와 `timestamp`가 둘 다 들어갑니다 (`ts`는 append 시 복제).

## CLI

```bash
scripts/worker-reap (-h) (--task-id TASK_ID | --dispatch-id DISPATCH_ID)
                   [--ledger LEDGER] [--dry-run] [--json]
```

<ParamField body="--task-id" type="string" required>
Task ID로 디스패치를 조회합니다. `--dispatch-id`와 상호 배타, 둘 중 하나 필수.
</ParamField>

<ParamField body="--dispatch-id" type="string" required>
Dispatch ID로 직접 조회합니다. `--task-id`와 상호 배타.
</ParamField>

<ParamField body="--ledger" type="path">
reap 감사 JSONL 경로. 실행 모드에서만 append. 절차상 감사 기록이 필요하면 지정합니다.
</ParamField>

<ParamField body="--dry-run" type="boolean">
close/remove/ledger 없이 계획된 `actions_taken`만 산출. 응답에 `"dry_run": true`.
</ParamField>

<ParamField body="--json" type="boolean">
stdout를 compact JSON으로 출력. 기본은 indent=2 pretty JSON.
</ParamField>

### 표준 절차

<Steps>
  <Step title="Dry-run으로 settled·액션 확인">
    ```bash
    scripts/worker-reap --task-id <task-id> --dry-run
    # 또는
    scripts/worker-reap --dispatch-id <dispatch-id> --dry-run
    ```
    exit 3이면 아직 열린 디스패치입니다. `orca orchestration dispatch-show`로 상태를 확인한 뒤 완료를 기다립니다.
  </Step>
  <Step title="실행과 ledger 기록">
    ```bash
    scripts/worker-reap \
      --task-id <task-id> \
      --ledger master-ops/ledger/dispatch-ledger.jsonl
    ```
    성공 시 exit 0, stdout에 `record` + `dry_run: false`.
  </Step>
  <Step title="부분 reap 해석">
    `actions_taken`에 `terminal_closed`만 있고 `worktree_left`가 있으면 터미널은 닫혔고 worktree는 수동 정리 대상입니다. dirty·unmerged·I/O 사유를 읽고 커밋·머지·수동 삭제 후 필요 시 다시 재프합니다.
  </Step>
</Steps>

<RequestExample>
```bash
scripts/worker-reap --task-id task_abc123 --dry-run
```
</RequestExample>

<ResponseExample>
```json
{
  "record": {
    "task_id": "task_abc123",
    "dispatch_id": "dispatch_xyz",
    "terminal_id": "term_123",
    "worktree_path": "/path/to/worktree",
    "actions_taken": "terminal_closed:term_123;worktree_removed:/path/to/worktree",
    "timestamp": 1722787200.0
  },
  "dry_run": true
}
```
</ResponseExample>

## Exit 코드

| Code | 의미 |
|------|------|
| 0 | 성공 (dry-run 포함, settled 통과 후 레코드 출력) |
| 1 | 기타 실패 (터미널 close 실패, worktree remove 실패, 타임아웃 등; runner code가 1인 경우) |
| 2 | `--task-id`/`--dispatch-id` 없음 (argparse 상호배타 그룹 또는 API 둘 다 누락) |
| 3 | 디스패치 not settled — open 재프 거부 |
| 4 | dispatch-show stdout JSON 파싱 실패 |

stderr에 `ReapError` 메시지가 찍히고 위 코드로 종료합니다. orca/git runner가 0이 아닌 code를 주면 그 값이 그대로 전달될 수 있습니다.

## 안전 가드

| 가드 | 동작 |
|------|------|
| Never auto-kill | 타이머·백그라운드 스윕 없음. 오퍼레이터 또는 마스터 시퀀스의 명시 호출만 |
| Never reap open | `RUNNING`/`REGISTERED` 등 non-settled → exit 3 |
| Ambiguous worktree stays | dirty, unmerged, unique changes, missing path, git 메타 오류, 검사 예외 → leave + reason |
| Squash-aware include | ancestry 없이도 `merge-tree` 트리 동등으로 포함 증명 가능 |
| Ledger as evidence | 실행 시 `--ledger`로 `event: "reap"` 감사 흔적 |

핵심 원칙: **잘못된 reap는 비싸고, 스킵은 싸다.**

## 잔여(debris) 관측

`ReapObservability`는 동일 ledger를 읽어 settled인데 reap 이벤트가 없는 디스패치를 반환합니다. 추적 이벤트:

- `dispatch_submitted` — 상태 시드
- `dispatch_completed` — status 갱신
- `reap` — `is_reaped=True`, `reaped_at` from `ts`

```python
from master_runtime.core.work_ledger import ReapObservability

obs = ReapObservability("master-ops/ledger/dispatch-ledger.jsonl")
for dispatch_id, state in obs.unreaped_settled_leases().items():
    print(f"{dispatch_id}: {state.status} (settled, not yet reaped)")
```

settled 집합은 reaper와 동일합니다: `COMPLETED`, `ACCEPTED`, `FAILED`, `ABANDONED`.

## actions_taken 토큰

| 토큰 | 의미 |
|------|------|
| `terminal_closed:<id>` | 터미널 close 실행 또는 dry-run 계획 |
| `worktree_removed:<path>` | 안전 통과 후 제거(또는 dry-run 계획) |
| `worktree_left:<path>:<reason>` | 제거하지 않음 + 사유 |

여러 액션은 `;`로 연결됩니다. 예: `terminal_closed:term1;worktree_left:/wt:Worktree has uncommitted changes`.

## 문제 해결

| 증상 | 확인 | 조치 |
|------|------|------|
| `not settled; refusing to reap` | `orca orchestration dispatch-show --task <id> --json` | 완료·실패·포기까지 대기 후 재시도 |
| `Worktree has uncommitted changes` | worktree `git status` | 커밋/푸시 또는 수동 정리 후 재프, 또는 터미널만 닫힌 부분 reap 수용 |
| `not merged` / `branch changes are not included` | 브랜치 vs `origin/main`, squash 여부 | 머지 후 재프, 또는 수동 worktree 정리 |
| `Could not parse dispatch JSON` (exit 4) | dispatch-show stdout | Orca/selector·JSON 출력 확인 |
| `Failed to fetch dispatch` | orca exit ≠ 0 | task/dispatch ID, Orca 런타임 가용성 |
| `Failed to close terminal` | terminal id | 이미 닫힌 세션·권한·Orca 상태 |
| `Failed to remove worktree` | `git worktree list` | 잠금·경로·수동 `git worktree remove` |

subprocess 타임아웃은 30초입니다. 타임아웃 시 runner는 code 1과 타임아웃 메시지를 반환합니다.

## 마스터 시퀀스에서의 위치

차터 **Worker Reap Duty**: 완료 검증·수락 후 `scripts/worker-reap --task-id <id> --ledger <path>`로 회수합니다. 워커 PR 머지 후에도 동일 호출로 자원을 회수하고, 애매한 worktree는 제거하지 않습니다. 디스패치 게이트(`dispatch-gate` check → launch → register)와 acceptance 판정 이후 단계이며, 재프 자체가 acceptance를 대체하지 않습니다.

## Related pages

<CardGroup cols={2}>
  <Card title="Supervised dispatch" href="/supervised-dispatch">
    check → dispatch → register, 계약 해시·ledger, 완료 채널.
  </Card>
  <Card title="증거와 수락" href="/evidence-and-acceptance">
    self-report와 독립 검증, acceptance 판정 — reap 직전 단계.
  </Card>
  <Card title="CLI 레퍼런스" href="/cli-reference">
    `scripts/` 공개 명령 표와 worker-reap 요약.
  </Card>
  <Card title="dispatch-gate 레퍼런스" href="/dispatch-gate-reference">
    ledger 스키마·reason code·티켓 TTL.
  </Card>
  <Card title="방어 인벤토리" href="/defense-inventory">
    디스패치·placement·redaction 등 가드 표.
  </Card>
  <Card title="Troubleshooting" href="/troubleshooting">
    placement mismatch, probe 실패, seat·revival 복구.
  </Card>
</CardGroup>
