# task-2773 보고서 — Delegation Completion Watcher (workspace 모듈+ledger+테스트)

## QC Verdict
WARN

(qc-result.json verdict=WARN 일치. WARN 사유: scope_check/claude_md_check WARN — 핵심 검증(test_runner/L1/file_check/git_evidence/spec_compliance) PASS. 회귀 4종 PASS + L1 1-shot 실증 통과. 머지 정책=manual.)

- 작업 ID: task-2773
- 팀: dev1-team (헤르메스 팀장 / 불칸 백엔드 / 아르고스 테스터)
- 레벨/게이트: Lv.2 (G1/G2/G3)
- 브랜치: task/task-2773-dev1 · PR #249
- ENFORCEMENT 상태: **ENFORCEMENT_0 (CANDIDATE)** — hook wiring(~/.claude) 전까지. wiring 은 ANU 영역.

## S (Situation)
ANU 가 dispatch 한 task 의 완료 산출물(.done/.failure-envelope.json/result.json/PR)을 강제 감지하는 watcher 가 필요. 기존 "다음 턴 폴링" 텍스트 약속은 enforcement 0 으로 반복 실패. 설계: `memory/reports/delegation_completion_watcher_design_260624.md`.

## C (Complication)
- deliverable(PR/result.json 생성됨)과 closeout(.done 도달)을 분리 평가하지 않으면, supervisor-crash 나 .done 부재만으로 **완료 작업을 실패로 오판**(PR#248 실증).
- 텍스트 규율이 아닌 **실행 장치**(ledger + 디스크 확인 함수)가 필요.

## Q (Question)
merge/activation 축을 건드리지 않고, **완료감지 축만** workspace 코드(모듈+ledger 호출+테스트)로 최소 구현하려면?

## A (Answer) — 구현 내용
expected_files 정확히 3개만 수정:

### 1. utils/delegation_watcher.py (신규, self-contained stdlib)
import: `json`, `datetime`, `pathlib`, `typing` 만 (외부 의존 0).
- `record_delegation(task_id, bot, team, branch, expected_result_paths, dispatch_time, ledger_path=None)` → `memory/state/pending_delegations.jsonl` 에 status="pending" 1줄 append. 중복 task_id 는 append(audit). expected_result_paths 는 str 강제 변환 저장.
- `check_pending_delegations(workspace=None, ledger_path=None)` → ledger 순서 순회로 task_id 별 **마지막 lifecycle 이벤트가 record(active)** 인 pending 항목만 판정:
  - `.done` 존재 → `status_detected="done"`; `.failure-envelope.json` → `failure`; result.json/PR 있고 `.done` 부재 → `ready_for_adjudication`(실패 단정 금지); 산출물 0 → `pending`.
  - `.supervisor-crash-marker.json` 존재 → `crash=true` 기록하되 deliverable(result.json/.done) 있으면 `deliverable_ok=true` 로 **분리 평가**(crash 만으로 실패 단정 0).
  - 반환 dict: task_id·status_detected·detected·crash·deliverable_ok·needs_adjudication·expected_result_paths.
- `close_delegation(task_id, status, ledger_path=None)` → close 레코드 append (status ∈ {done/failure/essence_pass/merged}, 외 ValueError).
- `render_pending_briefing(items)` → hook 주입용 텍스트(없으면 빈 문자열 → 무영향).

### 2. dispatch/__init__.py (성공 지점 1줄 호출)
`dispatch()` 함수 성공 블록(`if result.returncode == 0:` 내 `return _result` 직전)에 `record_delegation(...)` 호출 추가. expected_result_paths = [`memory/events/<task>.done`, `.failure-envelope.json`, `.result.json`]. **try/except 비차단**(dispatch 흐름 무손상). dispatch.py(86줄 wrapper)·dispatch/core.py 는 미수정.

### 3. tests/regression/test_delegation_watcher_2773.py (회귀 4종)
1. `test_ledger_creation` — record 후 jsonl 항목 존재.
2. `test_result_json_separated_from_done` — result.json만(.done 부재) → ready_for_adjudication(실패 아님).
3. `test_supervisor_crash_separated` — crash-marker+result.json → crash=true·deliverable_ok=true·≠failure.
4. `test_prelude_blocking_semantics` — pending 시 briefing 비어있지 않음(주입 강제), pending 0 시 빈 문자열(무영향).

## 변경 파일 목록 (expected_files 일치 — 정확히 3개)
- `utils/delegation_watcher.py` (신규)
- `dispatch/__init__.py` (수정: 성공 지점 record_delegation 1블록, 비차단)
- `tests/regression/test_delegation_watcher_2773.py` (신규)

## 테스트 결과
- pytest `tests/regression/test_delegation_watcher_2773.py` → **4 passed**
- `py_compile` utils/delegation_watcher.py + dispatch/__init__.py → **PASS**
- ledger 1-shot 실증: record→check 4분기(pending / ready_for_adjudication / crash-분리 / done) + close 제외 + 재오픈(record→close→record) + Path 객체 방어 모두 PASS.

## L1 스모크테스트 결과 (subprocess/모듈 작업)
- 서버 재시작: **해당없음** (서버/프론트 아님 — stdlib 모듈 + dispatch ledger 호출)
- API 응답 확인: **해당없음** (HTTP API 아님)
- 스크린샷: **해당없음**
- **실동작 1-shot 실증(통과)**: 실제 디스크에 record_delegation→check_pending_delegations 실행, A(pending)/B(ready_for_adjudication)/C(crash 분리)/D(done) 4분기 + close_delegation 후 제외 + render_pending_briefing 시맨틱(pending 시 비어있지 않음, 0 시 '') 전부 assert 통과. ("L1 SMOKE PASS — record→check 1-shot 실증 완료")

## G3 / Gemini PR 리뷰 대응 (PR #249)
Gemini: HIGH 1 + MEDIUM 3.
- **[HIGH] task_id 재시도 시 close 영구 제외 버그 — 수용/수정**: `closed_ids` 무조건 제외 → 순서기반 `active_ids` 판정 교체. record→close→record 재오픈 동작. (commit de43702e, REOPEN OK)
- **[MEDIUM] Path 객체 endswith AttributeError — 수용/수정**: 순회 시 `str(p_item)` 강제 변환(3 지점 + 저장 시점). (PATH_OBJ OK)
- **[MEDIUM] ledger append race — [DISMISS]**: stdlib-only + 단일 writer(dispatch 성공 지점 1곳) + POSIX 소형 append 원자성. self-contained 범위/완료감지 축 전용 설계상 defer. (PR 코멘트 기록)
- **미수정 High 0건** → G3 PASS 조건 충족.

## self-contained 증거
utils/delegation_watcher.py import: `json`, `from datetime import datetime, timezone`, `from pathlib import Path`, `from typing import Optional`. 외부 패키지/네트워크 의존 0.

## 범위 준수 (금지/STOP 조건)
- hook 파일(~/.claude/**) 미수정 · matrix(automation_capability_matrix.json) 미수정 — **ANU 영역** 명시.
- dispatch.py(wrapper)·dispatch/core.py·finish-task.sh·gate_shadow 미수정.
- P0-B/callback/runner/activation/owner-pickup 해동 0 · 자동 merge 권한 확대 0.
- expected_files 3개 밖 수정 0.

## 발견 이슈 및 해결
- (해결) main 직접 커밋 차단 hook + start_task_guard lock 요구 → worktree(.worktrees/task-2773-dev1) 정식 생성 + start_task_guard lock 발급 후 진행.
- (해결) taskctl 상태 미초기화(cron 실행) → init→dispatch→ack→run 으로 RUNNING 전이 후 PR 라이프사이클 진입.
- (해결) Gemini HIGH/MEDIUM → 수정 커밋 de43702e.

## 모델 사용 기록
- 불칸(백엔드, sonnet): 모듈 구현 + dispatch 호출 + Gemini HIGH/Path 수정.
- 아르고스(테스터, sonnet): 회귀 4종 작성.
- 팀장(헤르메스, opus): 설계/분배/검토/통합/게이트. haiku 미사용.

## 머지 판단
- 머지 필요: **Yes**
- 브랜치: task/task-2773-dev1
- 워크트리 경로: /home/jay/workspace/.worktrees/task-2773-dev1
- 머지 의견: 회귀 4종 PASS + L1 실증 + Gemini High 수정 완료. expected_files 3개 한정, self-contained, dispatch 비차단. 충돌 위험 낮음(신규 2 + 성공지점 1블록).

## 세션 통계
- 총 도구 호출: 0회

