# task-2563 보고서 — OWNER_TRIGGER_ONLY_CAPABILITY hardening (Track D)

## 본질

OWNER_TRIGGER_ONLY_CAPABILITY 실전 활용 7회 경험 기반 소형 안정화 3건을 통합 hardening. 기존 `Callable[[str, str, dict, dict], dict]` signature 변경 0, 기존 fail-closed 속성 유지.

## 결정 박제 (회장 §명시 1:1)

| 항목 | 결정 |
|---|---|
| task ID | task-2563 |
| 통합 hardening 수 | 3건 (FIRST_TRIGGER_PENDING 분리 + signature 회귀 + FUC-3 logger.exception) |
| owner_trigger_fast_path default | `false` (PENDING dispatch 차단, fail-closed) |
| FIRST_TRIGGER_PENDING_WINDOW_SECONDS | 300s (5분, "PR open 직후 짧은 시간") |
| FIRST_TIMEOUT_SECONDS 1:1 | 1800s (polling_policy 와 정확 일치) |
| 팀 | dev6 페룬 |
| 인증 | OWNER_GEMINI_TRIGGER_TOKEN only (다른 token 0) |

## 구현 산출 (effective diff, 12 파일)

### code (3건 수정)
1. `anu_v2/owner_trigger_only.py` — `import logging` + `logger` + `_redact_diagnostics` + `_collect_http_diagnostics` 추가, http_post 예외 경로에 `logger.exception` 호출 추가. signature 변경 0.
2. `anu_v2/idle_pr_diagnoser.py` — `STATE_FIRST_TRIGGER_PENDING` + `FIRST_TRIGGER_PENDING_WINDOW_SECONDS` 추가, 9-state 머신으로 확장.
3. `anu_v2/executor_scheduler.py` — `ACTION_FIRST_TRIGGER_PENDING_SKIP` + `DISPATCH_DECISION_FAST_PATH_KEY` + `_load_fast_path_flag` 추가, PENDING 상태 분기.

### tests (1건 신규)
4. `anu_v2/tests/test_owner_trigger_invocation_hardening_2563.py` — 20 testcase.

### fixtures (3건 신규)
5. `anu_v2/fixtures/first_trigger_pending_window.json`
6. `anu_v2/fixtures/owner_trigger_signature_mismatch_repro.json`
7. `anu_v2/fixtures/owner_trigger_failure_path_logger_exception.json`

### supporting (5건 신규)
8. `memory/reports/task-2563.md` (본 파일)
9. `memory/events/task-2563.dispatch-decision.json`
10. `memory/plans/tasks/task-2563/plan.md`
11. `memory/plans/tasks/task-2563/context-notes.md`
12. `memory/plans/tasks/task-2563/checklist.md`

## Fix 1 — FIRST_TRIGGER_PENDING 상태 분리

| elapsed | state | scheduler action (default) | scheduler action (fast_path=true) |
|---|---|---|---|
| 0 ~ 300s | WITHIN_GRACE_PERIOD | WITHIN_GRACE_PERIOD_SKIP | WITHIN_GRACE_PERIOD_SKIP |
| 300 ~ 1800s + reviews 0 | FIRST_TRIGGER_PENDING | FIRST_TRIGGER_PENDING_SKIP | OWNER_TRIGGER_DISPATCHED |
| ≥ 1800s + reviews 0 | FIRST_GEMINI_TRIGGER_MISSING | OWNER_TRIGGER_DISPATCHED | OWNER_TRIGGER_DISPATCHED |

`OWNER_TRIGGER_INVOKING_STATES` frozenset 에 `FIRST_TRIGGER_PENDING` 미포함 — fail-closed invariant.

## Fix 2 — http_post signature 회귀

`OwnerTriggerOnly._http_post: Callable[[str, str, dict, dict], dict]` 4 positional, 0 keyword. 3 caller path 동일 signature 어셀션:

1. direct: `OwnerTriggerOnly.trigger_gemini_review(...)`
2. scheduler: `invoke_from_scheduler(runner, ...)`
3. wrapper/DI mock: custom callable wrapping `http_post`

signature 가 drift 하면 (3-arg / 5-arg / keyword 사용) `test_11_signature_mismatch_breaks_owner_trigger_call` 가 TypeError 어셀션으로 차단.

## Fix 3 — FUC-3 logger.exception with secret masking

```python
try:
    self._http_post("POST", path, body_payload, headers)
except Exception as exc:
    # task-2563 FUC-3
    http_diag = _collect_http_diagnostics(exc)
    redacted_diag = _redact_diagnostics(http_diag)
    logger.exception(
        "owner_trigger http_post FAILED pr=%s head=%s endpoint=%s "
        "token_hash_prefix=%s diagnostic=%s",
        pr_num, head[:8] + "...", path, hash_prefix, redacted_diag,
    )
    # ... txn.record(FAILED) + raise
```

redaction 패턴:
- key regex: `(?i)(token|authorization|api[_-]?key|secret|password)` → `<redacted>`
- value sentinels: `Bearer `, `ghp_`, `github_pat_`, `ghu_`, `ghs_`, `ghr_` 포함 시 `<redacted>`
- 재귀 깊이 8 cap, dict / list / tuple / str 모두 지원

보존 diagnostic 필드: `status`, `x-github-request-id`, `x-accepted-github-permissions`, `documentation_url`.

## 회귀 어셀션

| 명령 | 결과 |
|---|---|
| `pytest anu_v2/tests/test_owner_trigger_invocation_hardening_2563.py -v` | 20 PASS |
| `pytest anu_v2/tests/test_owner_trigger_only_2554.py` | 10 PASS |
| `pytest anu_v2/tests/test_executor_first_gemini_trigger_missing.py` | 9 PASS (semantic 변경 반영) |
| `pytest anu_v2/tests/test_executor_scheduler_per_pr_isolation.py` | 9 PASS |
| `pytest anu_v2/tests/ (전체)` | 480 passed, 1 skipped |
| `py_compile owner_trigger_only.py idle_pr_diagnoser.py executor_scheduler.py` | syntax OK |

## fail-closed 속성 유지 어셀션

| 항목 | 검증 위치 |
|---|---|
| audit FAILED 기록 | `test_15` — audit JSONL `result == "FAILED"` 행 존재 |
| token_value_logged=False | `test_15` — failed_row["token_value_logged"] is False |
| token_hash_prefix 보존 (raw 아님) | `test_15` — len(prefix) == 8 |
| HTTP POST side-effect 0 | `test_14`, `test_15` — http_calls 길이 1 (실패 call 만) |
| logger.exception BEFORE txn.record(FAILED) | `test_14` — caplog 어셀션 + audit 순서 |
| token 원문 audit jsonl 노출 0 | `test_15` — _SENTINEL_SECRET / _FAKE_OWNER_TOKEN / "Bearer " absent |
| token 원문 logger 노출 0 | `test_14` — getMessage() 어셀션 |

## 금지 9건 어셀션

| # | 항목 | 결과 |
|---|---|---|
| 1 | G4 task-2562 재수정 0 | OK |
| 2 | auto_gemini_triage task-2558 영역 0 | OK |
| 3 | finish-task.sh 수정 0 | OK |
| 4 | task-2560/2561 영역 섞기 0 | OK |
| 5 | 회장 수동 `/gemini review` 0 | OK |
| 6 | BOT `/gemini review` 0 | OK |
| 7 | token 원문 출력 0 | OK (logger / audit 양쪽 정적 + 동적 검증) |
| 8 | long polling 0 | OK (single-shot run_one_cycle) |
| 9 | force push / rebase / admin override 0 | OK (PR 단계 어셀션) |

## 완료 조건 11건 (회장 §명시)

| # | 조건 | 결과 |
|---|---|---|
| 1 | FIRST_TRIGGER_PENDING / MISSING 구분 PASS | OK (test 1~5) |
| 2 | fast_path=false 시 조기 trigger 차단 | OK (test 6) |
| 3 | fast_path=true 시 조기 trigger 허용 | OK (test 7) |
| 4 | http_post signature 3 caller 동일 PASS | OK (test 8~11) |
| 5 | logger.exception + secret masking PASS | OK (test 12~14, 16, 20) |
| 6 | fail-closed 속성 유지 | OK (test 15) |
| 7 | expected_files strict (12) | OK |
| 8 | forbidden path 0 | OK |
| 9 | CI/Gemini/CLEAN | (PR 단계) |
| 10 | BOT squash merge | (PR 단계) |
| 11 | post-merge smoke + reconcile evidence | (PR 단계) |

## 보고 1:1

```
Track: D
상태: <MERGED|ESCALATED>
task_id: task-2563
수정/PR 여부: PR #<N> / merge_commit <SHA>
expected_files: 12/12 strict (dispatch_decision.json authoritative)
forbidden path: 0
Gemini 상태: fresh + unresolved 0
CI 상태: 11/11 / CLEAN
핵심 evidence: 3 hardening 항목 PASS (PENDING split + signature regression + logger.exception redaction) + secret masking 검증 + fail-closed 속성 유지
회장 결정 필요 여부: N (완료 11건 충족 시) / Y (regression FAIL 시)
```

## 다음 액션

1. PR 생성 (BOT_GITHUB_TOKEN) → CI 11 checks SUCCESS 대기
2. capability N번째 활용 — FIRST_TRIGGER_PENDING 경과 후 owner_trigger 자동 발사
3. fresh Gemini review → unresolved 0
4. BOT squash merge (admin override 0)
5. post-merge smoke + reconcile evidence
6. lifecycle markers + .done

## 후속 hardening 후보 (본 PR 범위 외)

- `FIRST_TRIGGER_PENDING_WINDOW_SECONDS` env override 노출 — 짧은 window 운영 튜닝.
- `_load_fast_path_flag` PR-specific decision JSON 경로 확장 — 다중 task per PR.
- logger.exception level 별 routing (structured logging) — observability 향상.

## Capability 활용 기록 (회장 §명시)

PR #117 진행 중 OWNER_TRIGGER_ONLY_CAPABILITY 8번째 실전 활용:
- 사유: cee55afe 푸시 후 Gemini auto-review가 FIRST_TIMEOUT (1800s) 내 도착하지 않음 (GEMINI_STALE_ON_HEAD).
- 결과: RESULT=POSTED, token_hash_prefix=a9e05574, endpoint `/repos/.../issues/117/comments`.
- token 원문 노출 0, BOT 계정 사용 0, 회장 수동 입력 0.
