# cron-targeting-spec — 독립 bot task cron 타겟팅 정책 (task-2526)

> 본 spec은 **2026-05-10 PR #74 재위임 오류 사고**(독립 bot task cron에 `--session`이 잘못
> 붙어 봇이 아니라 아누 자기 세션이 resume된 사고) 재발방지를 위한 운영 룰이다.
>
> 회장 §명시: 동일 사고가 다시 발생하면 안 된다. 잘못된 조합은 **실행 전에** 차단한다.

## 사고 회고 (read-only fact)

| 항목 | 값 |
| --- | --- |
| 일시 | 2026-05-10 |
| 잘못된 cron | `5C9995CCB` |
| 위임 의도 | dev3-team 다그다 (`key=0b94683120a691cf`) |
| 잘못 동반된 옵션 | `--session 5eee7634-b0be-4594-b84e-311ae64e557b` |
| 결과 | PID `448820` = 아누 자기 세션 resume → 회장 kill |
| 회장 조치 | `--session` 제거 후 cron `74325894`로 재발사 → PID `455634` 다그다 정상 trigger |
| 부수 사건 | 동시점 PID `455786`도 아누 세션 resume했으나 이건 별개 회장 메시지 처리이지 cron 결과 X |

## 1. 핵심 정책 (회장 §1)

1. **독립 bot task cron에 `--session` 사용 금지.**
2. `--session` 허용은 **오직 read-only followup만**.
3. `safe_cron_dispatch` wrapper 사용을 강제. 직접 cokacdir cron 명령 조립 금지.
4. wrapper가 잘못된 조합을 **실행 전에** 차단. BLOCK 사유는 `cron-targeting-audit.jsonl`에 기록.

## 2. 의사결정 매트릭스 (회장 §2 — 회귀 6 박제)

| # | task_kind | bot_key | --session | gh pr merge + GH_TOKEN injection | 결과 |
| --- | --- | --- | --- | --- | --- |
| 1 | `independent_task` | dagda key | 있음 | n/a | **BLOCK** `independent_task_must_not_resume_anu_session` |
| 2 | `merge_task` | dagda key | 있음 | n/a | **BLOCK** `merge_task_must_not_inherit_anu_session` |
| 3 | `followup_readonly` | (없음 가능) | 있음 (chair chat) | n/a | **ALLOW** (예외 1건) |
| 4 | `bot_task` | dagda key | 없음 | bot token 명시 주입 | **ALLOW** |
| 5 | `merge_task` 또는 `bot_task` | **없음** | n/a | n/a | **BLOCK** `bot_key_missing_for_bot_task` |
| 6 | `merge_task` / `bot_task` | dagda key | 없음 | `gh pr merge` 동반 + `GH_TOKEN=$BOT_GITHUB_TOKEN` 누락 | **BLOCK** `owner_pat_fallback_path_detected` |

(추가 보호) target bot의 session owner와 chat owner가 불일치하면 → **BLOCK** `target_bot_session_owner_mismatch`.

## 3. wrapper 사양 (회장 §3 — `scripts/safe_cron_dispatch.py`)

```python
def safe_cron_dispatch(
    prompt: str,
    schedule: str,
    chat: str,
    target_bot_key: str | None,
    task_kind: Literal["independent_task", "followup_readonly",
                       "merge_task", "bot_task", "human_response"],
    session_id: str | None = None,
    *,
    once: bool = False,
    audit_path: Path | None = None,
    cli_path: str = "/usr/local/bin/cokacdir",
) -> CronDispatchResult: ...
```

- **부수효과 없음**: subprocess 실행 X, chairman 메시지 송신 X, audit JSONL append O.
- **결과 객체**: `status` (`ALLOWED` | `BLOCKED`), `blocked_reason`, `audit_record`, `command_argv`, `chairman_notice`.
- **ALLOWED 경로**: caller가 `command_argv`를 직접 subprocess.call() 하거나, CLI `--apply` 옵션으로 wrapper가 실행 (선택).
- **BLOCKED 경로**: `command_argv == ()`, `chairman_notice`에 `CRON_TARGETING_GUARD_BLOCKED — task_kind=… reason=… target=…` 짧은 텍스트.

### 3.1 BLOCK 시 회장 보고 정책

- `CRON_TARGETING_GUARD_BLOCKED`는 **Critical 7종 enum이 아니다**. 별도 운영 신호로 audit JSONL에 기록.
- chairman chat 송신 여부는 **호출자 결정** (wrapper가 자동 송신하지 않음).
- 보고 시 길이 200자 미만, Critical 7종 분류명 사용 금지.

## 4. audit JSONL 스키마 (회장 §4)

`memory/orchestration-audit/cron-targeting-audit.jsonl`에 line 단위 append.

**필수 8 필드** (`REQUIRED_AUDIT_FIELDS_2526`):
1. `cron_id` — wrapper 시점에는 None (cron이 아직 발사되지 않음)
2. `target_bot` — `chat=<id>/key=<hash:prefix…>` 형태
3. `target_bot_key_hash` — `sha256(bot_key)`의 첫 16 hex
4. `session_id_present` — bool
5. `session_id_allowed` — bool (followup_readonly + 같은 chat anu session에서만 True)
6. `task_kind` — 5 enum
7. `actor_expected` — `bot_session` | `anu_session`
8. `actor_actual_if_known` — wrapper에서는 None (사후 식별용)

**보강 2 필드**:
- `command_preview_sanitized` — `--key`/`--session`/token env var이 마스킹된 cokacdir 명령 미리보기, prompt 본문은 80자로 절단
- `blocked_reason` — `BlockedReason.ALL` 중 하나 또는 None

**보안 보강**:
- 정적 검사 `ensure_no_raw_secrets()` — JSONL append 직전 호출, raw token / raw key / 완전 형태 UUID 패턴이 1건이라도 발견되면 ValueError로 중단.
- `ts`는 ISO8601 UTC (예: `2026-05-10T00:00:00Z`).

## 5. kill / recover 정책 (회장 §5 — `utils/cron_targeting_audit.py`)

### 5.1 misroute 감지

```python
detect_misrouted_session(pid, cmdline_reader=...) -> MisrouteReport
```

- `/proc/<pid>/cmdline`에서 `cokacdir --cron` + `--session <uuid>`가 모두 발견되면 → `suspected_misroute=True`.
- raw session UUID는 `prefix…(redacted)` 형태로 마스킹.
- cmdline 미리보기는 `sanitize_command_preview`로 마스킹.

### 5.2 soft kill (dry-run 우선)

```python
soft_kill_misrouted(pid, dry_run=True, killer=os.kill, audit_path=...)
```

- 기본 `dry_run=True` — 신호 송신하지 않음.
- `dry_run=False` 시 **SIGTERM만** 송신. SIGKILL 절대 사용 금지 (회장 §5).
- 모든 호출은 audit JSONL에 `kind="soft_kill_audit_2526"` line으로 기록.

### 5.3 evidence-based recover

```python
evidence_based_recover(task_id, signals: dict | None) -> RecoveryPlan
```

7 signal 검사 (`_RECOVERY_SIGNALS`):
1. `worktree_diff`
2. `worktree_untracked`
3. `branch_unpushed_commits`
4. `remote_branch_exists`
5. `open_pr_for_task`
6. `ci_run_for_branch`
7. `audit_jsonl_evidence`

분류:
- 모두 `False` → `clean_abort` (변경 없음 → 안전 abort 후 `safe_cron_dispatch`로 올바른 bot key + no `--session`로 재위임)
- 1개 이상 `True` → `contaminated_execution` (force-push / rebase / 자동 redispatch 금지, 회장 보고 후 수동 검증)
- `signals=None` → `no_evidence` (먼저 7 signal 수집)

회장 §5 정합: `feedback_bot_no_response_not_dead_260509.md`의 "no response ≠ dead" 원칙 — 변경 없음을 확인하기 전에 재발사 금지.

## 6. 금지 행위 (회장 §명시 — 본 task 기준)

- ❌ `dispatch.py` 본체 수정
- ❌ cokacdir CLI 본체 수정 (외부 도구)
- ❌ 직접 cokacdir cron 명령 조립 (외부 호출자도 본 wrapper만 사용)
- ❌ token / raw key / session secret 원문 기록
- ❌ admin override / `owner_pat` fallback 정당화
- ❌ force push / rebase / manual `.done`
- ❌ task-2523 코드 추가 수정
- ❌ task.md commit 포함
- ❌ task-2524 영역 (`tools/observability/`) 수정 (parallel 충돌 방지)
- ❌ Critical 7종 외 회장 보고 (`CRON_TARGETING_GUARD_BLOCKED`는 운영 신호이지 Critical 분류가 아님)
- ❌ expected_files 외 수정 (정확히 4 파일)

## 7. 회귀 fixture 참조 (회장 §6 정합)

`tests/regression/test_cron_session_safety_guard_2526.py`에 다음 박제:
- `test_regression_1_independent_task_with_session_blocks` — 매트릭스 #1
- `test_regression_2_merge_task_with_session_blocks` — 매트릭스 #2
- `test_regression_3_followup_readonly_with_session_allows` — 매트릭스 #3
- `test_regression_4_bot_task_with_key_no_session_allows` — 매트릭스 #4
- `test_regression_5_bot_task_without_key_blocks` — 매트릭스 #5
- `test_regression_6_owner_pat_fallback_blocks` — 매트릭스 #6
- `test_pr74_misroute_incident_replay_blocks_5C9995CCB` — 본 사건 replay
- `test_pr74_redispatch_cron_74325894_allows` — 재발사 정상 case

## 8. 의존성 / 위치

- 의존: `task-2523.merged` (PR #74, mergeCommit `94344526f1ae260218d6c1c46ea92ec4c3667f9c`, mergedAt `2026-05-10T00:53:46Z`, mergedBy `app/jeon-jonghyuk-taskctl-bot`).
- expected_files (정확히 4):
  - `scripts/safe_cron_dispatch.py`
  - `tests/regression/test_cron_session_safety_guard_2526.py`
  - `memory/specs/cron-targeting-spec.md`
  - `utils/cron_targeting_audit.py`
- audit JSONL: `memory/orchestration-audit/cron-targeting-audit.jsonl` (production), 테스트는 `tmp_path`에 격리.
