# 신호등 ↔ enforcement(.done 다단계) Sync 결함 분석

> **작성**: 2026-05-05 / 아누
> **요청자**: 제이회장님 — "신호등 끄는 로직만 심층 분석"
> **원칙**: 환각 금지. 코드/실제 파일 직접 grep + JSON 읽기. 모르는 부분은 "확인 불가" 명시.
> **단일 소스**: `/home/jay/workspace/memory/specs/traffic-light-spec.md` (2026-04-15, 본 task 후 v2) vs 실제 코드.
>
> **[2026-05-05 정정 — task-2452]** 본 보고서 작성 시 "auto_merge가 .done.escalated/.done.merging/.done.clear 마커를 만든다"고 추정한 부분은 사실이 아닌 것으로 회장 직접 진단 + 코드 grep 검증 완료:
>   - `auto_merge.py escalate()` (L400-444): cokacdir 메시지만 발송, 파일 생성 없음.
>   - `auto_merge.py _finalize_done_file()` (L1095-1107): `log_result()` 호출만, .done 파일 삭제도 안 함.
>   - `try_claim()` (L131-153): `.done.merging`만 생성 (선점 마커, escalated 아님).
>   - 결국 `.done.escalated`/`.done.clear` 마커는 auto_merge.py가 아닌 **출처 미상**의 외부 경로에서 생성됨 (별도 추적 권고).
>   - **실제 머지 경로 3개**: A) `finish-task.sh` → `worktree_manager.py finish` (hot path), B) `taskctl.py merge` → `gh pr merge`, C) GitHub `merge_group` + Ruleset(active).
>   - **task-2452에서 적용한 정정**: auto_merge.py 폐기 + 신호등 단일 책임을 `finish-task.sh` 끝부분(Step 2.99)으로 통합 + `done-watcher.py`를 30분 grace fallback only로 강등.

---

## 0. 결론 요약 (회장 먼저 읽을 것)

신호등 스펙은 **2026-04-15에 작성**되었고, 그 이후 **2026-05-04~05 enforcement 다단계(.done.acked / .done.clear / .done.merging / .done.escalated / .done.rejected)가 도입**되면서 **스펙과 실구현이 mismatch**.

**결정적 결함 5개**:

1. **stale processing**: `bot-activity.json`에 봇 status=`processing`인데 task-timers.json에 running task가 없는 좀비 상태 다수 (현재 8개 봇 중 7개). `since`는 stale, 카운터(53h 등)가 거짓.
2. **whisper의 우회 로직**: whisper-compile은 stale을 우회해서 "task-timers running 기준"으로 [팀] 줄을 다시 판단(L640) → 표시는 "유휴"로 보이지만 **bot-activity의 진실 상태가 자동 보정되지 않음** → 카운터가 영원히 stale.
3. **escalated dead-lock** [정정 2026-05-05 task-2452]: task-2451은 `.done` + `.done.escalated` 동시 존재 → done-watcher는 `.done.escalated` 마커 보고 처리 스킵, whisper-compile은 `.done`만 보고 [완료] 큐에 표시 → **회장 수동 acked 없으면 무한 큐에 박혀있음**. 원인 정정: 마커는 `auto_merge.py`가 만드는 것이 **아님 (코드 grep 0건)**. **마커 출처 미상 + done-watcher의 escalated 스킵 로직**의 조합. task-2452 Phase 3에서 done-watcher를 30분 grace + `.merge-done` 부재 조건의 fallback only로 강등하여 자동 해소.
4. **team_id 빈 문자열** [정정 2026-05-05 task-2452]: task-2451 task-timer의 `team_id: ""`. composite 작업이라 단일 팀 매핑 안 됨. done-watcher의 `extract_team_from_done_file()`은 파일명 패턴(`task-XXX.devN.done`)에서 추출하는데, `task-2451.done`은 패턴 불일치 → **신호등 끄기 시작도 못함**. task-2452 Phase 3에서 `team`/`composite_teams[0]` fallback 추가로 해소.
5. **다단계 신호등 시점 미정의** [정정 2026-05-05 task-2452]: 스펙은 ".done → 전원 유휴"라고만 적힘. 그러나 실제 .done은 5개 후속 상태로 분기. 어느 단계에서 신호등 꺼야 하는지 코드/스펙 어디에도 명확치 않음. task-2452 Phase 4의 `traffic-light-spec.md` v2에서 "단일 책임 = `finish-task.sh` 끝부분, 다단계 마커는 머지 라이프사이클 표시일 뿐 신호등 트리거 아님"으로 정의.

---

## 1. 신호등 스펙 (단일 소스, 2026-04-15)

`memory/specs/traffic-light-spec.md`:

```
신호등 상태: 유휴(회색), 대기(노란), 작업중(초록) 3종만 존재.

핵심 전이 규칙:
1. dispatch 위임 → 팀장: 작업중, 팀원 전원: 대기(노란)
2. 팀장이 subagent 호출 → 해당 팀원만: 작업중(초록)
3. 팀원 개별 완료 → 대기(노란) 복귀 (유휴 아님)
4. 팀 전체 완료(.done) → 전원 유휴(회색)
```

→ **트리거 = `.done` 생성**. 단일 단계 가정.

---

## 2. 신호등 끄는 로직 — 실제 코드 흐름

### 2.1 진실 source: `memory/events/bot-activity.json`

```json
{
  "bots": {
    "anu":  {"status": "processing", "since": "2026-05-05T04:56:26Z"},
    "dev1": {"status": "processing", "since": "2026-05-05T04:25:16Z"},
    "dev2": {"status": "idle",       "since": "2026-05-05T04:27:12Z"},
    "dev3": {"status": "processing", "since": "2026-05-05T04:25:17Z"},
    "dev4": {"status": "processing", "since": "2026-05-05T04:25:16Z"},
    "dev5": {"status": "processing", "since": "2026-05-02T23:32:33Z"},
    "dev6": {"status": "processing", "since": "2026-05-05T04:25:14Z"},
    "dev7": {"status": "processing", "since": "2026-05-02T23:18:52Z"},
    "dev8": {"status": "processing", "since": "2026-05-02T22:56:23Z"}
  }
}
```

**현재 시각 기준 stale 의심**: dev5/7/8은 `since`가 5/2부터 약 53시간째 processing — 사실상 좀비.

### 2.2 갱신자 1: `done-watcher.py` (메인 신호등 끄기)

`scripts/done-watcher.py` — `.done` 파일 감지하여 봇 idle 전환.

```python
# L194-204: scan_done_files()
processed_exts = [".acked", ".clear", ".merging", ".escalated"]
for done_file in EVENTS_DIR.glob("*.done"):
    if done_file.suffix != ".done": continue
    if any((EVENTS_DIR / (done_file.name + ext)).exists() for ext in processed_exts):
        continue  # ← 마커 있으면 처리 스킵
    done_files.append(done_file)
```

```python
# L136-144: extract_team_from_done_file()
match = re.match(r"task-\d+\.\d+\.(\w+)\.done$", name)
# task-648.1.dev1.done → "dev1"
# task-2451.done → 매치 실패 → None 반환
```

```python
# L164-186: set_bot_idle()
if bots[team_id].get("status") == "idle":
    return True  # ← 이미 idle이면 since 갱신 안 함
data["bots"][team_id]["status"] = "idle"
data["bots"][team_id]["since"] = utc_now
```

**핵심 관찰**:
- `.done.escalated` 등 마커가 있으면 **건드리지 않음** — Tier 2/3 escalate된 task의 신호등은 영영 안 꺼짐
- `extract_team_from_done_file()`은 `task-XXX.devN.done` 패턴만 인식. `task-XXX.done` (single team_id 부재)은 None 반환
- 이미 idle이면 since 갱신 안 함 → stale since가 그대로 누적

### 2.3 갱신자 2: `bot-status-watchdog.py` (스턱 봇 강제 idle)

```python
# scripts/bot-status-watchdog.py 헤더
"""processing 상태가 30분 이상 지속되면 자동 idle 전환
프로세스 생존 체크 추가: 실제 claude 프로세스가 살아있으면 idle 전환 보류"""
TIMEOUT_MINUTES = 30
DAEMON_INTERVAL = 300  # 5분
```

→ 30분 이상 processing이면 **프로세스 살아있는지 체크 후 idle 전환**.
→ 프로세스 없으면 강제 idle. **그러나 dev5/7/8이 53h째 processing인데도 안 꺼진 것을 보면 watchdog가 동작 안 하고 있거나 프로세스 살아있다고 판단 중**.
→ **확인 필요**: 이 watchdog가 실제로 systemd/cron에 돌고 있는지.

### 2.4 갱신자 3: `auto_merge.py` (Tier 분류 → escalate) — **[정정 2026-05-05 task-2452]**

본 보고서 작성 시 아래와 같이 추정했으나, **회장 직접 진단 + 코드 grep 검증 결과 사실이 아님**:

```python
# (추정 — 사실 아님)
# scripts/auto_merge.py — Tier 분류 후 마커 생성
# Tier 3: .done.escalated 마커 + escalate
# Tier 2: .done.escalated 마커 + 1-tap 승인 큐
# Tier 1: 자동 머지 → .done.clear 마커
```

**실제 검증 결과**:
- `auto_merge.py` 어디에도 `.done.escalated` 또는 `.done.clear` 파일 생성 코드 **0건** (grep 검증).
- `escalate()` (L400-444): cokacdir 메시지만 발송, 파일 생성 없음.
- `_finalize_done_file()` (L1095-1107): `log_result()` 호출만, .done 파일 삭제도 안 함.
- `try_claim()` (L131-153): `.done.merging`만 생성 (선점 마커, escalated 아님).
- 더 결정적: `logs/auto_merge.log` 24MB 최근 1000줄 = **227건 모두 `[BLOCKED]`** (TASKCTL_INVOKED 가드로 자기 자신 차단). 머지 0건.

**진실**:
- `.done.escalated` / `.done.clear` 마커의 **생성자는 미상** (auto_merge 아님). task-2451의 0byte `.done.escalated` (5/5 13:56) 출처 불명. 별도 추적 권고.
- **머지의 진짜 경로 3개**: A) `finish-task.sh` → `worktree_manager.py finish` (hot path), B) `taskctl.py merge` → `gh pr merge`, C) GitHub `merge_group` + Ruleset(active).
- **task-2452에서 적용한 정정**:
  - Phase 1: `auto_merge.py` cron 제거 + DEPRECATED 마커 (30일 유예 후 삭제).
  - Phase 2: 신호등 단일 책임을 `finish-task.sh` Step 2.99로 통합 (member-status + bot-activity 동시).
  - Phase 3: `done-watcher.py`를 30분 grace + `.merge-done` 부재 fallback only로 강등.

### 2.5 표시자: `whisper-compile.py` (대시보드 [팀] 줄)

`scripts/whisper-compile.py` L600~654:

```python
elif status == "idle" or (status == "processing" and not running_tasks):
    # "idle" 이거나, "processing"이지만 실제 running task가 없으면 → 유휴
    since_str = info.get("since", "")
    idle_h = _idle_hours(since_str)
    team_parts.append(f"{team_name}:유휴({idle_h}h)")
```

**핵심 관찰**:
- bot-activity가 stale processing이어도 **task-timers running task가 없으면 "유휴"로 표시**
- **그러나 since는 bot-activity 그대로 사용** → 카운터(53h 등) stale 누적
- 즉 **whisper는 표시는 정확하게 보정**하지만 **카운터는 거짓**

### 2.6 [완료] 큐: `whisper-compile.py` `scan_done_files()` (L133)

```python
for f in events_dir.iterdir():
    if not name.endswith(".done"): continue
    # ← 단순 .done 파일만 검사. .done.escalated 같은 마커 존재 여부 무시
    results.append(json.loads(f.read_text()))
```

**핵심 관찰**: whisper [완료] 큐는 `.done` 파일이 살아있으면 무조건 표시. `.done.escalated` 마커 있어도 [완료]에 뜸.
→ **회장이 `.done.acked` rename 해야만 [완료] 큐에서 사라짐**.

---

## 3. task-2451 실제 상태 (현재 진단 사례)

### 3.1 events 디렉터리

```
task-2451.anu-notified
task-2451.completion.txt
task-2451.done                ← 살아있음
task-2451.done.escalated      ← Tier 2/3 escalate 마커
task-2451.done.notified
task-2451.qc-done
task-2451.qc-result
task-2451.retry_count
task-2451.scope-diff.txt
task-2451.scope-guard-done
```

### 3.2 task-timers.json `task-2451`

```json
{
  "task_id": "task-2451",
  "team_id": "",                  ← 비어있음
  "description": "",
  "project_id": "system",
  "work_level": "",
  "status": "completed",
  "qc_result": "WARN"
}
```

### 3.3 .done 파일 내용

```json
{
  "task_id": "task-2451",
  "team_id": "",                  ← 비어있음
  "qc_result": "WARN",
  "gate_results": {...all PASS},
  "status": "done"
}
```

### 3.4 진단

| 단계 | 동작 | 결과 |
|------|------|------|
| done-watcher: scan | `.done.escalated` 존재 → 스킵 | **봇 idle 전환 안 함** |
| done-watcher: team_id | `task-2451.done` 패턴 미일치 → None | (앞에서 스킵되어 도달 안 함) |
| auto_merge: Tier 분류 | Tier 3 (보호 경로/Lv.3+) → escalate | `.done.escalated` 마커 생성됨 |
| whisper-compile: [완료] | `.done` 살아있음 → 표시 | **[완료] task-2451** 무한 표시 |
| whisper-compile: [팀] | task-timers에 task-2451 running 없음 + bot-activity 다수 stale processing | 8개 팀 모두 유휴(53h) — **stale 카운터 거짓 53h** |

→ **회장의 sync 의심은 정확**. 표시는 자연스러워 보이지만 내부 상태가 맞지 않음.

---

## 4. 5대 결함 상세

### 결함 1: stale processing in bot-activity.json

**증상**: dev5/7/8은 5/2부터 53시간째 status=processing, since 그대로.

**원인**:
- 봇 작업 끝나면 task-timer.py가 task-timers.json status=completed 처리
- 그러나 bot-activity.json의 status=processing은 **자동 idle 전환 안 됨**
  - done-watcher는 `.done.escalated` 등 마커가 있으면 스킵
  - composite/system-level task는 `team_id=""`라 매핑 실패
  - bot-status-watchdog가 동작 안 하거나 프로세스 살아있다고 판단
- 결과: 거짓 processing이 누적

**영향**: 모든 분석/대시보드가 stale source 위에서 동작

### 결함 2: whisper-compile의 우회 로직 (양날의 검)

**위치**: `scripts/whisper-compile.py:640`
```python
elif status == "idle" or (status == "processing" and not running_tasks):
```

**좋은 점**: stale processing을 task-timers running 기준으로 우회 → 표시는 정확
**나쁜 점**:
- 우회만 하지 **bot-activity 자체를 자동 보정하지 않음** → 진실은 그대로 stale
- since는 bot-activity 기반 그대로 사용 → 카운터(53h) 거짓 누적
- 다른 코드(예: 권한 검증, escalate 로직)가 bot-activity status를 진실로 사용하면 또 다른 mismatch

**필요**: bot-activity 자체를 보정하는 reconciler 또는 stale 감지 alert

### 결함 3: escalated dead-lock — **[정정 2026-05-05 task-2452]**

**증상**: task-2451은 .done.escalated 마커 있으면서도 .done이 살아있음 → [완료] 큐에 박혀있음

**원인 (정정)**:
- ~~auto_merge.py가 Tier 2/3에서 `.done.escalated` 마커 생성 + escalate~~ → **사실 아님**. auto_merge.py 코드 grep 결과 마커 생성 0건.
- 정정된 원인: **마커 출처 미상** (별도 추적 필요) + **done-watcher의 escalated 스킵 로직** (line 194 `processed_exts`에 `.escalated` 포함) 조합.
- 회장 수동 처리(`.done` → `.done.acked`) 전까지 .done 살아있음.
- whisper-compile [완료] 큐는 `.done` 존재만 검사 → 무한 표시.

**영향**: 회장이 매번 `.done` 보고 수동 acked 안 하면 큐 무한 누적.

**해소 (task-2452 Phase 3)**: done-watcher를 30분 grace + `.merge-done` 부재 fallback only로 강등. 정상 흐름은 finish-task.sh Step 2.99가 처리, 비정상 흐름만 done-watcher가 30분 후 강제 idle. 마커 출처 추적은 별도 task로 분리.

### 결함 4: team_id 빈 문자열

**증상**: task-2451 task-timer/`.done` 둘 다 `team_id: ""`

**원인**: composite/system-level task가 단일 팀 매핑 안 됨. dispatch.py가 빈 문자열로 저장.

**영향**:
- done-watcher가 `extract_team_from_done_file()`에서 None 반환 → 신호등 못 끔
- whisper [팀] 줄에서도 어느 팀 작업이었는지 못 추적

**필요**: composite의 모든 참여 팀 ID 리스트로 저장 + 모두 idle 전환

### 결함 5: 다단계 신호등 시점 미정의

**증상**: 스펙은 단일 단계(`.done → 유휴`). 실제는 5단계 후속(`.acked / .clear / .merging / .escalated / .rejected`).

**시점별 직관**:
- `.done` 생성: 봇 작업은 끝남 → "신호등 끄는 게 맞아 보임"
- `.done.escalated`: 회장 승인 대기 중 → "꺼져 있어야 하나?"
- `.done.clear`: 머지 완료 → "확실히 꺼야 함"
- `.done.acked`: 회장 인지 → "이미 꺼져 있어야 정상"

**현재 코드**: `.done` 발견 즉시 idle 처리 (마커 없을 때만). 마커 있으면 영영 안 처리. → 사람 직관과 어긋남.

**필요**: 스펙 갱신 + 다단계 신호등 시점 명시.

---

## 5. 권장 정정 방향 (회장 결정 대기, 작업 위임 X)

### A. 즉시 적용 가능 (최소 변경)

1. **stale reconciler 추가**: `bot-status-watchdog.py`를 cron 5분으로 확실히 가동 + processing 상태인데 task-timers running 없으면 강제 idle 전환 (현재 30분 타임아웃에 더해 "task 없음" 조건도).
2. **task-2451 같은 dead-lock 감지 alert**: `.done`이 24h 이상 살아있고 `.done.acked / .clear`가 없으면 회장 알림.
3. **신호등 스펙 v2 작성**: 다단계 후속 마커마다 신호등 상태 명시.

### B. 중기 (구조 변경)

4. **team_id 빈 문자열 금지**: composite 작업도 참여 팀 리스트로 명시 저장. dispatch.py 검증 강화.
5. **whisper-compile 우회 제거 + bot-activity 보정 자동화**: source of truth를 하나로 통일.
6. **`.done.acked`를 done-watcher가 자동 생성하는 옵션** 추가: 회장이 명시 "auto-ack" 정책으로 위임한 task만.

### C. 장기 (스펙 재설계)

7. **신호등을 task-timers running 기반으로 재정의**: bot-activity는 "프로세스 활성" 기준만, "작업 활성"은 task-timers로. 이중 source 명확히 분리.

---

## 6. 확인 불가 항목

1. **bot-status-watchdog가 실제로 cron/systemd에 가동 중인지** — 로그 위치는 `logs/bot-watchdog.log` 명시. 본 분석 범위 밖.
2. **Tier 2/3 escalate된 task의 봇 idle 전환 코드 위치** — `auto_merge.py`에서 `.done.escalated` 생성 후 봇 status를 어떻게 처리하는지 명시 코드 미발견.
3. **`task-2451.anu-notified` 마커의 정확한 역할** — extract_followup.py가 만드는 것으로 추정. 신호등과의 연관 미확인.
4. **회장이 본 sync 미스매치의 정확한 시점/사례** — 본 분석은 현재 시각 스냅샷 기반. 회장이 다른 시점/다른 task를 의심한 것일 수 있음.

---

## 7. 즉시 회장 결정 필요 (이 보고서 결과)

(a) task-2451 `.done` → `.done.acked` 처리 (큐에서 빼기 — 회장 hook이 시키는 작업)
(b) bot-activity stale 7개(dev1/3/4/5/6/7/8) 일괄 idle 보정 — 위임 또는 아누 직접 (단 직접 코딩 금지로 위임)
(c) 결함 5개 정정 작업 — 어느 우선순위로 할지 결정

본 보고서 위임 안 함. 분석만. 회장 지시에 따라 후속 위임.
