# CI_WATCH_HANDOFF_REAL_PR_ACTIVATION_GATE spec (task-2643 초안) — 260523

회장 결정 (2026-05-23 20:10 KST · task-2642 final classification 정정 직후): **CI_WATCH_HANDOFF_RUNNER 를 실제 PR lifecycle 에 연결하기 전 activation gate 만든다. BOT App token / push / PR write / watcher handoff / callback / rollback / forbidden action 경계를 코드와 schema 와 regression 으로 고정한다.**

기반: `system_ci_watch_handoff_policy_spec_260523.md` (정책 single source) + `system_ci_watch_handoff_runner_spec_260523.md` (runner spec) + `task_2642_FINAL_CLASSIFICATION_260523.json` (정정 박제) + `callback_registration_status_snapshot_mismatch_260523.json` (non-blocking finding · H1/H2/H3 후보 검증 대상).

---

## 1. 본 task 범위 (★ 코드 runtime 0 · activation gate 정책/스키마/regression 만)

### 1.1 핵심 원칙
- **real auto-merge activation 0** 유지 (ANU_REAL_MERGE_EXECUTOR_ACTIVE default=False hardcoded)
- **production PR write 0** 유지 (gh pr create / gh pr merge / gh api push 실 호출 0)
- **BOT App token 실사용 0** (token scope contract 만 정의 · 실 인증 0)
- **policy/spec/schema/fixture/test 기반 activation gate 만 구현** (회장 verbatim)
- **activation decision packet** 에 회장 최종 승인 필요 필드 포함

### 1.2 신규 산출물 (회장 verbatim 8 필수)

| # | 산출물 | 위치 | 책임 |
|---|---|---|---|
| 1 | **activation gate spec** | `memory/specs/system_ci_watch_handoff_real_pr_activation_gate_spec_260523.md` (본 파일) | 정책 single source · gate flow 정의 |
| 2 | **token scope contract** | `utils/ci_watch_handoff_token_scope_contract.py` | BOT App token scope/endpoint allowlist · raw token 차단 · 실 호출 0 |
| 3 | **real PR lifecycle dry-run fixture** | `tests/fixtures/ci_watch_handoff_real_pr_lifecycle/<6 시나리오>/` | open/push/nudge/thread resolve/callback/disable flag 시뮬 (live API 0) |
| 4 | **callback registration status mismatch regression** | `tests/regression/test_callback_registration_status_mismatch.py` | non-blocking finding H1/H2/H3 후보 검증 + register-after-update 패턴 단언 |
| 5 | **watcher handoff terminal-state regression** | `tests/regression/test_watcher_handoff_terminal_state_real_lifecycle.py` | 5 terminal_states × real lifecycle 시뮬 |
| 6 | **forbidden action regression** | `tests/regression/test_real_pr_activation_forbidden_action_guard.py` | finish-task.sh/cokacdir/replacement_pr_runner/admin override/PR #141 pilot 모든 forbidden hit = NO_OP_FORBIDDEN |
| 7 | **rollback / disable flag contract** | `utils/ci_watch_handoff_disable_flag_contract.py` | activation_flag toggle OFF · rollback decision schema · post-failure cleanup |
| 8 | **final activation decision packet** | `memory/specs/ci_watch_handoff_real_pr_activation_decision_packet_template_260523.json` | 회장 최종 승인 필요 필드 (verbatim signature / scope_limit / ttl / dryrun_to_real byte-equal / post-activation smoke / rollback authority) |

### 1.3 신규 helper / 검증자 (코드)
- `utils/ci_watch_handoff_token_scope_contract.py` — BOT App token scope 정의 · forbidden endpoint hard-block · raw token redaction · 실 호출 0
- `utils/ci_watch_handoff_disable_flag_contract.py` — activation_flag 정의 (ENV_NAME `ANU_CI_WATCH_HANDOFF_REAL_PR_ACTIVE` default=False) · rollback decision schema · disable-on-failure 자동 trigger
- `utils/ci_watch_handoff_activation_decision_packet.py` — 회장 verbatim signature 검증 + scope_limit 검증 + per-PR/per-head 1회 hard limit

### 1.4 fixture 6 시나리오 (real PR lifecycle dry-run)
1. `real_pr_open_token_scope_pass_dryrun_only` → PASS_DRYRUN (실 호출 0)
2. `real_pr_open_forbidden_endpoint_blocked` → NO_OP_FORBIDDEN_ENDPOINT (issues comments POST 외 차단)
3. `real_pr_open_activation_flag_off_no_op` → NO_OP_ACTIVATION_FLAG_OFF (default=False fail-closed)
4. `real_pr_open_activation_flag_on_but_no_chair_decision` → NO_OP_NO_CHAIR_DECISION_PACKET
5. `real_pr_open_full_pipeline_dry_run_pass` → DRYRUN_PASS_AWAITING_CHAIR (chair_authorization 미발급 상태에서 dryrun lifecycle 완료)
6. `real_pr_open_rollback_disable_flag_triggered` → ROLLBACK_DISABLE_FLAG_AUTO_TRIGGERED (post-failure smoke fail 시 자동 disable)

### 1.5 regression 5 (★ PR #145 사고 박제 승격으로 1건 추가)
- `test_callback_registration_status_mismatch.py` (H1/H2/H3 후보 + register-after-update 패턴)
- `test_watcher_handoff_terminal_state_real_lifecycle.py` (5 terminal_states × dry-run)
- `test_real_pr_activation_forbidden_action_guard.py` (forbidden 15 + admin override + PR #141 + finish-task.sh + cokacdir + replacement_pr_runner)
- `test_ci_watch_handoff_real_pr_activation_decision_packet.py` (decision packet schema · 회장 signature 검증)
- ★ **`test_anu_direct_polling_violation_guard.py`** (PR #145 ANU_DIRECT_CI_POLLING_POLICY_VIOLATION 박제 승격 · 회장 verbatim 2026-05-23)

### 1.6 ★ task-2643 추가 필수 acceptance (회장 verbatim · PR #145 사고 박제 승격)

`tests/regression/test_anu_direct_polling_violation_guard.py` 의무 포함 7 항목:

1. **ANU 본체가 `gh pr view/checks/statusCheckRollup` 반복 polling loop 만들면 fail**
2. **`run_in_background` + `sleep` + `gh pr view` 조합을 CI watch 목적으로 쓰면 fail**
3. **"CI 재실행 대기 백그라운드 진입" 문구 또는 동등 행위를 forbidden pattern 으로 감지** (subprocess/Bash command string 정적 검사)
4. **허용 경로는 watcher callback/envelope 수신 뿐**
5. **bridge watcher/dev bot 내부 polling 은 허용하되 collector_role 과 owner 가 ANU 본체이면 fail** (위장 폴링 차단)
6. **callback-only reporting=false 이면 fail** (handoff envelope `callback_on_terminal_state=true` 강제)
7. **session-bound polling vs contract-bound polling 명시 구분** (task_id/pr/head_sha/terminal_states/ttl/callback_target/dup_policy/owner/collector_role 9 필드 전부 존재 시 contract-bound 인정 · 누락 시 fail)

박제 사건:
- `memory/feedback_anu_direct_polling_violation_pr_145_260523.md`
- `memory/events/anu_direct_ci_polling_violation_pr_145_260523.json`
- 위반 schedule: `bzaona6au` (claude-code background task · 2026-05-23 20:53 KST · 정책 도입 ~75분 후 위반)

---

## 2. activation gate 흐름 (state machine)

```
PR_OPEN_INTENT (handoff received)
    ↓
[0a] ACTIVATION_FLAG check (default=False)
    ├─ False → NO_OP_ACTIVATION_FLAG_OFF (dryrun mode · 모든 후속 동작 = mock)
    └─ True → 0b
[0b] CHAIR_DECISION_PACKET check
    ├─ missing → NO_OP_NO_CHAIR_DECISION_PACKET (verbatim signature 부재)
    ├─ expired → NO_OP_CHAIR_DECISION_EXPIRED
    └─ verified → 0c
[0c] TOKEN_SCOPE check (BOT App token allowlist)
    ├─ scope insufficient → NO_OP_TOKEN_SCOPE_INSUFFICIENT (403 expected)
    └─ ok → 0d
[0d] FORBIDDEN_ACTION_GUARD
    ├─ finish-task.sh / cokacdir / replacement_pr_runner / admin override / PR #141 pilot → NO_OP_FORBIDDEN
    └─ ok → 0e
[0e] DRYRUN_TO_REAL_BYTE_EQUAL check (dryrun output == real plan)
    ├─ mismatch → NO_OP_DRYRUN_REAL_MISMATCH
    └─ match → REAL_PR_LIFECYCLE_ENTER (★ 본 task 는 여기까지 정의만 · 실 실행 0)
```

### 2.1 ACTIVATION_FLAG default=False hardcoded
- ENV_NAME: `ANU_CI_WATCH_HANDOFF_REAL_PR_ACTIVE` (별도 신규)
- default: `False` (hardcoded constant)
- toggle 권한: 회장 verbatim signature 만
- post-failure auto-disable: post-activation smoke fail 시 즉시 OFF

### 2.2 CHAIR_DECISION_PACKET schema
```json
{
  "schema": "anu.ci_watch_handoff_real_pr_activation_decision.v1",
  "pr_number": int,
  "head_sha": str (40 hex),
  "expected_files_snapshot": list[str],
  "forbidden_paths_explicit": list[str],
  "ttl_seconds": int (≤3600),
  "verbatim_signature": str (회장 명시 한 줄),
  "scope_limit": {
    "real_pr_open": bool,
    "real_push": bool,
    "real_thread_resolve": bool,
    "real_callback_emit": bool,
    "real_merge": false (★ hardcoded · 본 task 에서는 0)
  },
  "rollback_authority": "ANU_AUTO_DISABLE_ON_SMOKE_FAIL" | "CHAIR_ONLY"
}
```

### 2.3 TOKEN_SCOPE_CONTRACT
- BOT App token (ghs_*) — scope `pull_requests:write` + `issues:write` 둘 다 필요
- single endpoint allowlist (회장 PR #98 OWNER_TRIGGER_ONLY_CAPABILITY 패턴 재사용):
  - `/repos/{owner}/{repo}/pulls` (open)
  - `/repos/{owner}/{repo}/pulls/{pr}/comments`
  - `/repos/{owner}/{repo}/issues/{pr}/comments` (Gemini nudge)
  - `/repos/{owner}/{repo}/git/refs` (push)
- forbidden endpoint hard-block (11 endpoint · PR #98 패턴 재사용)
- raw token 출력 금지 (token_hash_prefix 12 hex 만)
- 본 task 는 schema/contract 만 정의 · 실 호출 0

---

## 3. 회장 verbatim acceptance 검증 (8 + 4)

### 3.1 8 무수정 조건 (회장 verbatim)
- ✅ real auto-merge activation 0 (ACTIVATION_FLAG default=False)
- ✅ PR #141 pilot 실행 0 (forbidden_action_guard 차단)
- ✅ chair_authorization 발급 0 (본 task = template 만)
- ✅ BOT App token 실사용 0 (contract 만 · 실 호출 0 regression mock)
- ✅ production PR write 0 (dryrun_to_real byte-equal 단위 검증만)
- ✅ finish-task.sh 수정 0
- ✅ cokacdir 본체 수정 0
- ✅ replacement_pr_runner 수정 0

### 3.2 4 추가 acceptance
- ✅ policy/spec/schema/fixture/test 기반 activation gate 만 구현
- ✅ activation decision packet 에 회장 최종 승인 필요 필드 포함
- ✅ forbidden 15종 + owner_trigger 4종 + owner_gemini_trigger_router 3종 + ci_watch_handoff_runner 3종 무수정 (task-2642 merge 후 stack 그대로)
- ✅ ANU 직접 코드 구현 0 (dev bot 위임)

---

## 4. CALLBACK_REGISTRATION_STATUS_SNAPSHOT_MISMATCH regression (★ 산출물 #4)

회장 verbatim 후속 hardening candidate 3종 (H1/H2/H3) 모두 regression 으로 검증:

### 4.1 H1 — registry snapshot timing 문제
- fixture: registrar.register() ts vs envelope.write() ts 시간 차 시뮬
- 검증: register-after-commit barrier 도입 시 mismatch 0
- envelope build 전 `registrar.await_committed()` 호출 강제

### 4.2 H2 — callback registrar emit 순서 문제 (★ task-2635+1 envelope drift 동일 root cause 가능성 ↑)
- fixture: register-then-emit vs emit-then-register 두 패턴 비교
- 검증: register-after-update 패턴 (task-2635+1 정정 적용) 정합
- registrar.update_status() 호출 후에만 envelope serialization 허용

### 4.3 H3 — result.json 작성 시점 문제
- fixture: result.json snapshot ts vs envelope ts vs registrar status update ts 3개 비교
- 검증: result.json 작성 시 registrar live status re-fetch (cache 사용 0)
- atomic transaction (envelope + result.json + registrar status) 묶기

### 4.4 회장 후속 결정
- H1/H2/H3 후보 중 우선 검토 순서 (regression 결과 기반)
- task-2643 merge 후 hardening task 발행 여부

---

## 5. dispatch 전 preflight 보고 (★ 본 spec 작성 후 즉시)

회장 verbatim "**우선 task-2643 spec 초안을 만들고, dispatch 전 preflight 보고하라.**"

### 5.1 본 spec 초안 = 본 파일 (preflight 단계)
### 5.2 sha256 확정 후 → preflight 보고 → 회장 결정 후 dispatch

### 5.3 base 선택
- **option A**: origin/main 0e172435 (현재 · PR #144 merge 직후) — 안전 · 즉시 가능
- **option B**: task-2642 merge 후 origin/main — task-2642 merge 결정 후 진행
- **회장 결정 대기**

### 5.4 담당 봇 후보
- dev6 페룬 (task-2640/2641/2642 연속 작업 · OWNER_TRIGGER + CI_WATCH_HANDOFF stack 경험)
- 대안: dev3 다그다 (resolver 재투입 제한 doctrine 외 신규 영역)

---

## 6. 안전 불변식

- ANU key `c119085addb0f8b7` 단일 출처 유지
- OWNER_GEMINI_TRIGGER_TOKEN 단일 출처
- 신규 `ANU_CI_WATCH_HANDOFF_REAL_PR_ACTIVE` default=False hardcoded
- envelope UTF-8 ≤3900 bytes 유지
- callback prompt ≤2800 bytes 권장
- live cokacdir / gh CLI 실호출 0 (regression mock)
- merge/push/PR/admin override 0
- real auto-merge activation 0
- PR #141 pilot 재시도 혼합 0
- foreign dirty 정리 0
- production service task 혼합 0
- ANU 직접 코드 구현 0 (dev bot 위임)
- expected_files 외부 수정 0
- BLOCKING_SECRET 0
- forbidden 15종 + owner_trigger 4종 + owner_gemini_trigger_router 3종 + ci_watch_handoff_runner 3종 무수정

---

## 7. 자동수렴 정책

- Gemini medium/style/quality + expected_files 내부 → 자동수렴
- 동일 함수 HIGH 반복 시 LOOP_BOUNDARY → 회장 보고
- 회장 보고 트리거: Critical7 / credential expansion / expected_files 밖 / admin override / post-merge smoke fail

---

## 8. 금지 (회장 verbatim 9 항목)

- real auto-merge activation 금지
- PR #141 pilot 금지
- production PR write 금지
- BOT App token 임의 사용 금지
- chair_authorization 발급 금지
- foreign dirty 정리 금지
- finish-task.sh 수정 금지
- cokacdir 본체 수정 금지
- replacement_pr_runner 수정 금지

---

## 9. expected_files (task-2643 범위 · 예상 ~30 file)

신규:
- `memory/specs/system_ci_watch_handoff_real_pr_activation_gate_spec_260523.md` (본 파일 · spec)
- `memory/specs/ci_watch_handoff_real_pr_activation_decision_packet_template_260523.json` (decision packet template · 회장 verbatim signature 필드)
- `utils/ci_watch_handoff_token_scope_contract.py`
- `utils/ci_watch_handoff_disable_flag_contract.py`
- `utils/ci_watch_handoff_activation_decision_packet.py`
- `tests/fixtures/ci_watch_handoff_real_pr_lifecycle/<6 시나리오>/{evidence,expected,PROVENANCE}` (18 file)
- (선택) `tests/fixtures/ci_watch_handoff_real_pr_lifecycle/INDEX.md`
- `tests/regression/test_callback_registration_status_mismatch.py`
- `tests/regression/test_watcher_handoff_terminal_state_real_lifecycle.py`
- `tests/regression/test_real_pr_activation_forbidden_action_guard.py`
- `tests/regression/test_ci_watch_handoff_real_pr_activation_decision_packet.py`

총 ~30 file. **프로덕션 영향**: utils helper 3 + spec/template 2 + fixture 19 + regression 4. **실 PR write / live API call / chair_authorization 발급 = 0.**

---

## 10. post-task_2643 chair decision 3 옵션 (회장 verbatim)

A. **BOT App token 기반 limited real PR activation 승인** (per-PR scope · activation_flag toggle · post-activation smoke 필수)
B. **PR #141 pilot 재시도 승인** (real_merge_executor + 새 chair_authorization 별도)
C. **real auto-merge 는 계속 OFF** (현재 default 유지)

★ 본 task 완료 후 회장 결정 명시 시 위 3 옵션 중 선택.

---

## 11. finalize 프로토콜 (★ BOT App token 부재 — 로컬 한정)

1. base = 회장 결정 (0e172435 또는 task-2642 merge 후)
2. 신규 helper 3 + spec/template 2 + fixture 19 + regression 4 PASS · 기존 baseline 유지 · full new fail 0
3. **로컬 commit 만** (push/PR/merge 금지)
4. ANU normal callback (★ 본 task = activation gate 자체 자기검증 강제):
   - validate_spawn_callback_contract self-check (task-2640 결선 active)
   - envelope 5축 + canonical_root=/home/jay/workspace 명시
   - REGISTERED + schedule_id non-null + DELIVERED + UNCONFIRMED
   - envelope UTF-8 ≤3900 bytes
   - result.json 에 callback prompt UTF-8 byte 수 기록
5. ANU collector key: c119085addb0f8b7
6. executor 시작/종료 ts + 로컬 commit SHA 명기

---

## 12. frozen anchor

- ANCHOR-1: "본 task = CI_WATCH_HANDOFF_RUNNER 를 real PR lifecycle 에 연결하기 전 activation gate 신설 · 코드 runtime 0 · policy/schema/regression 만"
- ANCHOR-2: "회장 verbatim 8 필수 산출물 (activation gate spec + token scope contract + dry-run fixture + status mismatch regression + terminal-state regression + forbidden action regression + rollback/disable contract + activation decision packet)"
- ANCHOR-3: "9 금지 (real auto-merge / PR #141 pilot / production PR write / BOT App token 실사용 / chair_authorization 발급 / foreign dirty / finish-task.sh / cokacdir / replacement_pr_runner)"
- ANCHOR-4: "ACTIVATION_FLAG default=False hardcoded · CHAIR_DECISION_PACKET schema · TOKEN_SCOPE_CONTRACT (single endpoint allowlist + 11 forbidden) · DRYRUN_TO_REAL byte-equal"
- ANCHOR-5: "CALLBACK_REGISTRATION_STATUS_SNAPSHOT_MISMATCH H1/H2/H3 후보 regression 으로 검증 (task-2635+1 register-after-update 패턴 정합 단언)"
- ANCHOR-6: "post-task_2643 chair decision 3 옵션 (A BOT App token limited real PR activation / B PR #141 pilot 재시도 / C real auto-merge OFF 유지)"
- ANCHOR-7: "dispatch 전 preflight 보고 필수 (sha256 + base + 담당 봇 + 회장 결정 대기)"
- ANCHOR-8: "기존 stack 무수정 (forbidden 15 + owner_trigger 4 + owner_gemini_trigger_router 3 + ci_watch_handoff_runner 3 → 25종)"
