# task-2778A — Terminal Artifact Surfacing 구현 보고서

상태명: `TASK2778A_TERMINAL_ARTIFACT_SURFACING_PR_READY_ACTIVE_FALSE`
담당: dev2-team (오딘 팀장 / 토르 백엔드)
작성일: 2026-06-28 KST
범위: surfacing layer only (hook/activation 전혀 아님, ACTIVE=false 유지)

---

## Situation (상황)

normal callback / terminal artifact visibility gap이 이미 발생한 명시적 병목이다.
PR #257 / task-2777+2 검증에서, dev2는 worktree-local에 terminal artifact(report·.done)를
**남겼으나** canonical `/home/jay/workspace`의 ANU authoritative polling 시야로 surface되지
않아 "terminal callback miss"처럼 보였다. 선행 설계
`memory/reports/terminal_callback_enforcement_design_260628.md` (DESIGN_READY)가 근본 원인을
`canonical ↔ worktree terminal artifact visibility gap`으로 확정했다.

## Complication (문제)

canonical-only 폴링은 worktree-local terminal artifact를 놓치고, callback envelope가
가리키는 report/result가 canonical에 없으면 "dangling pointer"로 오판한다. 그 결과 불필요한
재생성 사이클 + 추가 callback-miss 위험이 생긴다. 강제해야 할 것은 "남기기"가 아니라
**"ANU가 authoritative하게 볼 수 있는 위치로 surface하기"**다.

## Question (질문)

worktree-local(gitignored .done 포함)에 존재하는 terminal artifact의 존재를, 코드를 직접
수정하거나 callback을 발사하지 않고, ANU가 canonical에서 authoritative하게 알 수 있게
하려면 어떤 최소 surfacing 계층이 필요한가?

## Answer (답변)

canonical 단일 진실원인 **terminal artifact manifest(index)** 를
`memory/events/terminal_artifact_index/<task_id>.json`에 생성하는 **읽기+manifest-write 전용**
계층을 구현했다. worktree의 report/.done/envelope은 읽기만 하고, canonical에 manifest로
존재를 surface한다. hook 설치/Stop hook 수정/strict enforcement/callback delivery/activation은
전혀 포함하지 않는다 (범위 밖 — 절대 금지 준수).

---

## 생성 파일 (expected_files 4개 — effective diff == expected_files)

1. `dispatch/terminal_artifact_index.py` (495줄) — manifest 생성/검증/조회 모듈
2. `scripts/anu_terminal_artifact_surface.py` (143줄) — CLI (read/surface only)
3. `tests/regression/test_terminal_artifact_visibility_2778.py` (400줄) — 신규 회귀 (7 cases)
4. `memory/reports/task-2778a.md` — 본 보고서

> 본 보고서는 canonical `/home/jay/workspace/memory/reports/task-2778_a.md`에도 동일 내용으로
> surface된다 (이 task가 푸는 문제 그 자체 — worktree-local에만 남기지 않음).

## 구현 상세

### dispatch/terminal_artifact_index.py
- **상태 enum 7종** (task 명세 정확히 일치): `WORKTREE_LOCAL_TERMINAL_ARTIFACT_PRESENT`,
  `CANONICAL_TERMINAL_ARTIFACT_MISSING`, `TERMINAL_ARTIFACT_SURFACED_TO_CANONICAL`,
  `CALLBACK_ENVELOPE_ONLY`, `CALLBACK_ENVELOPE_DANGLING_TARGETS`,
  `ANU_PICKUP_FROM_WORKTREE_REQUIRED`, `TERMINAL_CALLBACK_MISSING`
- `TerminalArtifactManifest` dataclass: task_id, branch, head, worktree_path, report_path,
  done_path, envelope_path, status, statuses[], schema_version, generated_at + `to_dict()`.
  **모든 path 필드 상대경로 중심, raw key 0.**
- `build_manifest(...)` — **읽기 전용**(디스크 write 0). worktree/canonical 양쪽에서 artifact
  존재 확인 후 manifest 반환. envelope JSON 파싱하여 result_path/report_path/done_path 타겟을
  추출하고, 타겟이 canonical에 없어도 **worktree에 존재하면 dangling 오판 방지**(구현요구5).
  fail-open: 예외 시 explicit status(`TERMINAL_CALLBACK_MISSING`) manifest 반환.
- `surface_manifest(...)` — **유일한 write**. canonical index 파일에 JSON 기록 + statuses에
  `TERMINAL_ARTIFACT_SURFACED_TO_CANONICAL` 추가. task_id path traversal 방어(whitelist).
- `read_manifest(...)` / `surface(...)` 편의 함수.
- import는 stdlib만(json, os, re, dataclasses, datetime, typing). callback/cron/cokacdir/driver/
  subprocess/network 호출 0.

### scripts/anu_terminal_artifact_surface.py
- argparse CLI: `--task-id`(필수) `--worktree`(필수) `--canonical-root`(기본 /home/jay/workspace)
  `--branch` `--head` `--json` `--no-write`(read-only 모드).
- read/surface only. callback/fire/cron/driver 절대 없음. main() fail-open(traceback 비노출).

## 발견 이슈 및 해결
- **이슈1**: `datetime.utcnow()` deprecation (Pyright 경고). → timezone-aware
  `datetime.now(datetime.timezone.utc)`로 교체 해결.
- **이슈2**: 테스트 내 미사용 `import ast`. → 제거.
- Pyright `reportMissingImports`(dispatch.terminal_artifact_index)는 정적 분석 한계로 인한
  false positive — 런타임 sys.path 부트스트랩으로 resolve되며 pytest 7/7 PASS로 실증됨.

## 검증 결과 (13항목)
1. py_compile (모듈 2개): **OK**
2. 신규 regression: **7 passed**
3. task-2777 status command test: **30 passed**
4. task-2776 live inbox evidence test: **6 passed**
5. task-2775 runtime guard test: **63 passed**
6. raw key/secret scan: **0건** (cokacdir/cron/fire 매칭은 전부 "0" 부정 docstring/주석)
7. effective diff == expected_files: **충족** (untracked 3 코드 + 보고서)
8. forbidden paths 변경: **0** (finish-task.sh/dispatch.py/.github/.claude/.env/memory/state 무변경)
9. ACTIVE=false: **유지** (flag ON 코드 없음)
10. flags OFF: **유지**
11. real fire: **0**
12. systemd inactive/disabled: **불변** (조작 코드 없음)
13. cron 신규: **0** (executor self 등록 없음 — ANU callback은 finish-task.sh 게이트 경유)

## L1 스모크테스트 결과
- 서버 재시작: **해당없음** (서버 아님 — manifest surfacing 모듈)
- API 응답 확인: **해당없음** (CLI 도구)
- 실제 프로세스 실행 (subprocess/정제 작업 유형):
  - tmp 격리 환경에서 CLI 실제 실행 → manifest JSON 생성 확인.
    status=`WORKTREE_LOCAL_TERMINAL_ARTIFACT_PRESENT`, statuses에 `CANONICAL_TERMINAL_ARTIFACT_MISSING`,
    `ANU_PICKUP_FROM_WORKTREE_REQUIRED`, `TERMINAL_ARTIFACT_SURFACED_TO_CANONICAL` 포함.
  - canonical index 파일 `memory/events/terminal_artifact_index/task-2778_a.json` 실제 write 확인.
  - worktree 변경 0건 (read-only 불변식 실증).
- 스크린샷: **해당없음** (CLI/백엔드 모듈)

## 모델 사용 기록
- 토르(백엔드): **sonnet** — 모듈 2개 + 회귀 테스트 구현 (코딩 작업, haiku 미사용 정당)
- 오딘(팀장, Opus): 설계/API 계약/검증/통합 + datetime·import 정리 (직접 코딩 최소화)
- 프레이야/미미르/헤임달: 미소환 (surfacing 백엔드 단일 성격 — 프론트/UX/별도 테스터 불요)

## 머지 판단
- **머지 필요**: No (봇 merge 금지 — merge_policy=none). PR 생성까지만.
- **브랜치**: `task/task-2778_a-dev2`
- **워크트리 경로**: `/home/jay/workspace/.worktrees/task-2778_a-dev2`
- **머지 의견**: 신규 모듈 2 + 테스트 1, 기존 회귀 무영향(2775/2776/2777 전부 PASS). forbidden
  paths 무변경. ACTIVE=false. merge는 ANU 독립검증 → fresh Gemini → CI → 회장 승인 별도 단계.

## 비고
- 디자인팀 호출: 불요 (이미지/배너 없음).
- callback: 종료 시 finish-task.sh가 ANU-owned callback을 게이트 경유로 처리(executor self-key
  자가발사 0, ANU normal collector 별도 spawn). 본 executor는 manifest only, callback fire 0.

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


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


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


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

