# task-2779_b+1 보고서 — Axis B HIGH microfix: `_is_production_canonical` 하위경로 차단

- 작업 ID: task-2779_b+1
- 팀: dev2-team (오딘)
- 완료 후보 상태: `TASK2779B_AXIS_B_STOP_HOOK_DRYRUN_HIGH_MICROFIX_DONE_ACTIVE_FALSE`
- 대상 PR: #260 (브랜치 `task/task-2779_b-dev2`, head c0f92f1b → **1d8f4932** fast-forward 갱신, **미머지**)
- 작성일: 2026-06-30

## Situation (상황)
PR #260의 Axis B dry-run Stop hook terminal enforcer는 운영 canonical root(`/home/jay/workspace`)에 marker를 생성하지 않도록 `_is_production_canonical()` 안전장치를 둔다. fresh Gemini(head c0f92f1b) 리뷰에서 active HIGH ×2(동일 이슈, line 61)가 제기되었다.

## Complication (문제)
`_is_production_canonical()`이 `given == prod`(정확 일치)만 차단했다. canonical_root를 운영 root의 **하위 디렉토리**(예: `/home/jay/workspace/sub_project`)로 주면 검사를 우회 → 운영 환경 하위에 marker/manifest 생성 위험. Axis B 핵심 safety invariant("실제 canonical marker 0")의 구멍.

## Question (질문)
운영 root와 정확히 일치하는 경우뿐 아니라 **하위경로(under-prod)** 도 운영으로 간주하여 거부하되, sibling-prefix(`/home/jay/workspace_evil`)나 격리 tmp root는 오판/거짓양성 없이 허용할 수 있는가?

## Answer (해결)
1줄 수준 boundary microfix로 해결.

### 수정 내용
파일: `scripts/stop_hook_terminal_enforcer.py` (line 61)
```diff
-        return given == prod
+        return given == prod or prod in given.parents
```
- `prod in given.parents` = "운영 root가 given의 상위 디렉토리 중 하나" = given이 운영 root의 하위경로. 이를 거부에 포함.
- resolve(strict=False) 기반 symlink/상대경로 방어 및 resolve 실패 시 fail-safe True(거부) 동작은 **무변경**.
- broad refactor 없음. 해당 함수 1줄 + 테스트만 변경.

### 신규 테스트 2건
파일: `tests/regression/test_stop_hook_terminal_enforcer_2779.py`
- `test_subpath_of_production_canonical_refused`: 운영 root 하위경로(`/home/jay/workspace/sub_project`) canonical → `refused_production=True`, `surfaced_index_path==""`, REFUSED status 포함, 운영 하위경로에 json 미생성, `_is_production_canonical()` 직접 검증(하위경로·정확일치 모두 True).
- `test_sibling_prefix_not_under_production_allowed`: sibling-prefix(`/home/jay/workspace_evil`) → `_is_production_canonical=False`(오판 금지), 격리 tmp root → False(거짓양성 0).

## 생성/수정 파일 목록
- (수정) `scripts/stop_hook_terminal_enforcer.py` — line 61, 1줄
- (수정) `tests/regression/test_stop_hook_terminal_enforcer_2779.py` — 신규 테스트 2건(+47줄)
- (commit) `1d8f4932` — c0f92f1b의 직계 자식(fast-forward, force 0)

## 테스트 결과
- `python3 -m py_compile scripts/stop_hook_terminal_enforcer.py` → OK
- `pytest test_stop_hook_terminal_enforcer_2779.py` → **12 passed** (기존 10 + 신규 2)
- 축 A `test_pre_push_terminal_guard_2779.py` → **8 passed** (무회귀)
- 2778 `test_terminal_artifact_visibility_2778.py` → **31 passed** (무회귀)
- 합계 51 passed, 회귀 0

## L1 스모크테스트 결과 (필수)
- 서버 재시작: **해당없음** (이 모듈은 dry-run 라이브러리 함수, 상시 서버/데몬 아님 — 실제 hook 설치·strict 금지 contract)
- API 응답 확인: **해당없음** (HTTP API 아님)
- 실동작 검증(subprocess/함수 직접 실행): **성공**
  - `_is_production_canonical` 직접 호출:
    - `/home/jay/workspace` → True (거부)
    - `/home/jay/workspace/sub_project` → True (거부, 이번 HIGH)
    - `/home/jay/workspace/a/b/c` (deep subpath) → True (거부)
    - `/home/jay/workspace_evil` (sibling) → False (허용, 오판 0)
  - `run_enforcer(canonical_root=운영 root)` 및 `run_enforcer(canonical_root=운영 root/sub_project)`:
    - 둘 다 `refused_production=True`, `blocked=False`, `exit_code=0`(dry-run 비차단), `surfaced_index_path=""`, **marker_created=False**
  - 운영 root 하위 leaked marker 스캔(`glob /home/jay/workspace/**/*L1smoke*`) → `[]` (0건)
- 스크린샷: 해당없음 (CLI/라이브러리 작업)

## 필수 요구(12) 충족
1. ✅ `given == prod or prod in given.parents`
2. ✅ 운영 root 하위경로 marker 거부 (L1 + test)
3. ✅ 정확 일치 root 무회귀 거부 (기존 test 통과)
4. ✅ 격리 tmp root 정상 허용 (거짓양성 0)
5. ✅ broad refactor 0 — 함수 1줄 + 테스트만
6. ✅ dry-run 비차단/fail-open 무변경 (exit_code=0 확인)
7. ✅ 실제 Stop hook 설치 0 · 실제 종료 차단 0 · strict 0
8. ✅ callback/real fire/systemd/cron 0 · raw/ANU key 0 (소스 정적 테스트 통과, git/pytest만 사용)
9. ✅ finish-task.sh 본체 변경 0
10. ✅ P0c 증거 + 축 A 산출물 무변경 (축 A 8 pass, diff 2파일만)
11. ✅ expected_files 밖 수정 0 · forbidden paths 변경 0 (`git diff --name-only c0f92f1b..1d8f4932` = 2파일)
12. ✅ ACTIVE=false 유지 (활성화 코드 0)

## 검증 요구(10) 충족
1. ✅ py_compile OK
2. ✅ pytest 2779 stop-hook PASS (12)
3. ✅ 축 A + 2778 PASS (8+31, 무회귀)
4. ✅ L1: 운영 root 및 하위경로 모두 marker 생성 0
5. ✅ raw/ANU/self-key 0
6. ✅ expected_files 밖 0 · forbidden 0
7. ✅ callback/fire/systemd/cron/activation 0 (executor self 발사 0)
8. ✅ PR #260 head fast-forward only(c0f92f1b..1d8f4932) · force 0 · 새 PR 0 · merge 0
9. ✅ ACTIVE=false · 증거 무변경
10. ✅ strict/blocking·실제 hook 미포함

## 발견 이슈 및 해결
- **이슈(deviation B)**: 브랜치 `task/task-2779_b-dev2`의 underscore(`_b`)가 pre-commit/pre-push hook의 `task/task-N-bot` 정규식과 불일치하여 commit/push가 차단됨. task 문서에 명시된 알려진 deviation B.
  - **해결**: task가 명시적으로 허용한 공식 경로 `TASKCTL_BYPASS=1` + `TASKCTL_BYPASS_REASON`(branch-pattern 1건만)으로 commit·push 수행. evidence가 `.tasks/evidence/`에 자동 atomic 기록됨(2건: pre-commit, pre-push). force/rebase 없이 순수 fast-forward.
- 범위 내 미해결 0건.

## 머지 판단
- **머지 필요**: No (merge_policy=none — head 갱신까지만)
- **브랜치**: `task/task-2779_b-dev2`
- **워크트리 경로**: `/home/jay/workspace/.worktrees/task-2779_b-dev2`
- **머지 의견**: 봇 merge 금지. 후속(별도 경로): ANU 독립검증 → fresh Gemini active HIGH/Critical 0 → CI → 회장 승인 별도 → MERGE_CANDIDATE_READY. 코드 품질: 1줄 boundary 강화 + 테스트 2건, 회귀 0, 충돌 가능성 낮음(2파일 격리).

## 모델 사용 기록
- 토르(백엔드, 구현+테스트): **sonnet** — 일반 코딩/로직. haiku 미사용.
- 헤임달(테스터) 역할 검증: 팀장(오딘)이 직접 회귀/L1 실행(외부 네트워크 의존 없는 결정론 검증, 토큰 효율).
- 팀장(오딘, Opus): 설계/분배/검토/통합/게이트만. 직접 코딩 0.

## 게이트
- G1 설계: affected_files 2개(`scripts/stop_hook_terminal_enforcer.py`, `tests/regression/test_stop_hook_terminal_enforcer_2779.py`) — 다른 팀 겹침 없음. ✅
- G2 구현: 기능 테스트(12) + 회귀(39) + L1 실동작 PASS. ✅
- G3 머지: merge_policy=none이므로 PR head ff 갱신까지만 수행, 봇 merge 미수행. 후속은 회장 승인 별도. ✅

## callback / 종료 규칙
- executor self-key callback 발사 0. `callback_schedule_created: false` (OS-level pickup runner가 owner-proof 후 ANU-owned closeout 소유).
- finish-task.sh가 canonical p0b_inbox result.json(owner_pickup_required=true, raw ANU key 0)을 작성 → 별도 ANU-key(P0-b) runner가 normal callback 발사.
- relay_hints 필드 부재 → green deterministic closeout.
