# task-2721+4 보고서 — PR #167 P0-b CRITICAL+HIGH fix

- 팀: dev1-team (헤르메스 팀장)
- 작업자: 불칸(백엔드), 아르고스(테스터)
- 작성일: 2026-06-02
- worktree: `/home/jay/workspace/.worktrees/task-2721-dev1` (branch `task/task-2721-dev1`, base origin/main 29175f80)
- 커밋: `4990a353` (head 818b351f → 이어서)

---

## S (Situation)
P0-b OS-level pickup driver(PR #167, head 818b351f)는 default DISABLED 상태로 구현/배선 후보까지 완료된 상태였다. 회장 Option 1(fresh CRITICAL+HIGH 명시 승인)으로, PR #167 독립 파서가 새로 식별한 CRITICAL 1건 + HIGH 1건을 expected_files 6개 내부 bounded fix로 해소하라는 지시를 받았다.

## C (Complication)
- **CRITICAL — 무한 재트리거**: 처리 완료된 `task-*.result.json`이 `PathExistsGlob` 대상에 계속 남아 systemd path watcher가 무한 재감지/재실행. WAKE_BUILT/PICKUP_SKIP 도달분이 watched 디렉터리(`memory/events`)에 잔류하던 것이 근본 원인.
- **HIGH — /tmp lock**: driver-level flock lock 파일이 `${XDG_RUNTIME_DIR:-/tmp}` fallback으로 world-writable `/tmp`를 쓸 수 있어 symlink/truncation 위험.

## Q (Question)
PR hold/전면 재설계 없이, result writer를 건드리지 않고(소비 측 이동만), 6개 파일 내부에서 무한 재트리거를 차단하고 lock 경로를 안전화할 수 있는가?

## A (Answer) — 수정 내역

### 수정 1 — CRITICAL: terminal 파일 이동 (`dispatch/anu_pickup_driver.py`)
처리 완료(terminal verdict)된 result 파일을 watched 밖 `memory/p0b_state/processed/`로 **atomic 이동**(`os.replace` 우선, 실패 시 `shutil.move` fallback)하여 glob 재매칭을 0으로 만든다.

- 신규 상수 `PROCESSED_DIR_REL = "memory/p0b_state/processed"` (+ `__all__` 등록).
- 신규 helper `_collision_safe_dest()` — overwrite 방지(ms 타임스탬프 suffix).
- 신규 helper `_move_processed()` — atomic 이동 + fail-safe(크래시 0, 실패 시 error 메시지 반환).
- terminal verdict별 정책:
  - `WAKE_BUILT` → processed 이동 → glob 비대상.
  - `PICKUP_SKIP`(dedupe/done/acked, 또는 pickup_once SKIP_TERMINAL/SKIP_DEDUPE) → processed 이동 → glob 비대상.
  - `QUARANTINE` → 기존대로 `quarantine/` 이동(변경 없음).
  - `NOOP_NOT_READY`(DEFER) → **그대로 잔류**(다음 트리거 재평가, terminal 아님).
  - `NOOP_NOT_TARGET`/`NOOP_DISABLED`/`FIRE_FAILED` → 파일 미이동.
- 이동 실패 시 `DriverRecord.error`에 사유 기록(verdict 유지), 크래시 0. result writer atomic화는 미적용(P0-b+1 후속 유지).

### 수정 2 — HIGH: lock 경로 안전화 (`scripts/anu_pickup_entrypoint.sh`)
- `/tmp` 고정 fallback **전면 중단**(파일 내 `/tmp` 문자열 0건).
- `XDG_RUNTIME_DIR`(사용자 전용 0700) 우선 → 없으면 workspace 내부 `memory/p0b_state/locks/`.
- `mkdir -p` + `chmod 700`(소유자 한정)으로 parent dir 존재/권한 보장.
- lock 경로가 symlink면(`[[ -L ]]`) 차단(exit 0) — truncation/redirect 방지.
- driver-level flock single-flight + UID 분리 유지. pickup_once 내부 lock 재도입 0(lock-free 유지).

### 수정 3 — medium: quarantine overwrite 방지
- 동일 basename 재이동 시 `_collision_safe_dest`가 suffix를 붙여 덮어쓰기 방지. quarantine·processed 양쪽 적용.

---

## 수정/생성 파일 목록
1. `dispatch/anu_pickup_driver.py` — terminal 이동 로직(주 수정)
2. `scripts/anu_pickup_entrypoint.sh` — lock 경로 안전화
3. `tests/regression/test_anu_pickup_driver_2721.py` — 회귀 8건 추가(26→34)
4. `docs/p0b_driver_runbook_260601.md` — §10 CRITICAL+HIGH fix 이력 추가
- (`deploy/systemd/anu-pickup.path`, `anu-pickup.service`는 +4에서 수정 불필요 — origin/main 대비 diff는 여전히 6파일 유지)

## 테스트 결과
- `python3 -m pytest tests/regression/test_anu_pickup_driver_2721.py -q` → **34 passed** (기존 26 + 신규 8). 기존 회귀 0.
- 신규 테스트: WAKE_BUILT 이동+glob 재매칭 0 / done marker PICKUP_SKIP 이동 / pickup_fn SKIP_TERMINAL 이동 / DEFER 잔류(재평가 대상) / scan_once 후 watched 0건 / 이동 fail-safe(os.replace+shutil.move 동시 실패 시 크래시 0) / quarantine overwrite 방지 / entrypoint lock 정적검사(/tmp 0 + XDG + mkdir + symlink 가드 + flock).
- pickup_once 전부 mock/stub. 실 pickup/wake/ANU key wake 실행 0. key literal 0.

## L1 스모크테스트 결과 (필수 기록)
- **서버 재시작**: 해당없음 (systemd unit 미설치/미활성 작업 — active=false 유지가 요구사항).
- **API 응답 확인**: 해당없음 (HTTP API 아님).
- **실 프로세스 구동 검증(subprocess L1)**: 임시 root에서 driver `scan_once`를 실제 Python 프로세스로 구동(mock pickup/verify, 실 wake 0):
  - verdict=`['WAKE_BUILT']`, watched glob 잔류=`[]`(기대 []), processed 이동=`task-999.result.json`, evidence jsonl 생성=True, evidence 내 ANU key 누출=False → **PASS**.
- **entrypoint 검증**: `bash -n` 문법 OK. 샌드박스 HOME 실행 — flag 부재 시 default-disabled no-op(exit 0). flag enabled + XDG 없음 → workspace fallback lock 디렉토리 `memory/p0b_state/locks` 0700 권한 생성 확인(/tmp 미사용) → **PASS**.
- **스크린샷**: 해당없음 (프론트엔드 작업 아님).

## 검증 (finalize 전)
- goal_assertion ①: pytest 34 PASS.
- goal_assertion ②: `/tmp/anu-pickup` not in entrypoint.sh → PASS (전체 `/tmp` 0건).
- goal_assertion ③: driver `(os.replace or shutil.move) and 'processed'` → PASS.
- `git diff --name-only origin/main` = **6 파일** (그 외 0). `finish-task.sh` 수정 0. ANU key net-new 0.
- terminal 파일 이동/rename 로직 존재. `/tmp` 고정 lock 0. parent dir 보장 + symlink 방어 존재.
- active=false / installed=false / wired=false 유지. systemctl enable/start/daemon-reload/unit 설치/activation flag 생성 0. merge/push/PR/force push/rebase 0.

## 발견 이슈 및 해결
- 없음(추가 신규 HIGH/CRITICAL 미발생). 범위 내 bounded fix로 완결. CHAIR_REQUIRED 사유 없음.

## 모델 사용 기록
- 불칸(백엔드): sonnet — driver 이동 로직 + entrypoint lock 안전화 (로직 구현).
- 아르고스(테스터): sonnet — 회귀 테스트 8건 (테스트 작성).
- 헤르메스(팀장): Opus — 설계/분배/검토/통합 + runbook 문서 통합 + L1 스모크 검증.
- haiku 미사용 (안정성 핵심 로직 — sonnet 이상 필수).

## 머지 판단
- **머지 필요**: Yes (단, **ANU governance 수행** — 본 task는 로컬 commit only, push/PR/merge 금지).
- **브랜치**: `task/task-2721-dev1`
- **워크트리 경로**: `/home/jay/workspace/.worktrees/task-2721-dev1`
- **머지 의견**: 6파일 bounded fix, regression 34 PASS, goal_assertion 3건 PASS, active=false 유지. ANU 독립 재검증(6파일/terminal 이동·glob 재매칭 0/DEFER 잔류/lock 안전·/tmp 0/regression/key 0/active=false) 후 FF push → OWNER `/gemini review` 1회 → CI GREEN + fresh unresolved 0 시 MERGE_READY_CANDIDATE. merge는 회장 승인 전 금지.

## 금지 준수 요약
finish-task.sh 무수정 · systemctl/daemon-reload/unit 설치/flag 생성 0 · 실 pickup/wake/ANU key wake 0 · merge/push/PR/force push/rebase 0 · ANU key literal 0 · pickup_once lock 재도입 0 · P0-a main 무수정 · expected_files 밖 수정 0 · active=false 유지.

## 결론 (이슈 해결 상태)
- 본 task가 대상으로 한 무한 재트리거 이슈(terminal 파일 잔류) + /tmp lock 위험 이슈 2건을 식별.
- 위 2건 모두 **수정 완료**(resolved) — terminal 파일 processed 이동(glob 재매칭 0) + lock 경로 안전화로 fixed. regression 26→34 PASS 및 L1 실프로세스 스모크로 검증 완료.
- 잔여 미해결 이슈 없음. 추가 신규 고위험/치명 이슈 미발생으로 CHAIR_REQUIRED 사유 없음 — 전 항목 해결 종료.
