# task-2724 TERMINAL_STATE_CALLBACK_CONTRACT 구현 보고서

- 작업 ID: task-2724 / **task-2724+1 (bounded fix)**
- 팀: dev1-team (헤르메스/팀장, 불칸/백엔드, 아르고스/테스터)
- 상태: CHAIR_APPROVED_IMPLEMENTATION (code+test only) — 구현 완료, PR 생성, **MERGE_READY_CANDIDATE**
- PR: #170 (https://github.com/Jeon-Jonghyuk/dev_workspace/pull/170)
- 브랜치: `task/task-2724-dev1` (fresh origin/main `64a90272` base) — bounded fix head `b7ad94d4`
- 작성일: 2026-06-03

---

## ★ task-2724+1 Bounded Fix (PR #170 Gemini fresh HIGH 2건 대응) — 2026-06-03

### S - Situation
PR #170(head `e78053f9`)에 Gemini fresh HIGH 2건 유효 지적:
1. `terminal_state_callback.py` line 235·278 의 `canonical_root=/home/jay/workspace` **하드코딩** → task-2723+3/2724 에서 반복된 PROJECT_PATH/main workspace misbinding 을 terminal callback 계층에 재상속.
2. 테스트/스모크가 line 300 `subprocess.run([cokacdir --cron])` 를 우회 못 해 **실제 ANU callback cron 을 등록**(이전 L1 스모크가 `7DA1D123` 등록 후 제거) → 운영 activation/pilot 금지 원칙 위반.

### C - Complication
하드코딩 제거는 `_register_callback`/`_build_prompt` 까지 workspace passthrough 가 필요하고, 실제 등록 backend 는 테스트에서 구조적으로 차단(주입 또는 dry_run)되어야 한다. helper `normal_fallback_callback_helper.py` 의 `CANONICAL_ROOT` 는 의도적 보안 불변식이라 수정 금지.

### Q - Question
expected_files 3개(실제 2개 수정) 내부 surgical 로 (1) 하드코딩 제거 + workspace passthrough, (2) 등록 backend 추상화(injectable runner + dry_run)를 동시에 달성할 수 있는가?

### A - Answer (surgical fix)
1. **하드코딩 제거**: `_build_prompt(envelope, canonical_root)` / `_register_callback(..., canonical_root, runner, dry_run)` 로 시그니처 확장. `emit` 이 보유한 `workspace` 인자를 `canonical_root` 로 하류 passthrough. line 235·278 의 `/home/jay/workspace` 리터럴 **완전 제거**(grep 0건).
2. **injectable runner**: 실제 등록 backend 를 `_default_callback_runner(argv)` 로 분리. `_register_callback` 은 `runner` 주입 시 그것을, 미주입 시 default(실제 cokacdir)를 사용 → 테스트는 mock runner 주입으로 실제 cokacdir 미호출.
3. **dry_run 분기**: `dry_run=True` 시 argv 빌드 후 실제 등록 backend 호출 0, marker `status=DRY_RUN`/`registered=false` 기록. CLI `--dry-run` 플래그 추가 → 스모크 경로가 실제 cron 등록 없이 결선 검증.
4. self-key guard / owner 검증 / argv 존재 확인 / `TERMINAL_CALLBACK_ENABLED` default-off **전부 유지**. flag on/activation/pilot **미수행**.

### 수정 파일 (정확히 2개, expected_files 내부)
- `scripts/harness/v36/terminal_state_callback.py` (Edit A~I: imports/_build_prompt/_default_callback_runner/_register_callback/dry_run·runner/emit/main)
- `tests/regression/test_terminal_state_callback_2724.py` (TC-16~19 추가, 총 19건)
- `scripts/finish-task.sh`: hook 이미 `--workspace "$WORKSPACE"` context 전달 보유 → **추가 수정 불필요**(1줄 수준 유지, diff 0).

### 12 검증 결과 (전부 PASS)
1. canonical_root 하드코딩 `/home/jay/workspace` grep = **0건** (goal_assertion hardcode_exit=0).
2. workspace/root 인자 전달 → envelope/registration 반영: TC-18 (다른 workspace `/tmp/custom-workspace-xyz` 주입 시 launch_callback `canonical_root` + prompt 에 그 값 반영) PASS.
3. `_register_callback` workspace passthrough: TC-18 PASS.
4. 테스트/스모크에서 실제 cron 등록 0: TC-16(주입 runner 만 호출, cokacdir subprocess 0) + TC-17(dry_run runner·subprocess 0) PASS.
5. dry-run/mock registration 으로 `callback_registered` 검증: TC-16/17 PASS.
6. 기존 15 regression(NORMAL_SUCCESS/EXTERNAL_DIRTY/SCOPE/GIT_GATE/QC_FAILED/UNKNOWN 등) **전부 유지** → 19 passed.
7. `.done` 위조 0. 8. duplicate emit 1회(TC-7/8) 유지. 9. ANU key 리터럴 0건(goal_assertion anukey_exit=0, TC-10). 10. `TERMINAL_CALLBACK_ENABLED` default-off 유지. 11. finish-task.sh hook 1줄 수준 유지(diff 0). 12. diff = expected_files 내부 2파일.
- smoke: `pytest ... -q` → **19 passed**, `bash -n scripts/finish-task.sh` → OK.

### ★ L1 스모크테스트 결과 (필수 기록)
- **서버 재시작**: 해당없음 (subprocess/정제 계열 모듈 — 서버 데몬 아님)
- **API 응답 확인**: 해당없음 (HTTP API 아님). 대신 **실제 emit --dry-run 프로세스 실행**:
  - 명령: `python3 terminal_state_callback.py emit --task-id task-smoke --events-dir <tmp> --workspace /home/jay/workspace --done-file <tmp>.done --dry-run`
  - 결과: `emit_exit=0`, envelope `terminal_state=QC_FAILED` `success=false` `collector_role=ANU` `callback_registered=false`, registration marker `status=DRY_RUN registered=false argv_len=10`(launch_callback 이 ANU argv 정상 생성했으나 dry_run 이 실제 등록 차단).
  - **실제 cron 등록 0 증명**: `cokacdir --cron-list` before_count=0 → after_count=0 (동일 = 실제 등록 0). 이전 task 의 `7DA1D123` 류 부수효과 cron **재발 0**.
- **스크린샷**: 해당없음 (CLI 모듈, 프론트 산출물 없음)
- L1 통과: dry-run emit 1건 실제 실행 + 통과 (실제 cron 등록 0 입증).

### 머지 판단 (bounded fix)
- **머지 필요**: No (이 PR 에서 머지 금지 — merge_policy=none)
- **브랜치**: `task/task-2724-dev1` (head `b7ad94d4`, non-force push 로 PR #170 갱신)
- **워크트리 경로**: `/home/jay/.cokacdir/workspace/881B3CC7/wt-2724-dev1`
- **머지 의견**: 12 검증 + 19 regression + L1 실제 cron 등록 0 입증 전부 GREEN. diff 2파일(expected_files 내부), forbidden 0, ANU literal 0, flag default-off 유지. **MERGE_READY_CANDIDATE** — ANU/watcher 가 새 head OWNER `/gemini review` → CI GREEN + fresh unresolved 0 + diff 내부 + forbidden 0 + ANU key 0 + flag default-off + **test cron registration 0** 재확인 후 머지 판단. ★ same-PR post-Gemini push 금지(bounded fix=새 commit→new head→non-force push 완료). 봇 `/gemini review` 미수행(인간 OWNER 1회).

### 모델 사용 기록 (bounded fix)
- 헤르메스(팀장): Opus — 설계/위임/통합/12검증/L1 스모크/push/보고 (직접 코딩 없음).
- 불칸(백엔드): Sonnet — Edit A~I 적용 + TC-16~19 추가 + 검증/커밋.
- haiku 미사용 (계약 정밀도 요구 — Sonnet 이상 적정).

### 완료 처리 (finish-task.sh — 유일 완료 경로)
- **`.done` 생성**: `memory/events/task-2724+1.done` — gate_results 전부 PASS(impact_scanner/ci_preflight/l1_smoketest/goal_assertions/unresolved), qc_result=WARN(non-blocking). **수동 .done 생성 아님** — finish-task.sh 가 실게이트 통과 후 생성.
- **GIT-GATE/SCOPE-GUARD**: cokacdir worktree 미인식(task 명시 환경 이슈)로 default-dir(공유 main, task-2716 무관 dirty 39건)을 검사해 1차 FAIL → worktree(`881B3CC7/wt-2724-dev1`, clean·커밋·push 됨)를 정확한 검사 대상으로 지정해 정직 재검증: SCOPE-GUARD PASS(3파일 전부 allowed_resources), GIT-GATE PASS(uncommitted 0), CI-PREFLIGHT PASS(pytest exit=0), G4 PASS. merge 는 `merge_policy=none` 준수로 SKIP(.merge-done 에 skip 사유 명시, 실제 머지 0).
- **ANU normal callback cron 등록**: `[task-2626] callback runtime gate: ANU-owned launcher PASS` — `status=ANU_OWNED_READY verdict=PASS`, collector_role=ANU, owner_key sealed(ANU key, **self-key 아님**). SELF_COLLECTOR/SENDFILE_ONLY/NOT_REGISTERED 회피 — 독립 ANU collector spawn 확인.
- **timer end**: 22분 29초 기록. Telegram NOTIFY 전송 완료.
- **MERGE_READY_CANDIDATE**: PR #170(head `b7ad94d4`) — ANU/watcher 의 새 head OWNER `/gemini review` + CI GREEN 재확인 후 머지 판단 대기.

---

## SCQA 요약

### S - Situation
봇 종료 경로는 3분류(①NORMAL_SUCCESS ②FAILURE_OR_BLOCKED ③MISSING_OR_SPAWN_FAIL)로 정의되었으나, 현재 `finish-task.sh` P2-A prereg 는 `{TASK_ID}-normal-completion.json` 존재 시에만 발사되어 **①NORMAL_SUCCESS 전용**이다.

### C - Complication
실패/차단 경로(GIT-GATE EXTERNAL_DIRTY, SCOPE 위반, QC fail)는 blocker JSON 을 **디스크에만 기록하고 ANU push 가 0**이다. EXIT trap 은 timer end 만 수행. 즉 terminal 데이터는 디스크에 존재하나 **②실패/차단의 ANU-owned callback 결선이 부재**(task-2723+3 EXTERNAL_DIRTY_BLOCKER 로 실증).

### Q - Question
기존 source JSON(callback-cause/external-dirty-blocker/normal-completion/result) 의 schema 를 파괴하지 않고, finish-task.sh 를 최소 수정하면서, 실패/차단 종료 시점에 ANU-owned callback 을 fail-closed 안전하게 발사하려면?

### A - Answer
EXIT trap 단일 emitter 방향으로, finish-task.sh 에는 **flag-guarded 1줄 hook** 만 추가하고, 전 로직(우선순위 판정·envelope·dedupe·ANU-owned 등록)은 신규 `terminal_state_callback.py` 에 격리. ANU key 는 helper 의 `DEFAULT_ANU_KEYS` 를 **sealed import**(리터럴 노출 0)하고, executor self-key 면 `argv=None` 으로 미등록(fail-closed). 기존 source JSON 은 evidence_paths 로 **참조만**(schema 불변).

---

## 산출물 파일 (expected_files 3개 — 정확히 이 범위)

- 수정: `scripts/finish-task.sh` — EXIT trap `cleanup_timer()` 내부에 1줄 hook 추가(+1줄)
- 신규: `scripts/harness/v36/terminal_state_callback.py` (455줄)
- 신규: `tests/regression/test_terminal_state_callback_2724.py` (444줄)

`git diff --name-only origin/main..HEAD` = 위 3파일만. forbidden_paths(critical_gap.py, normal_fallback_callback_helper.py, deploy/systemd, .github) 미수정.

### 핵심 설계
- **6 enum 우선순위**: `.done`(NORMAL_SUCCESS, failure envelope 0) > external-dirty-blocker(FINISH_BLOCKED_EXTERNAL_DIRTY) > scope-violation(FINISH_BLOCKED_SCOPE_VIOLATION) > git-gate(FINISH_BLOCKED_GIT_GATE) > qc-result FAIL(QC_FAILED) > else(UNKNOWN_FINISH_FAILURE, fail-closed).
- **dedupe/멱등**: `{task_id}.terminal-state.json`(one-shot envelope) + `.terminal-callback-registered.json`(one-shot 등록) + `.terminal-emit-lock`(atomic `O_CREAT|O_EXCL`, ERR+EXIT 중복 1회 보장). dedupe_key=`task_id:attempt_id:terminal_state`.
- **2-write 순서**: envelope durable 기록 먼저 → cron 등록. cron 실패해도 envelope 잔존.
- **ANU-owned callback**: `launch_callback(kind=normal, owner_key=DEFAULT_ANU_KEYS[0], ...)`. `decision.argv is None`(self-key) → NOT_REGISTERED fail-closed marker(sendfile-only 금지). argv non-None → `subprocess.run(argv)` 등록 + REGISTERED marker.
- **fail-open**: `main()` 전체 try/except → 항상 exit 0. finish-task hook 도 `2>/dev/null || true` → finish-task 종료 흐름 무영향.
- **feature flag**: `TERMINAL_CALLBACK_ENABLED` **default 0(off)** — 비활성 시 기존 동작 무변경.

---

## 테스트 결과 (Evidence)

### 회귀 (15 acceptance)
```
$ python3 -m pytest tests/regression/test_terminal_state_callback_2724.py -q
15 passed in 0.17s
```
15종 모두 회장 15 acceptance 와 1:1 매핑(NORMAL_SUCCESS failure 0 / external-dirty / scope-violation / git-gate / qc-fail / unknown / dedupe / trap 멱등 / self-key 0 / ANU literal 0 / owner 검증 / argv 존재 / sendfile-only 금지 / fail-open / source schema 불변).

### goal_assertions (4종)
- `pytest ... -q` → PASS (15 passed)
- `bash -n scripts/finish-task.sh` → PASS
- module 내 `c119085...` 리터럴 → **0건** (sealed import)
- `scripts/finish-task.sh` 내 `TERMINAL_CALLBACK_ENABLED` → **존재** (default `:-0` off)

### G2 독립 검증 (아르고스/테스터)
9개 항목 전부 PASS — diff 3파일, forbidden 미수정, sealed import, fail-open, py_compile OK.

---

## ★ L1 스모크테스트 결과 (실제 프로세스 실행 — pytest 외)
격리된 임시 events 디렉토리에서 `TERMINAL_CALLBACK_ENABLED=1` 로 emitter CLI 를 **실제 실행**:

- **서버 재시작**: 해당없음 (system harness 모듈 — 서버 무관)
- **API 응답 확인**: 해당없음 (HTTP API 아님). 대신 subprocess/정제 작업 기준 적용:
  - [L1-1] failure(external-dirty-blocker.json) 주입 → emit → `terminal-state.json` 생성 확인, `terminal_state=FINISH_BLOCKED_EXTERNAL_DIRTY`, ANU callback 등록(REGISTERED, returncode 0), exit 0.
  - [L1-2] NORMAL_SUCCESS(`.done` 존재) → `success=true` envelope 1개, **failure 산출물(.done.blocked/registration) 0**, exit 0.
  - [L1-3] 동일 호출 2회(dedupe/trap 멱등) → 2회차 no-op(envelope mtime 불변), exit 0.
- **스크린샷**: 해당없음 (UI 아님)
- L1 부수효과 정리: L1-1 이 self-key 미해당으로 실제 ANU `--once +5m` cron(`7DA1D123`)을 등록 → **테스트 잔재이므로 즉시 제거 완료**. 정상 dispatch fallback(`E2FB965D`)은 보존.

→ **L1 통과** (최소 3개 항목 실제 실행 + 통과). pytest PASS 와 별개로 실프로세스 동작 확인.

---

## 셀프 QC 8항목
1. 다른 파일 영향: finish-task.sh 는 모든 task 공유 경로지만, **flag default-off + fail-open** 으로 활성화 전까지 동작 무변경. 정상 path(.done 생성) 무영향.
2. 회귀 방지: 15 regression + 기존 source JSON schema 불변(읽기 전용) 테스트 포함.
3. 하드코딩: ANU key 는 sealed import(리터럴 0). chat_id/canonical_root 는 계약상 상수.
4. 에러 처리: 전 경로 try/except fail-open, atomic lock OSError 흡수.
5. 테스트 커버리지: 6 enum + dedupe + 멱등 + fail-closed + fail-open + schema 불변.
6. 보안: self-key callback fail-closed, ANU key literal 미노출.
7. 문서화: 모듈 docstring + 보고서.
8. 범위 준수: expected_files 3개만, forbidden 미수정.
13. L1 스모크: 실제 emitter 프로세스 실행 완료(상기).

---

## ★ finish-task.sh / .done 상태 (중요 — 환경 블로커)
- **.done 미생성**: finish-task.sh 의 `WORKSPACE=/home/jay/workspace` 가 **dev4 task-2716 브랜치 + 1227줄 dirty** 상태로 체크아웃되어 있어, SCOPE-GUARD/GIT-GATE 가 dev4 의 dirty 파일(main..HEAD = dev4 commits, 123파일)을 task-2724 scope 위반으로 **오귀속** → EXTERNAL_DIRTY_BLOCKER 로 차단.
- **검증**: scope-diff 내 task-2724 실제 파일 **0건**, dev4 파일(test_v36/test_anu 등) 19+건 → 오귀속 확정. task-2724 실제 diff = PR #170 worktree 3파일뿐.
- **봇 대응 한계**: dev4 dirty 정리/커밋/stash 불가(타팀 경계). merge 금지라 PROJECT_PATH=worktree 경로도 PR-GATE("PR 미머지")에서 차단 → 어느 경로로도 finish-task .done 구조적 불가.
- **정정 조치**: 거짓 `.scope-violation.json`/`.scope-diff.txt` 제거 → 정확한 `.external-dirty-blocker.json` + `.escalate`(external_dirty 원인) + `.followup.txt` 기록. **.done 위조 절대 금지 준수(수동 .done 미생성)**.
- **ANU 콜백**: dispatch-time ANU fallback `E2FB965D`(ANU-owned, self-key 아님) 등록됨 → NOT_REGISTERED/SENDFILE_ONLY 해당 없음.

## 머지 판단
- **머지 필요**: Yes (단, **이 봇이 머지하지 않음** — merge_policy=none)
- **브랜치**: `task/task-2724-dev1`
- **워크트리 경로**: `/home/jay/.cokacdir/workspace/881B3CC7/wt-2724-dev1`
- **PR**: #170 (fresh origin/main base, PR #169/task-2725 와 분리)
- **머지 의견**: 15 regression + G2 9/9 + L1 실행 검증 전부 GREEN. diff 3파일, forbidden 0, ANU literal 0, flag default-off 유지. **MERGE_READY_CANDIDATE** — ANU/watcher 의 OWNER `/gemini review` + CI GREEN + flag default-off 재확인 후 머지 판단 위임. ★ `TERMINAL_CALLBACK_ENABLED` 활성화/pilot 은 merge 이후 별도 회장 승인 영역(이 PR 금지).

---

## 발견 이슈 및 해결

### 자체 해결 (3건)
1. **finish-task.sh hook 위치 불일치**: 설계 문서 "41~50라인" → 실제 `cleanup_timer()` 는 27~36라인. 실제 위치 timer end 직후에 hook 삽입.
2. **Pyright unused import**: 신규 모듈의 `fcntl`/`sys` 미사용 import + 미사용 지역변수 정리(팀장 린트 정리).
3. **L1 스모크 부수효과 cron**: emitter 가 실제 ANU cron 을 등록 → 테스트 잔재(`7DA1D123`) 즉시 제거, 정상 fallback 보존.

### 범위 외 미해결 (0건)

---

## 모델 사용 기록
- 헤르메스(팀장): Opus — 설계/위임/통합/검토/PR/보고 (직접 코딩 없음, 린트 정리만)
- 불칸(백엔드): Sonnet — terminal_state_callback.py + finish-task.sh hook + 15 regression 구현
- 아르고스(테스터): Sonnet — G2 독립 검증 9항목
- haiku 미사용 (계약 정밀도 요구 작업 — Sonnet 이상 적정)

## 비고
- 설계 산출물 7종(envelope schema/expected_files/dedupe/enum/priority table/regression/risk) 전부 구현 반영.
- SPAWN_FAILED_OR_MISSING_HEARTBEAT 는 ANU fallback classifier 소관으로 이 task 제외(관계만 기록).
- doctrine 준수: same-PR post-Gemini push 금지, 봇 `/gemini review` 미수행, long polling 미수행(watcher 위임).

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

