# 복합업무 임시팀(Composite Team) 설계 미팅 기록

> 작업 ID: task-1045.1
> 미팅 일시: 2026-03-25
> 참석자: 불칸(백엔드), 아테나(UX), 아르고스(QA), 로키(레드팀/DA)
> 진행: 헤르메스(개발1팀장)

---

## 사이클 1 핵심 결과
- 23개 개선점 도출
- P0: 화이트리스트 검증, injection_guard, Phase 0 계획
- 로키 공격 6건: Phase 오판, 인젝션, 무한루프, 과잉설계, 토큰 비대화, 기존 대안

## 사이클 2 핵심 결과
- 15개 추가 개선점 도출
- P0-Security: injection_guard 차단(로깅만→block), 안전장치 연속성, Defense in Depth 검증, AI 중간파일 스캔
- P0-Arch: _dispatch_composite() 누락 단계 5개 보완, team_id Optional[str], "design" TEAM_INFO 불일치
- P1-UX: Phase 0 명확화(FYI not gate), 핸드오프 동적화, 보고서 2레이어, 파일참조 현실화

---

## 설계안 v0.3 (사이클 1+2 피드백 통합)

### 1. CLI 인터페이스 (변경없음)
```bash
python3 dispatch.py --composite marketing,design --task-file /path/to/task.md [--level normal|critical|security]
```
- `--team`과 `--composite`: `add_mutually_exclusive_group(required=True)`
- `--composite` 값: 쉼표 구분, 2개 이상 필수

### 2. dispatch.py 변경사항

#### 2-1. 새 상수
```python
COMPOSITE_ALLOWED_TEAMS = {"marketing", "design", "consulting", "publishing"}
```

#### 2-2. _validate_composite_teams() (개선)
```python
def _validate_composite_teams(teams_str: str) -> List[str]:
    teams = [t.strip() for t in teams_str.split(",") if t.strip()]  # 빈 토큰 필터링
    if len(teams) < 2:
        raise ValueError(f"composite는 2개 이상의 팀이 필요합니다 (입력: {teams_str!r})")
    unknown = [t for t in teams if t not in COMPOSITE_ALLOWED_TEAMS]
    if unknown:
        raise ValueError(f"알 수 없는 팀 ID: {unknown}. 허용 목록(소문자): {sorted(COMPOSITE_ALLOWED_TEAMS)}")
    if len(teams) != len(set(teams)):
        raise ValueError(f"중복 팀 ID: {teams}")
    return teams
```

#### 2-3. 공통 안전장치 추출: _validate_and_setup_task()
```python
def _validate_and_setup_task(
    task_id: str,
    task_desc: str,
    team_id_for_check: str,  # composite: "composite", 단일: team_id
    level: str,
    force: bool = False,
) -> Optional[dict]:
    """dispatch()와 _dispatch_composite() 공통 안전장치.
    Returns: 에러 dict (에러 시) 또는 None (정상)

    포함 로직:
    - injection_guard 검사
    - approval 검사
    - model_router 검사
    - task-timer start
    - 동일 팀 running 중복 체크 (force 지원)
    """
```
- 기존 dispatch()에서 L436~L526까지의 공통 로직을 추출
- 단일 팀 dispatch()와 _dispatch_composite() 모두 이 함수 호출

#### 2-4. dispatch() 시그니처 변경
```python
def dispatch(
    team_id: Optional[str] = None,    # str → Optional[str]
    task_desc: str = "",
    level: str = "normal",
    composite_teams: Optional[List[str]] = None,  # 신규
    ...
) -> dict:
    # XOR 검증
    if not team_id and not composite_teams:
        return {"status": "error", "message": "team_id 또는 composite_teams 중 하나 필수"}
    if team_id and composite_teams:
        return {"status": "error", "message": "team_id와 composite_teams는 동시 사용 불가"}
    # composite 분기
    if composite_teams:
        return _dispatch_composite(composite_teams, task_desc, level, ...)
    # 기존 단일 팀 로직
    ...
```

#### 2-5. _dispatch_composite() 신규
```python
def _dispatch_composite(
    composite_teams: List[str],
    task_desc: str,
    level: str = "normal",
    task_id: Optional[str] = None,
    force: bool = False,
) -> dict:
    """복합업무 임시팀 디스패치.

    흐름:
    1. task_id 생성 (generate_task_id())
    2. _validate_composite_teams() 재검증
    3. injection_guard (high/critical → 즉시 차단, 단일팀보다 엄격)
    4. _validate_and_setup_task() 공통 안전장치
    5. _find_available_bot() 가용 봇 선택
    6. task_desc 파일 저장 + task_id prefix 치환
    7. build_composite_prompt() 호출
    8. _patch_timer_metadata (role="composite", composite_teams=[...])
    9. cokacdir --cron 호출
    10. 성공: set_bot_status, daily 로그, 반환
    11. 실패: _cleanup_task, 에러 반환
    """
```

#### 2-6. _patch_timer_metadata() 수정
```python
def _patch_timer_metadata(task_id: str, **metadata: Any) -> None:  # str → Any
```

### 3. team_prompts.py 변경사항

#### 3-1. 에이전트 풀 로딩 (캐시 포함)
```python
_TEAM_AGENT_CACHE: dict = {}

def _load_logical_team_agents(team_id: str) -> dict:
    if team_id in _TEAM_AGENT_CACHE:
        return _TEAM_AGENT_CACHE[team_id]
    detail_path = Path(WORKSPACE_ROOT) / "memory" / "org-details" / f"{team_id}-team.json"
    if detail_path.exists():
        with open(detail_path, "r", encoding="utf-8") as f:
            result = json.load(f)
            _TEAM_AGENT_CACHE[team_id] = result
            return result
    # fallback
    fallback = {"members": TEAM_INFO.get(team_id, {}).get("members", "")}
    _TEAM_AGENT_CACHE[team_id] = fallback
    return fallback
```

#### 3-2. 핸드오프 필수 전달 사항 (동적)
```python
HANDOFF_REQUIRED_FIELDS: dict = {
    frozenset({"marketing", "design"}): ["플랫폼", "사이즈/형식", "톤앤매너", "CTA 문구", "타겟 오디언스"],
    frozenset({"consulting", "publishing"}): ["대상 독자", "법적 검토 여부", "인용 출처", "데이터 정확성"],
    frozenset({"marketing", "publishing"}): ["톤앤매너", "타겟 오디언스", "키워드", "SEO 메타"],
    frozenset({"marketing", "consulting"}): ["타겟 고객", "상품 정보", "규제 사항", "CTA 문구"],
}
DEFAULT_HANDOFF_FIELDS = ["핵심 결정사항", "산출물 파일 경로", "다음 Phase 요구사항"]
```

#### 3-3. build_composite_prompt() (독립 진입점, Defense in Depth)
```python
def build_composite_prompt(
    composite_teams: List[str],
    task_id: str,
    task_desc: str,
    level: str = "normal",
    project_id: Optional[str] = None,
) -> str:
    """복합업무 임시팀 프롬프트 생성.

    Defense in Depth: 내부에서 composite_teams 재검증.
    """
    # 내부 재검증 (dispatch.py와 독립적)
    for t in composite_teams:
        if t not in COMPOSITE_ALLOWED_TEAMS:
            raise ValueError(f"build_composite_prompt: 허용되지 않은 팀 ID: {t}")
    if len(composite_teams) < 2:
        raise ValueError("build_composite_prompt: 2개 이상의 팀 필요")

    # task_desc 파일 저장
    task_file_path = f"{WORKSPACE_ROOT}/memory/tasks/{task_id}.md"
    task_file = Path(task_file_path)
    task_file.parent.mkdir(parents=True, exist_ok=True)
    task_file.write_text(task_desc, encoding="utf-8")

    # 각 팀 에이전트 정보 로드
    team_agents = {}
    for team in composite_teams:
        team_agents[team] = _load_logical_team_agents(team)

    # 핸드오프 필수 필드 결정
    team_set = frozenset(composite_teams)
    handoff_fields = HANDOFF_REQUIRED_FIELDS.get(team_set, DEFAULT_HANDOFF_FIELDS)

    # 프롬프트 조립
    prompt = _assemble_composite_prompt(
        composite_teams, team_agents, handoff_fields,
        task_id, task_file_path, level, project_id,
    )

    # level 헤더 삽입
    if level == "critical":
        prompt = "**[CRITICAL] ...**\n\n" + prompt
    elif level == "security":
        prompt = "**[SECURITY] ...**\n\n" + prompt

    return prompt
```

#### 3-4. _assemble_composite_prompt() 내부 함수
```python
def _assemble_composite_prompt(
    composite_teams: List[str],
    team_agents: dict,
    handoff_fields: list,
    task_id: str,
    task_file_path: str,
    level: str,
    project_id: Optional[str],
) -> str:
    """복합업무 프롬프트 조립.

    프롬프트 구조:
    1. 임시팀장 페르소나 + 역할
    2. 작업 지시 (task_file_path 참조)
    3. 팀별 에이전트 목록 + 스킬
    4. Phase 관리 프로토콜
    5. 핸드오프 규격 (동적 필수 필드)
    6. Quality Gate 규칙
    7. 보고서 규칙 (2레이어)
    8. 토큰 관리 지시
    9. 완료 마무리 (finish-task.sh)

    토큰 예산: 시스템 프롬프트 상한 4,000 토큰
    """
```

### 4. 임시팀장 프로토콜 (v0.3)

#### Phase 0: 계획 수립 (FYI — 실질 게이트 아님)
- 목적: **감사 추적 + 비동기 알림**
- 임시팀장이 task.md 분석 → Phase 계획 작성
- 파일: `memory/tasks/{task_id}-phase-plan.md`
- Telegram 알림: 비동기 FYI (승인 대기 없음)
- 한정승인 작업이므로 즉시 Phase 1 실행
- ⚠️ Phase 계획 파일 작성 후 injection_guard 검사 (AI 생성 파일 스캔)

#### Phase N (1~M): 순차 실행
- 해당 Phase 팀 에이전트 활성화
- 산출물 → 파일 저장
- Quality Gate (최대 2회 재작업, 카운터는 컨텍스트 내 관리)
- 2회 실패: 이슈 기록 + **Telegram 즉시 알림** + 다음 Phase 진행
- 핸드오프 문서: `memory/tasks/{task_id}-handoff-{N}.md`
  - 최상단에 **핵심 요약 (200자 이내)** 섹션 의무
  - 다음 Phase는 요약 섹션만 Read (토큰 절감)

#### Phase Final: 통합 보고서 (2레이어)
```markdown
# 복합업무 보고서 ({task_id})

## Executive Summary (아누용)
- **결과**: 성공/부분성공/실패
- **QG 실패**: N건 (Phase X, Phase Y)
- **산출물**: [파일 경로 목록]
- **후속 조치**: [필요 시]

## Phase별 상세 (팀장/감사용)
### Phase 1: {팀명}
**S**: ...  **C**: ...  **Q**: ...  **A**: ...

### Phase 2: {팀명}
**S**: ...  **C**: ...  **Q**: ...  **A**: ...

## Quality Gate 이력
- Phase 1: PASS (1회차)
- Phase 2: PASS (재작업 1회 후)

## finish-task.sh 실행
```

### 5. 보안 설계 (v0.3 신규)

#### injection_guard 이원화
- 단일 팀 dispatch: 현행 유지 (로깅만, 워크플로우 호환)
- composite dispatch: **high/critical 감지 시 즉시 error 반환** (여러 팀 피해 방지)

#### AI 생성 중간 파일 스캔
- Phase 계획 파일, 핸드오프 파일은 AI가 생성
- 생성 직후 injection_guard 재검사 지시를 프롬프트에 포함
- 프롬프트 레벨 지시이므로 강제력은 제한적 → 최소한의 안전장치

#### Defense in Depth
- dispatch.py: _validate_composite_teams() (CLI/API 레벨)
- team_prompts.py: build_composite_prompt() 내부 재검증 (라이브러리 레벨)
- 프롬프트: 임시팀장에게 허용되지 않는 지시 패턴 명시 (행동 레벨)

### 6. "design" 팀 TEAM_INFO 불일치 해소
- TEAM_INFO에 "design" 항목 추가하지 않음
- composite 경로에서 TEAM_INFO 직접 접근 제거
- _dispatch_composite()와 build_composite_prompt()는 _load_logical_team_agents()만 사용
- org-details/design-team.json이 유일한 소스

### 7. 토큰 관리
- 복합 프롬프트 시스템 메시지 상한: 4,000 토큰
- 3개 이상 팀: 경고 메시지 (logger.warning)
- Phase 전환 시: 핸드오프 요약만 참조 (전문 포함 금지 지시)

### 8. 테스트 전략 (아르고스)
- _validate_composite_teams(): 5개 엣지 케이스
- build_composite_prompt(): mock 전략 (파일 I/O, ANU_KEY)
- _dispatch_composite(): 4개 에러 경로
- TestCompositeConstants: COMPOSITE_ALLOWED_TEAMS 존재 + TEAM_BOT 오염 방지

---

## 사이클 3 미팅

(아래에 기록 예정)
