# task-timer.py task_id 형식 문제 분석 + 개선방안

**작성**: 오딘 (dev2-team) | task-418
**작성일**: 2026-03-10

---

## 1. 에러 재현 분석

팀장(헤르메스)이 task-417 작업 완료 후 `task-timer.py end`를 호출할 때 3회 실패:

1. `task-timer.py end task-417` → "잘못된 task_id 형식: task-417 (expected: task-N.N)"
2. `task-timer.py end 417` → "잘못된 task_id 형식: 417 (expected: task-N.N)"
3. `task-timer.py end task-4.17` → "Task 'task-4.17' not found"

## 2. 근본 원인 분석

### 2.1 핵심 불일치: task_id의 이중 체계

시스템에 **두 가지 task_id 형식**이 혼재:

| 생성 경로 | 형식 | 예시 |
|---|---|---|
| `dispatch.py --task-id` (명시적) | `task-N` | task-417, task-418 |
| `dispatch.py generate_task_id()` (자동) | `task-N.N` | task-417.1, task-418.1 |

**task-timers.json 실제 데이터:**
- `task-N` 형식 (dot 없음): **10건**  (예: task-293, task-296, task-350, task-398, task-417)
- `task-N.N` 형식 (dot 있음): **352건** (예: task-415.1, task-416.1, task-417.1)

### 2.2 dispatch.py의 task_id 생성 흐름

```python
# dispatch.py:dispatch() (Line 285)
if task_id is None:
    task_id = generate_task_id()  # → "task-N.N" (항상 .1 접미사)
```

- `--task-id` 미지정 시: `generate_task_id()` → `task-N.N` (예: `task-417.1`)
- `--task-id task-417` 명시 시: 그대로 `task-417` 사용

**문제**: `--task-id`로 `task-N` 형식을 전달해도 dispatch.py는 이를 검증하지 않고 그대로 진행.

### 2.3 dispatch.py → task-timer start 호출 시 실패 무시

```python
# dispatch.py:dispatch() (Line 291-294)
timer_cmd = ["python3", str(TASK_TIMER), "start", task_id, "--team", team_id, "--desc", short_desc]
timer_result = subprocess.run(timer_cmd, capture_output=True, text=True, timeout=30)
if timer_result.returncode != 0:
    logger.warning(f"task-timer start 실패: {timer_result.stderr.strip()}")  # ← warning만!
```

- `task-N` 형식으로 `task-timer start`를 호출하면 regex 검증 실패
- **하지만 dispatch.py는 warning만 로깅하고 작업 위임을 계속 진행**
- 결과: 팀장에게 `task_id: task-417`이 전달되지만, task-timers.json에는 해당 ID가 등록되지 않음

### 2.4 generate_task_id()의 별도 timer 등록

`generate_task_id()`가 호출되면 reserved 엔트리를 직접 task-timers.json에 기록 (Line 115):
```python
timer_data["tasks"][next_id] = {"status": "reserved", "reserved_at": datetime.now().isoformat()}
```
이후 `dispatch()`에서 `task-timer.py start`로 정식 시작 (Line 291). 이 과정에서 reserved → running으로 전환.

### 2.5 task-timer.py의 엄격한 형식 검증

```python
# task-timer.py (Line 28)
TASK_ID_PATTERN = re.compile(r"^task-\d+\.\d+$")
```

CLI 레벨에서 `start`, `end`, `status` 모든 명령에 이 검증을 적용 (Lines 501, 544, 558).
**`task-N` 형식은 무조건 거부.**

### 2.6 task-417 구체 사례 타임라인

task-timers.json의 task-417 관련 3건:

- `task-417.1`: team=dev1-team, start=08:46:20, completed (dispatch.py generate_task_id()가 생성)
- `task-417.0`: team='', start=08:48:49, completed (경위 불명 — 수동 또는 다른 경로)
- `task-417`: team=dev1-team, start=08:49:33, completed (팀장이 삽질 끝에 수동 등록한 것으로 추정)

events 디렉토리에도 3개 .done 파일 생성:
- `task-417.0.done.clear`
- `task-417.1.done.clear`
- `task-417.done.clear`

→ 하나의 논리적 작업에 3개의 timer 엔트리와 3개의 .done 파일이 생성됨

### 2.7 현재 task-418도 동일 문제 재현 중

- task-timers.json: `task-418.1` (status=running, team=dev2-team)
- 프롬프트에 전달된 task_id: `task-418` (dot 없음)
- task 파일: `task-418.md` (dot 없음)
- **DIRECT-WORKFLOW step 7에서 `task-timer.py end task-418` 호출 시 동일 에러 예상**

### 2.8 DIRECT-WORKFLOW.md의 task_id 안내

```
7. task-timer end: `python3 {WORKSPACE_ROOT}/memory/task-timer.py end {task_id}`
```

`{task_id}`는 프롬프트에서 전달받은 값 (`task-418` 등)을 그대로 사용.
프롬프트가 `task-N` 형식이면 → task-timer CLI 검증 실패.

### 2.9 completion-handler-instructions.md도 동일 문제

Step 4에서 `task-timer.py end {task_id}`를 호출하므로, 아누의 완료 처리에서도 동일 에러 발생 가능.

### 2.10 `.N` 접미사의 목적과 현실

`generate_task_id()`에서 `.1`을 항상 붙이지만:
- `.2`, `.3` 등의 서브태스크 분할은 실제로 사용되지 않음
- `task-N.N` 형식에서 `.N`의 의미가 코드베이스 어디에도 정의되지 않음
- 결과적으로 불필요한 복잡성만 추가

## 3. 영향 범위

- **직접 영향**: 모든 팀장(헤르메스, 오딘, 라)의 task-timer end 호출
- **간접 영향**: completion-handler의 task-timer end 호출
- **데이터 오염**: 중복 timer 엔트리, 중복 .done 파일
- **작업 시간 측정 왜곡**: 삽질 시간이 작업 시간에 포함됨

## 4. 개선방안 비교 분석

### 방안 A: task-timer.py가 유연한 형식 허용

**내용**: `task-N` 형식도 허용하고, 자동으로 `task-N`을 `task-N.0` 또는 기존 매칭 엔트리로 매핑

**장점**:
- 변경 범위 최소 (task-timer.py만 수정)
- 기존 `task-N.N` 데이터와 호환성 유지
- 하위호환 100%

**단점**:
- `task-N`으로 end 호출 시 `task-N.1`을 찾아야 하는데, 매핑 로직이 애매 (N.1? N.0? 가장 최신?)
- 근본 원인(이중 체계)을 해결하지 않음 — 증상만 완화
- 검색/매핑 로직 추가로 edge case 발생 가능

**구현 난이도**: 낮음 (task-timer.py 내 validate_task_id + end_task 수정)

### 방안 B: dispatch.py가 처음부터 task-N 형식으로 통일

**내용**: `generate_task_id()`가 `.N` 접미사 없이 `task-N`만 생성. task-timer.py 정규식도 `^task-\d+$`로 변경.

**장점**:
- 이중 체계 근본 해결
- task_id가 단순하고 직관적
- 파일명(task-N.md)과 timer ID가 일치
- 프롬프트에서 혼동 없음

**단점**:
- 기존 352건의 `task-N.N` 데이터와 호환성 문제 (마이그레이션 필요?)
- 향후 서브태스크 분할이 필요해지면 재설계 필요
- dispatch.py + task-timer.py 동시 수정

**구현 난이도**: 중간 (generate_task_id + TASK_ID_PATTERN + 기존 데이터 호환)

### 방안 C: 팀장 워크플로우에서 task-timer 호출 자동화

**내용**:
- dispatch.py에서 `task-timer start` 자동 호출 (이미 구현됨)
- `qc_verify.py --gate` 또는 `notify-completion.py`에서 `task-timer end` 자동 호출
- DIRECT-WORKFLOW.md에서 수동 task-timer end 지시 제거

**장점**:
- 팀장이 task-timer를 직접 호출할 필요 없음 → 형식 에러 원천 차단
- 작업 시간 측정이 더 정확 (삽질 시간 제외)
- 팀장 워크플로우 단순화

**단점**:
- 여러 파일 동시 수정 (qc_verify.py, notify-completion.py, DIRECT-WORKFLOW.md, completion-handler-instructions.md)
- task-timer end의 호출 시점이 분산됨 (어디서 end가 호출되는지 추적 어려움)
- qc_verify.py가 task-timer까지 알아야 하는 결합도 증가

**구현 난이도**: 중간~높음

### 방안 D: 하이브리드 (A+C 조합) — **권장**

**내용**:
1. `task-timer.py`가 `task-N` 형식도 허용 (방안 A)
2. `task-N`으로 end 호출 시 → `task-N.*` 패턴으로 매칭하여 running 상태인 항목 종료
3. `dispatch.py`의 `task-timer start` 실패 시 에러 로깅 + 자동 복구 (task-N 형식이면 .1 자동 붙여서 재시도)
4. DIRECT-WORKFLOW step 7은 유지하되, 이미 qc_verify.py `--gate`가 .done 생성 + notify-completion.py에서 completion-handler가 end 처리하므로, 팀장의 수동 end는 **"실패해도 무방"** 하도록 멱등성 강화
5. completion-handler-instructions.md Step 4가 최종 안전망 역할

**장점**:
- 팀장의 형식 실수 허용 (UX 개선)
- 기존 데이터 호환성 유지
- 어디서 end가 호출되든 작동 (다중 안전망)
- 근본 원인은 아니지만, 실용적으로 모든 에러 케이스 해결

**단점**:
- `task-N` ↔ `task-N.N` 이중 체계 자체는 유지
- fuzzy matching 로직 추가

**구현 난이도**: 낮음~중간

## 5. 권장 방안: D (하이브리드)

### 5.1 이유

- **즉시 효과**: 팀장의 삽질 즉시 해결 (task-N도 허용)
- **안전**: 기존 데이터 마이그레이션 불필요
- **최소 변경**: task-timer.py 주로 수정, 나머지 파일은 선택적
- **장기 방향**: 향후 방안 B(task-N 통일)로 전환해도 충돌 없음

### 5.2 구현 상세 설계

#### (1) task-timer.py: validate_task_id 확장

```python
# 현재
TASK_ID_PATTERN = re.compile(r"^task-\d+\.\d+$")

# 변경: task-N 또는 task-N.N 둘 다 허용
TASK_ID_PATTERN = re.compile(r"^task-\d+(\.\d+)?$")
```

#### (2) task-timer.py: normalize_task_id 함수 추가

```python
def normalize_task_id(task_id: str) -> str:
    """task-N 형식을 task-N.0으로 정규화. task-N.N은 그대로."""
    if re.match(r"^task-\d+$", task_id):
        return f"{task_id}.0"
    return task_id
```

- `start` 명령: `task-417` → `task-417.0`으로 저장 (또는 원본 유지)
- `end` 명령: `task-417` → fuzzy match로 `task-417.*` 중 running 찾기
- `status` 명령: 동일 fuzzy match

#### (3) task-timer.py: end_task에 fuzzy match 추가

```python
def end_task(self, task_id: str) -> Dict:
    # 정확한 매치 먼저 시도
    if task_id in self.timers["tasks"]:
        return self._do_end(task_id)

    # task-N 형식이면 task-N.* 패턴으로 running 항목 검색
    if re.match(r"^task-\d+$", task_id):
        prefix = task_id  # e.g., "task-417"
        candidates = [
            k for k in self.timers["tasks"]
            if k.startswith(prefix + ".") and self.timers["tasks"][k].get("status") == "running"
        ]
        if len(candidates) == 1:
            return self._do_end(candidates[0])
        elif len(candidates) > 1:
            return {"status": "error", "reason": f"Multiple running tasks match '{task_id}': {candidates}"}

    return {"status": "error", "reason": f"Task '{task_id}' not found"}
```

#### (4) dispatch.py: timer start 실패 시 재시도 로직

```python
timer_result = subprocess.run(timer_cmd, capture_output=True, text=True, timeout=30)
if timer_result.returncode != 0:
    # task-N 형식이면 .0 붙여서 재시도 (하위호환)
    if re.match(r"^task-\d+$", task_id):
        normalized = f"{task_id}.0"
        retry_cmd = ["python3", str(TASK_TIMER), "start", normalized, "--team", team_id, "--desc", short_desc]
        subprocess.run(retry_cmd, capture_output=True, text=True, timeout=30)
```

#### (5) DIRECT-WORKFLOW.md: 주석 보강 (선택적)

Step 7 옆에 힌트 추가:
```
7. task-timer end: `python3 {WORKSPACE_ROOT}/memory/task-timer.py end {task_id}`
   (task-N 형식도 자동 매핑됨)
```

### 5.3 테스트 시나리오

1. `task-timer.py end task-417` → task-417.* 중 running 찾아서 종료 ✓
2. `task-timer.py end task-417.1` → 정확한 매치로 종료 ✓
3. `task-timer.py start task-500` → task-500.0으로 정규화하여 등록 ✓
4. `task-timer.py start task-500.3` → 그대로 등록 ✓
5. `task-timer.py end task-999` → "Task not found" ✓
6. `task-timer.py end 417` → 형식 에러 (task- 접두사 필수) ✓

## 6. 장기 개선 제안

방안 D로 즉시 문제를 해결한 후, 다음 단계로:

1. **`.N` 접미사 폐지 검토**: 실제로 서브태스크 분할을 사용하는 곳이 없다면, `generate_task_id()`에서 `.1`을 제거하고 `task-N`으로 통일 (방안 B)
2. **task-timers.json 정리**: 중복/orphan 엔트리 클린업 스크립트 작성
3. **형식 표준 문서화**: task_id 형식 규격을 명확히 정의하는 ADR(Architecture Decision Record) 작성

## 7. 참고 파일 목록

- `/home/jay/workspace/memory/task-timer.py` — task-timer 스크립트 (Line 28: TASK_ID_PATTERN, Line 537-548: CLI end 검증)
- `/home/jay/workspace/dispatch.py` — 위임 스크립트 (Line 71: generate_task_id, Line 285-294: dispatch 내 timer start)
- `/home/jay/workspace/prompts/DIRECT-WORKFLOW.md` — 팀장 워크플로우 (Line 71: task-timer end 지시)
- `/home/jay/workspace/prompts/team_prompts.py` — 프롬프트 생성 (Line 143: timer_end 경로)
- `/home/jay/workspace/scripts/notify-completion.py` — 완료 통보 스크립트
- `/home/jay/workspace/scripts/completion-handler-instructions.md` — 완료 처리 지시문 (Step 4: task-timer end)
