# 신호등 체계 스펙 (Traffic Light System Spec)

> 작성일: 2026-04-15 (v1)
> 최종 수정: **2026-05-05 (v2)** — task-2452 신호등 sync 정정 + 다단계 마커 시점 정의
> 근거: 제이회장님 직접 정의 (2026-04-15 v1, 2026-05-05 v2)
> 반복 오류 방지용 — 이 문서가 신호등 구현의 단일 소스(Single Source of Truth)

---

## 0. v2 변경 요약 (먼저 읽을 것)

v1 (2026-04-15)는 ".done 생성 → 전원 유휴" 단일 단계만 가정했으나, 2026-05-04~05 enforcement 다단계 마커(`.done`, `.done.acked`, `.done.clear`, `.done.merging`, `.done.escalated`, `.done.rejected`)가 도입되면서 스펙과 실구현 mismatch 발생. v2는 다음 정의로 정정:

1. **신호등 끄는 단일 책임 = `finish-task.sh` 끝부분 (Step 2.99, .done 생성 직전)**.
   - 회장 원칙: "오토 머지까지 다 되어야 작업이 정말 완성된 거고, 그 이후 finish-task.sh가 진행되니, 마지막에 신호등이 꺼지는 로직이어야 정상."
   - `member-status.json` (팀원 단위) + `bot-activity.json` (팀 단위) 두 source 동시·동일 시점 idle 전환.
   - composite 작업 `team_id=""` 케이스: `task-timers.json[task_id].affected_teams` / `composite_teams` fallback.

2. **다단계 마커는 머지 라이프사이클 상태 표시일 뿐, 신호등 트리거가 아님**.
   - `.done`: finish-task.sh가 작업 완료 시점에 원자적으로 생성. 신호등은 이미 직전 Step에서 꺼져 있음.
   - `.done.merging`: auto_merge.py가 try_claim() 시 생성 (선점 마커). 신호등 무관. **[참고] task-2452 Phase 1에서 auto_merge.py 폐기.**
   - `.done.clear`, `.done.acked`, `.done.escalated`, `.done.rejected`: 머지 결과/회장 처리 마커. 신호등 무관.
   - **마커 생성자 일부 미상** (예: `.done.escalated`): auto_merge.py 코드 grep 0건. 별도 추적 권고.

3. **Fallback (비정상 흐름 only)**: `done-watcher.py`가 .done이 30분 이상 살아있고 `.merge-done` 마커 부재 시 `[FALLBACK-IDLE]` 로그 + `set_bot_idle()`. 정상 흐름은 finish-task.sh가 처리하므로 도달 안 함.

4. **머지의 진짜 경로 3개** (auto_merge.py 폐기 후):
   - **A) `finish-task.sh` Step 2** → `worktree_manager.py finish --action auto` (현재 hot path).
   - **B) `taskctl.py merge`** → `gh pr merge` 직접 호출.
   - **C) GitHub `merge_group` + Ruleset(active, id=15896715)** → 7개 required checks + server-side 직렬 머지.

---

## 1. 상태 정의 (3가지만 존재)

| 상태 | 색상 | 의미 |
|------|------|------|
| **유휴** | 회색 | 아무 작업도 없음. 팀에 할당된 task가 없는 상태 |
| **대기** | 노란색 | 팀에 task가 할당되었지만, 아직 이 멤버가 실제 호출되지 않음 |
| **작업중** | 초록색 | 팀장이 이 멤버를 호출하여 실제 작업 수행 중 |

---

## 2. 상태 전이 규칙

### 2.1 dev팀 자체 작업 시
```
dispatch.py로 dev팀에 직접 작업 위임
  → 팀장: 작업중 (초록)
  → 팀원 전원: 대기 (노란) ← 유휴가 아님!
```

### 2.2 팀장이 subagent(팀원) 호출 시
```
팀장이 특정 팀원에게 작업 위임
  → 해당 팀원: 대기(노란) → 작업중(초록)
  → 나머지 팀원: 대기(노란) 유지
```

### 2.3 팀원 작업 완료 시
```
팀원의 개별 작업 완료
  → 해당 팀원: 작업중(초록) → 대기(노란)
  → 팀 전체 작업은 아직 진행 중
```

### 2.4 팀 전체 작업 완료 시 (.done 파일 생성) — **[v2 정정 2026-05-05 task-2452]**

```
finish-task.sh 끝부분 Step 2.99 (.done 생성 직전, 단일 책임):
  → member-status.json: 팀원 working/standby → 유휴(회색)
  → bot-activity.json:  영향 받는 팀 봇 processing → 유휴(회색)
finish-task.sh Step 3 (.done 원자적 생성):
  → .done 파일 생성 (마커는 신호등 트리거 아님)
```

**중요**:
- 신호등이 꺼지는 시점은 .done **생성 직전**의 finish-task.sh 마지막 Step. .done 자체가 신호등을 끄는 게 아님.
- finish-task.sh가 앞 단계에서 exit하면 (QC FAIL 등) 신호등은 안 꺼짐 — 정상 (실패 시 stale processing 유지가 의도).
- 비정상 흐름 (봇 크래시·finish-task.sh 누락) fallback: `done-watcher.py`가 30분 grace + `.merge-done` 부재 조건일 때 강제 idle (`[FALLBACK-IDLE]` 로그).

```
(v1 단순 표현 — 정상 흐름의 결과만 보면)
팀장이 .done 파일 생성
  → 팀장: 작업중(초록) → 유휴(회색)
  → 팀원 전원: 대기/작업중 → 유휴(회색)
  → 팀 전체: 유휴
```

---

## 3. 논리적/복합팀 봇 차용 (Borrowed Tasks)

### 3.1 개념
marketing, content, design, consulting, publishing, composite 등 **논리적 팀**은 자체 봇이 없음.
dispatch 시 dev팀 봇을 **차용(borrow)**하여 사용.

### 3.2 차용 시 상태 전이
```
marketing 팀에 작업 위임 → bot-b (dev1팀 봇) 배정
  → dev1팀 카드:
    - has_running_tasks = True
    - logical_team_using = "마케팅팀"
    - borrowed_tasks = [{task_id, team_id: "marketing", ...}]
  → dev1팀 팀장(헤르메스): 작업중 (초록)
  → dev1팀 팀원: 대기 (노란)
  → marketing 카드: "1팀봇 사용" 배지 표시
```

### 3.3 차용 task 시각 구분
- dev팀 자체 task: 초록 도트 (`bg-emerald-400`)
- 차용 task: **보라색 도트** (`bg-violet-400`)

### 3.4 구현 위치
- **백엔드**: `data_loader.py` → `_enrich_bot_activity()` 함수
  - task의 `team_id`가 봇의 dev팀과 다르면 → `borrowed_tasks` 리스트에 추가
  - `logical_team_using` 필드에 차용 팀 이름 설정
  - 복수 논리팀 동시 차용 시 `"+"` 결합 (예: "마케팅팀+컨텐츠팀")
- **프론트엔드**: `App.js` → `ownTasks + borrowedTasks` 병합하여 `allTasks` 전달
- **프론트엔드**: `utils.js` → `isBorrowed` 조건으로 보라색 도트 렌더링

### 3.5 필수 조건
borrowed_tasks가 동작하려면 task-timers.json에 **`bot` 필드가 반드시 존재**해야 함.
- dispatch.py로 위임 시: 자동 배정됨 ✅
- task-timer.py로 수동 start 시: bot 필드 없음 → borrowed_tasks 미동작 ⚠️

### 3.6 검증 완료 (2026-04-15 런타임 테스트)
```
marketing 팀에 task-9999 생성 + bot-b 배정
  → /api/bot-activity 응답:
    dev1: has_running=True, logical_team_using=마케팅팀,
          borrowed_tasks=[{task_id: task-9999, team_id: marketing, ...}]
  → 정상 동작 확인 ✅
```

---

## 4. 상태 전이 다이어그램

```
                    dispatch 위임
                        │
    ┌───────────────────┼───────────────────┐
    ▼                   ▼                   ▼
 [팀장]             [팀원A]             [팀원B]
 작업중(초록)       대기(노란)          대기(노란)
    │                   │
    │ 팀장이 A 호출     │
    │──────────────────►│
    │                   ▼
    │              작업중(초록)
    │                   │
    │    A 작업 완료    │
    │◄──────────────────│
    │                   ▼
    │              대기(노란)
    │
    │ .done 생성
    ▼
 유휴(회색)         유휴(회색)          유휴(회색)
```

### 논리적 팀 차용 시 다이어그램

```
marketing 작업 위임 → bot-b 배정
    │
    ▼
 [dev1 팀장]        [dev1 팀원]        [marketing 카드]
 작업중(초록)       대기(노란)         "1팀봇 사용" 배지
    │
    │  task 표시: 보라색 도트 (차용)
    │
    │ .done 생성
    ▼
 유휴(회색)         유휴(회색)          배지 제거
```

---

## 5. 핵심 원칙

1. **팀 작업 시작 = 팀원 전원 대기(노란)**. 유휴(회색)로 남아있으면 안 됨
2. **실제 호출 = 작업중(초록)**. 팀장이 subagent를 실제로 호출한 팀원만 초록
3. **개별 완료 = 대기 복귀(노란)**. 팀 작업이 끝나지 않았으므로 유휴가 아님
4. **팀 완료 = 전원 유휴(회색)**. .done 파일 기준
5. **차용 task = 보라색 도트**. dev팀 자체 task(초록)와 시각 구분 필수
6. **borrowed_tasks 동작 = bot 필드 필수**. task-timers.json에 bot 배정 없으면 차용 미감지

---

## 6. 구현 매핑 (코드 위치)

### 6.1 백엔드
| 기능 | 파일 | 함수/위치 |
|------|------|-----------|
| 멤버 상태 결정 | data_loader.py | `get_member_status()` — is_lead로 working/standby 분기 |
| 봇 활동 enrichment | data_loader.py | `_enrich_bot_activity()` — borrowed_tasks, logical_team_using |
| 멤버 상태 enrichment | data_loader.py | `_enrich_member_status()` — member-status.json 보강 |

### 6.2 프론트엔드
| 기능 | 파일 | 컴포넌트/위치 |
|------|------|-------------|
| 상태 색상 렌더링 | utils.js | `StatusDot` — working→초록, standby→노란, idle→회색 |
| 멤버 행 상태 결정 | utils.js | `MemberRow` — hasRunningTasks + isLead로 분기 |
| 인원 현황 바 | App.js | borrowedTasks.length > 0 반영 |
| 팀 카드 task 병합 | App.js | ownTasks + borrowedTasks → allTasks |
| 차용 도트 색상 | utils.js | isBorrowed → `bg-violet-400` |

### 6.3 API 엔드포인트
| API | 용도 |
|-----|------|
| `/api/member-status` | 멤버별 상태 (working/standby/idle) |
| `/api/bot-activity` | 봇별 활동 (borrowed_tasks, logical_team_using) |
| `/api/teams` | 팀 카드 데이터 |

---

## 7. 과거 오류 패턴 (반복 발생 방지)

### 7.1 팀원이 유휴로 표시 (task-1854에서 수정)
- **원인**: `MemberRow`에서 `hasRunningTasks && isLead`만 체크 → 팀원 누락
- **수정**: `hasRunningTasks && !isLead` → `"standby"` 분기 추가

### 7.2 전원 작업중으로 표시
- **원인**: `get_member_status()`가 `is_lead` 무시하고 전원 `"working"` 반환
- **수정**: `is_lead`이면 `"working"`, 아니면 `"standby"` 반환

### 7.3 서버 미재시작으로 코드 미반영
- **원인**: data_loader.py 수정 후 서버 재시작 안 함
- **수정**: 코드 수정 후 반드시 서버 재시작 또는 PID kill + 재시작

### 7.4 borrowed_tasks 미동작
- **원인**: task-timers.json에 `bot` 필드 누락 (수동 task-timer start 시)
- **수정**: dispatch.py 통해 위임하면 bot 자동 배정됨. 수동 start 시 주의.
