---
qc_verdict: WARN
---

# task-2517 보고서 — canonical_workspace_resolver (P0 runtime determinism)

## QC Verdict

WARN


- 작업 ID: task-2517
- 팀: dev2-team (오딘 / 토르 / 헤임달)
- 레벨: Lv.3+ (P0 — 회장 명시 highest-priority runtime hardening)
- 일시: 2026-05-09
- 작업 유형: runtime infrastructure hardening + 회귀 테스트

---

## SCQA 요약

**S**: 회장 결정(2026-05-09): 5 모듈 + wiring + e2e 완료(task-2509~2515 merged) 후, 시스템 병목은 기능 부족이 아닌 runtime determinism / repository governance / lifecycle consistency. task-2516+1에서 **wrong cwd / stale main / dirty workspace false detection / finish-task context mismatch** 4 ambiguity가 노출되었음.

**C**: 6 hook(scope-guard / finish-task / guard / smoke / qc / merge_execution)이 각자 독립적으로 cwd/worktree/main_head를 평가하면 4 ambiguity가 매번 재발. 단일 deterministic 인스턴스를 한 번 resolve하고 6 hook이 공유해야 일관성이 보장됨.

**Q**: 어떻게 단일 entry로 6 hook이 공유 가능한 deterministic CanonicalWorkspace를 제공하면서, 회장 §forbidden 19종(특히 "새 abstraction 과다 생성 금지", "5 모듈 본체 수정 금지", "task-2518/2519 wiring 직접 시도 금지")을 모두 준수할 수 있는가?

**A**: `utils/canonical_workspace_resolver.py` 신규 1 모듈에 frozen dataclass `CanonicalWorkspace` + `resolve_canonical_workspace` entry + 5 assert/evaluate 함수 + `resolve_for_hooks(6 hook)` 통합 인터페이스 + CLI 제공. 회귀 14건으로 4 ambiguity fixture-level 해소를 직접 검증. 본 task는 **인터페이스만 제공**(spec line 137 명시), 실제 hook wiring은 후속 task-2518/2519 책임.

## 수정 파일별 검증 상태

| 파일 | 변경 내용 | grep 검증 | 상태 |
|---|---|---|---|
| utils/canonical_workspace_resolver.py | 신규 — CanonicalWorkspace + 7 함수 + CLI | grep "resolve_canonical_workspace" OK | verified |
| tests/regression/test_canonical_workspace_resolver_2517.py | 신규 — 14 회귀 테스트 (정상4 + ambiguity4 + edge3 + hook3) | grep "test_wrong_cwd_raises" OK | verified |

---

## 작업 결과

### 신규 파일 (정확히 2개 — expected_files 매칭)

| 파일 | 줄 수 | 설명 |
|---|---|---|
| `utils/canonical_workspace_resolver.py` | 450줄 | CanonicalWorkspace + 7 함수 + CLI |
| `tests/regression/test_canonical_workspace_resolver_2517.py` | 480줄 | 14 회귀 테스트 |

### 미수정 파일 (회장 §forbidden 준수)
- `utils/automation_contracts.py` (READ ONLY) — 0 byte 수정
- `utils/{merge_queue_executor, replacement_pr_runner, auto_gemini_triage, post_merge_smoke_runner, critical_escalation_reporter, merge_topology_gate}.py` — 0 byte 수정
- `dispatch.py` — 0 byte 수정
- `scripts/finish-task.sh` / `scripts/start_task_guard.py` — 0 byte 수정
- task-2516/2516+1 영역(`replacement_pr_runner.py`) — 0 byte 수정
- task-2518/2519 wiring — 직접 시도 0회

### 커밋 이력 (worktree `task/task-2517-dev2`)

| SHA | 메시지 |
|---|---|
| 5671cbe8 | utils/canonical_workspace_resolver.py 신규 (CanonicalWorkspace + 7 함수 + CLI) |
| cb095a34 | pyright 경고 해소 (_kwargs unused) |
| e3208554 | tests/regression/test_canonical_workspace_resolver_2517.py 신규 (14 tests PASS) |
| 93637ea6 | pyright unused imports 정리 (Optional/MagicMock, stateful_runner cwd 사용) |
| 59a3168a | workspace_root는 main worktree로 결정 (git worktree list 첫 entry 우선) + is_main 정확 판정 |

---

## 핵심 구현 (회장 §필수 7건)

### 1. CanonicalWorkspace dataclass (frozen=True, immutable snapshot)
```python
@dataclass(frozen=True)
class CanonicalWorkspace:
    task_id: str
    workspace_root: Path     # 항상 main repo root (git worktree list 첫 entry)
    worktree_path: Path      # task별 worktree
    branch_name: str
    main_head_sha: str       # resolve 시점 lock
    base_sha: str            # diff base = main_head_sha
    cwd: Path
    is_main: bool            # cwd == workspace_root
    is_clean: bool
```

### 2. `resolve_canonical_workspace(task_id, *, cwd=None, fetch=True, runner=None)`
- cwd 정규화 → `git rev-parse --show-toplevel` (seed) → `git worktree list --porcelain`
- 첫 entry = main worktree (git spec) → workspace_root authoritative
- task_id 매칭 worktree → worktree_path + branch_name
- 매칭 worktree 없으면 expected `<workspace_root>/.worktrees/<task_id>-*` glob → 없으면 산출만(생성 X)
- fetch=True → `git fetch origin main --quiet` + `git rev-parse origin/main` → main_head_sha lock
- 환경변수 WORKSPACE_ROOT는 git 결과 우선 (spec §2 명시)

### 3. `assert_cwd_matches_workspace(ws)` — `RuntimeError("WRONG_CWD: ...")`
### 4. `assert_main_fresh(ws, *, runner=None)` — SHA lock 비교 (Codex 제안 반영, 5초 시간 규칙 → SHA 비교로 단순화). `RuntimeError("STALE_MAIN: ...")`
### 5. `evaluate_scope_dirty(ws, expected_files, *, runner=None)` — `git status --porcelain -- <files>` (scope-aware, false detection 직접 해소)
### 6. `assert_finish_task_context(ws, finish_target, *, runner=None)` — 3축(head_sha + branch_name + worktree_path) 일치 검증. `RuntimeError("FINISH_TASK_CONTEXT_MISMATCH: ...")`
### 7. `resolve_for_hooks(task_id, hook_name, ...)` — 6 hook 통합 인터페이스. `_ALLOWED_HOOKS = {"scope-guard", "finish-task", "guard", "smoke", "qc", "merge_execution"}`. invalid → `ValueError("UNKNOWN_HOOK: ...")`

### CLI: `python3 utils/canonical_workspace_resolver.py --task-id <id> [--json|--assert-cwd|--assert-fresh|--scope=...|--no-fetch]`

---

## 회귀 테스트 14/14 PASS

| # | 카테고리 | 테스트 | 결과 |
|---|---|---|---|
| 1 | 정상 | test_resolve_in_main_repo | PASS |
| 2 | 정상 | test_resolve_in_worktree | PASS |
| 3 | 정상 | test_main_head_sha_lock | PASS |
| 4 | 정상 | test_env_var_priority_git_wins | PASS |
| 5 | 4 ambiguity | test_wrong_cwd_raises (WRONG_CWD) | PASS |
| 6 | 4 ambiguity | test_stale_main_raises (STALE_MAIN) | PASS |
| 7 | 4 ambiguity | test_dirty_workspace_false_detection (scope-aware clean) | PASS |
| 8 | 4 ambiguity | test_finish_task_context_mismatch (FINISH_TASK_CONTEXT_MISMATCH) | PASS |
| 9 | edge | test_resolve_when_worktree_missing | PASS |
| 10 | edge | test_env_var_project_path_stale | PASS |
| 11 | edge | test_canonical_workspace_json_round_trip | PASS |
| 12 | hook | test_hook_scope_guard_shares_workspace | PASS |
| 13 | hook | test_hook_finish_task_shares_workspace | PASS |
| 14 | hook | test_hooks_share_main_head_sha | PASS |

```
============================== 14 passed in 0.10s ==============================
```

---

## L1 스모크테스트 결과

- **서버 재시작**: 해당없음 (stateless utility 모듈, 서버 비동반)
- **API 응답 확인**: 해당없음 (HTTP API 아님)
- **CLI 실행 확인** (필수 — task spec goal_assertions):
  - **Goal 1 (import)**:
    ```
    $ python3 -c "from utils.canonical_workspace_resolver import CanonicalWorkspace, resolve_canonical_workspace; print('import ok')"
    import ok
    ```
  - **Goal 2 (pytest 14/14)**:
    ```
    $ python3 -m pytest tests/regression/test_canonical_workspace_resolver_2517.py -q
    14 passed in 0.10s
    ```
  - **Goal 3 (CLI JSON, worktree에서 실행)**:
    ```json
    {
      "task_id": "task-2517",
      "workspace_root": "/home/jay/workspace",
      "worktree_path": "/home/jay/workspace/.worktrees/task-2517-dev2",
      "branch_name": "task/task-2517-dev2",
      "main_head_sha": "05259f816557a101d50456ddd59e9b8f2f1baf73",
      "base_sha": "05259f816557a101d50456ddd59e9b8f2f1baf73",
      "cwd": "/home/jay/workspace/.worktrees/task-2517-dev2",
      "is_main": false,
      "is_clean": true
    }
    ```
  - **L1 추가 검증** (main repo에서 실행):
    ```
    workspace_root: /home/jay/workspace
    worktree_path: /home/jay/workspace/.worktrees/task-2517-dev2
    cwd: /home/jay/workspace
    is_main: true   ← 정확
    ```
  - **L1 추가 검증** (`--assert-cwd` from worktree):
    ```
    exit=0  (assert_cwd_matches_workspace PASS — 워크트리 cwd는 ws.worktree_path와 일치)
    ```
- **스크린샷**: 해당없음 (UI 아님)

---

## 발견 이슈 및 해결

### 이슈 1: pyright unused imports 4건 (해결)
- `_kwargs` (resolver line 88), `Optional` / `MagicMock` (테스트 line 13-14), `cwd` (테스트 line 248)
- **해결**: 토르가 `**_kwargs` 제거(RunnerType은 `Callable[..., ...]`로 가변), 헤임달이 unused import 제거 + stateful_runner의 cwd를 KeyError 메시지에 사용
- **재검증**: pyright unused 진단 0건, pytest 14/14 유지

### 이슈 2: workspace_root / is_main 오판 (해결 — CLI 실시 검증으로 발견)
- 초기 구현은 `git rev-parse --show-toplevel`만 사용 → worktree에서 실행 시 worktree path를 workspace_root로 잘못 인식 → `is_main=true` 오판
- 회귀 테스트는 mock에서 main_root를 반환하도록 stub되어 PASS했으나, 실제 CLI 실행 시 spec(`workspace_root: 항상 main repo root`)과 어긋남
- **해결**: `git worktree list --porcelain`의 **첫 entry**를 main worktree(workspace_root) authoritative로 사용. spec §2 "git rev-parse와 충돌 시 git 우선" 준수 + 진짜 main repo root 반환
- **재검증**: 워크트리에서 `is_main=false`, main에서 `is_main=true` 정확 판정. pytest 14/14 유지(테스트도 실제 git 동작 반영하도록 mock 정렬)

### 이슈 3: import resolution 진단 1건 (워크트리 외부 pyright 환경 문제 — 무해)
- `Import "utils.canonical_workspace_resolver" could not be resolved`
- main repo에는 신규 모듈이 아직 없어 워크트리 외부 pyright가 못 봄. 워크트리에서는 정상 import + pytest PASS
- 머지 후 자연 해소 (별도 조치 불필요)

---

## Codex 사전 검증 대응 (G1 게이트)

Codex 5건 risk:
- **Critical 1 (구현물 부재)**: 시작 단계 자연 발생 → 구현 완료 후 자연 해소
- **Critical 2 (완료 조건 vs 변경 제한 충돌)**: spec line 137 "본 task는 인터페이스만 제공, 실제 hook 통합은 task-2518/2519"가 명시 → 본 task scope 정확. 인터페이스 + fixture-level 검증까지가 본 task 범위. context-notes.md에 명시
- **High 1 (dirty workspace 실제 운영 경로 false detection)**: 본 task는 **인터페이스 + fixture-level 해소**. wiring(실제 finish-task.sh / guard 통합)은 후속 task-2518/2519. spec 일관 → 본 task scope 외
- **High 2 (cwd/worktree 판정 분산)**: 신규 resolver는 기존 `utils/worktree_resolver.py` 보다 상위 통합 모듈. 우선순위는 후속 wiring task에서 deprecation 결정. 현 task는 단일 entry 제공
- **Medium (stale 5초 규칙 모호)**: Codex 제안 반영 → SHA lock vs 재검증 SHA 비교로 단순화 (assert_main_fresh 구현)

---

## 머지 판단

- **머지 필요**: Yes
- **브랜치**: `task/task-2517-dev2`
- **워크트리 경로**: `/home/jay/workspace/.worktrees/task-2517-dev2`
- **머지 의견**:
  - expected_files 정확 매칭 (2/2 신규 파일만)
  - 회귀 14/14 PASS, goal_assertions 3/3 PASS
  - 5 모듈 / automation_contracts / dispatch.py / finish-task.sh / start_task_guard.py / replacement_pr_runner.py 모두 0 byte 수정
  - PR #52/#49/#50/#51 미수정 (커밋 영향 없음)
  - force push / rebase / admin override / cherry-pick / live pilot / Critical 7종 외 회장 보고 모두 0회
  - amendment / mid-dispatch correction 무시 0건
  - 본 task 완료 후 P1 task-2518/2519(spec/테스트 설계 병렬 가능, wiring은 serial)로 진행 가능

---

## 모델 사용 기록

| 페르소나 | 작업 | 모델 | 정당성 |
|---|---|---|---|
| 오딘 | 설계, 검토, 통합, 보고 | opus (parent) | 팀장 — 판단/검토 |
| 토르 | utils/canonical_workspace_resolver.py 작성 + workspace_root fix + pyright fix | sonnet | 일반 코딩/로직 구현 |
| 헤임달 | tests/regression/... 14 회귀 테스트 작성 + pyright unused 정리 | sonnet | 일반 코딩/테스트 |

haiku 사용 0건 (Lv.3+ critical task — 비용보다 품질 우선).

---

## affected_files (Merge Topology Gate 자기참조)

```yaml
expected_files:
  - utils/canonical_workspace_resolver.py
  - tests/regression/test_canonical_workspace_resolver_2517.py

actual_diff (origin/main...HEAD):
  - utils/canonical_workspace_resolver.py        # NEW 450줄
  - tests/regression/test_canonical_workspace_resolver_2517.py  # NEW 480줄

match: EXACT (2/2)
forbidden_path_intrusion: 0
```

---

## .done 생성 상태 (시스템 운영 메모)

본 task의 본질적 작업(코드/테스트/머지)은 완료되었습니다:
- ✅ PR #68 origin/main 머지 완료 (commit `4d45947e`)
- ✅ origin/main에 신규 2 파일 정상 반영 (`git ls-tree origin/main`로 확인)
- ✅ task-timer end 완료 (40분 18초, qc_result=PASS_WITH_WARN)

**그러나 `memory/events/task-2517.done` 파일은 자동 생성 차단됨**. 사유:

1. main repo working tree가 `task/task-2479-dev1` 브랜치에 있어 origin/main 신규 파일이 working directory에 보이지 않음 (`task/task-2479-dev1`에는 미반영)
2. main repo가 origin/main 대비 33 commits ahead + 90 behind 상태(시스템 운영 진행 중인 다른 task의 변경 다수)
3. finish-task.sh의 G3 game이 `WORKSPACE_ROOT=/home/jay/workspace`의 working directory를 검사 → 신규 2 파일 없음 → `file_existence FAIL`
4. 회장 §forbidden "manual .done 생성 절대 금지" 준수 → 우회 X

**이는 본 task가 구조적으로 해소하려는 task-2516+1 §4 ambiguity 중 "wrong cwd / dirty workspace false detection / finish-task context mismatch"의 실사례**입니다. 본 task의 `canonical_workspace_resolver`가 wiring(task-2518/2519)되면 이러한 G3 실패가 fixture-level이 아닌 runtime-level에서 자동 회피됩니다.

### 회장/아누 후속 처리 권고

운영 환경 정리 후 다음 한 줄로 .done 자동 생성 가능:
```bash
# main repo working tree를 origin/main HEAD에 동기화 후
cd /home/jay/workspace
git checkout main && git fetch origin && git reset --hard origin/main
# 그 후 (이미 PR #68 머지된 상태이므로) 단순 G3 재검증으로 .done 생성:
bash scripts/finish-task.sh task-2517 dev2 /home/jay/workspace/.worktrees/task-2517-dev2
```

또는 시스템 운영 정리 후 자연스럽게 main에 동기화되면 `bash scripts/finish-task.sh task-2517 dev2`로 .done 생성됨.

## 후행 권고 (회장 §P1/P2)

본 task 완료 후:
- **P1 (병렬 설계 가능, wiring serial)**:
  - task-2518 — lifecycle_reconciliation_manager (CanonicalWorkspace 인스턴스 사용)
  - task-2519 — repository_policy_adapter
- **P2**:
  - task-2520 — low-risk live pilot
  - task-2521 — automation observability/dashboard

본 모듈은 6 hook 통합 인터페이스만 제공 — 실제 wiring(`scripts/finish-task.sh` / `scripts/start_task_guard.py` / 5 모듈)은 task-2518/2519의 책임.

## 세션 통계
- 총 도구 호출: 0회

