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

> 작성일: 2026-04-15
> 최종 수정: 2026-05-10 (task-2536 — 유휴 base 명시 규칙 박제)
> 근거: 제이회장님 직접 정의 (2026-04-15) + 런타임 검증 완료 + 회장 §결정 2026-05-10 (신호등 sync fix D)
> 반복 오류 방지용 — 이 문서가 신호등 구현의 단일 소스(Single Source of Truth)

---

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

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

---

## 2. 상태 전이 규칙

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

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

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

### 2.4 팀 전체 작업 완료 시 (.done 파일 생성)
```
팀장이 .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 시 주의.

---

## 8. 유휴 base 명시 규칙 (task-2536 — 회장 §결정 2026-05-10 fix D)

### 8.1 배경
회장 발견(2026-05-10): `"1팀:유휴(85h)"` 라벨에서 **어떤 시각 기준 85시간인지 base가 표시되지 않음**.
사용자가 base를 알 수 없어 stale 여부 판단 불가 → fix D 채택.

### 8.2 idle_base 정의
- **base 시각** = 해당 팀의 task-timers.json 내 `status == "completed"` 엔트리 중 **가장 최근 `end_time`**
- **base task** = 그 엔트리의 `task_id`
- **idle_hours** = 현재 시각 - base 시각 (정수 시간, floor)
- 팀이 작업중(running 1건 이상)이면 base 의미 없음 → idle_base 미부착

### 8.3 idle_base 데이터 형식
```python
{
    "task_id": "task-2470",
    "ts": "2026-05-06T13:59:00",     # ISO 8601 (naive 또는 +offset)
    "idle_hours": 85,
}
```

### 8.4 라벨 표시 규칙 (SystemView)
- **정상**: `"유휴(85h since task-2470 13:59)"`
- **base 누락(데이터 없음)**: `"유휴(?)"` — graceful 표시 (signal 병합 깨지지 않음)
- **시각 포맷**: `HH:MM` (24시간, 로컬 타임존 기준)

### 8.5 stale base 처리
- base가 24h+ 경과한 경우 → 라벨에 `~` 또는 `?` 보강 표시 권장 (현 구현은 `?` 단일 표기)
- ※ 24h+ 자체로는 정상 (장기 유휴 팀 발생 가능). 다만 base 자체가 사라진 경우만 `?`로 명시.

### 8.6 구현 매핑
| 기능 | 파일 | 함수/위치 |
|------|------|-----------|
| idle_base 계산 | dashboard/data_loader.py | `_compute_team_idle_base(team_id)` |
| 팀별 idle_base 노출 | dashboard/data_loader.py | `get_teams_info()` (running 0건일 때만) |
| 멤버별 idle_base 노출 | dashboard/data_loader.py | `_enrich_member_status()` (idle 멤버만) |
| 라벨 렌더 | dashboard/components/SystemView.js | `formatIdleBaseLabel(idleBase)` + `idleTeamRows` |
| API 노출 | dashboard/routes_get.py | `/api/teams`, `/api/member-status` (기존 엔드포인트 그대로 사용 — 추가 라우팅 X) |

### 8.7 절대 금지
- ❌ signal 병합 로직 수정 (task-2534 영역) — idle_base는 **부착 only, 병합 X**
- ❌ HistoryView/AutomationView/ArchiveView에서 idle_base 직접 표시 — SystemView 단독
- ❌ whisper-compile.py briefing_summary 포맷 수정 — task-2536 외 영역
