# 작업 보고: task-3065

- **팀**: dev2-team (오딘)
- **레벨**: Lv.1 · 회장 승인 완료
- **저장소**: `Jeon-Jonghyuk/InsuRo` · main `22f74be` (변경 없음)
- **PR**: **없음** — ★ 명세 전제가 반증되어 구현하지 않았다. 명세 계약 "★ ANU 명세 전제가 틀렸다고 판단되면 구현하지 말고 반증을 먼저 기록한 뒤 보고하라" 에 따른다
- **명세 sha256(앞16)**: `7bd10809abf19b0b` (일치 확인)
- **레벨: 코드 수정 없음** — ★ 이 해치의 부여 권한은 ANU 다. 자가부여가 아니라 **판단 요청**으로 기재한다

---

## 요약 (SCQA)

### S — 회장이 "github actions 왜 자꾸 비용이 증가할까?" 라고 물었고, ANU 는 "같은 head_sha 에 CI 가 2회씩 돈다"를 근인으로 제시했다. 원인은 `concurrency` 그룹 키가 `github.ref` 라서 PR ref(`refs/pull/N/merge`)와 push ref(`refs/heads/main`)가 갈려 중복 제거가 작동하지 않는 것이라 했다. 회장은 3안 중 **(a) concurrency 수정만** 을 선택했다.

### C — 그 "2회" 를 실측으로 재현하려고 `head_sha` 별로 두 실행의 정체를 열어 보니, **두 실행 모두 `pull_request` 이벤트였고 `push` 는 한 건도 섞여 있지 않았다.** 초 단위까지 같은 시각, 인접한 run id, 전부 task 브랜치였다. ANU 가 말한 "PR 1회 + 머지 push 1회" 구조가 아니었다.

### Q — 그러면 같은 커밋에 CI 가 2번 뜬 것은 무엇인가? 그리고 제안된 그룹 키 수정은 무엇을 절감하는가?

### A — **중복이 아니었다.** `ci.yml` 과 `taskctl-ci.yml` 이 **둘 다 `name: CI`** 다. ANU 는 실행을 `.name` 으로 집계해 서로 다른 두 워크플로우를 한 워크플로우의 중복 실행으로 읽었다. `.path` 로 재집계하면 **4개 워크플로우 전부 head_sha 당 최대 1회**다. 중복은 처음부터 존재하지 않았고, 따라서 제안된 수정의 절감 효과는 **0분**이다. 이를 실데이터 76건으로 그룹 분할이 **완전히 동일**함을 증명해 확정했다. 구현하면 0을 위해 Actions 약 13분을 태우는 것이라 태스크 목적과 정면으로 어긋난다.

## 수정 파일별 검증 상태

| 파일 | 변경 | 검증 |
|---|---|---|
| .github/workflows/ci.yml | 없음 (0줄) | 중복 미존재 실증 — head_sha당 최대 1회(31실행) |
| .github/workflows/e2e.yml | 없음 (0줄) | head_sha당 최대 1회(21실행) · push 트리거 자체가 부재(t3033) |
| .github/workflows/diagnostic-pytest.yml | 없음 (0줄) | head_sha당 최대 1회(24실행) · push는 schedule로 이관됨(t3033) |
| .github/workflows/taskctl-ci.yml | 없음 (0줄) | concurrency 미추가 판단 — 근거 5절 |
| memory/reports/task-3065.md | 신규 | 본 문서 |

---

## 1. 반증 ① — "같은 커밋에 CI 2회" 는 집계 오류였다

ANU 가 제시한 중복 head_sha 를 `.path` 까지 열었다.

```
--- f2cb9c1a
    .github/workflows/taskctl-ci.yml   (pull_request)
    .github/workflows/ci.yml           (pull_request)
--- ee316f0d
    .github/workflows/ci.yml           (pull_request)
    .github/workflows/taskctl-ci.yml   (pull_request)
--- ed759331 / ea4bfcb7 / e168b775 / daeab93c / d4309498 / c526f0ef
    (전부 동일 — ci.yml 1회 + taskctl-ci.yml 1회)
```

원인은 이름 충돌이다.

```
ci.yml                 name: CI
taskctl-ci.yml         name: CI     ← 동명
e2e.yml                name: E2E Tests
diagnostic-pytest.yml  name: Diagnostic Pytest (full failure matrix)
```

`.name` 집계와 `.path` 집계가 갈린다.

```
.name 기준 (ANU)          .path 기준 (실제)
  CI                53회    ci.yml           31회
                            taskctl-ci.yml   22회   ← 합 53
  Diagnostic        24회    diagnostic-pytest.yml  24회
  E2E               21회    e2e.yml                21회
  Extension          2회    extension-release.yml   2회
```

**head_sha 당 실행 횟수 (최근 100건, path 기준)**

```
ci.yml                  최대 1 회
e2e.yml                 최대 1 회
diagnostic-pytest.yml   최대 1 회
taskctl-ci.yml          최대 1 회
```

**중복은 0건이다.** ANU 명세의 "조회한 전 커밋이 2회" 는 서로 다른 두 워크플로우를 더한 값이다.

> ★ 이 함정은 이미 기록돼 있었다. task-3033 결과물에 **"집계는 `.name` 아닌 `.path` — ci.yml·taskctl-ci.yml 동명 `CI`"** 가 남아 있다. 같은 저장소에서 3주 만에 같은 착시가 재발했다.

## 2. 반증 ② — 중복 쌍에 push 이벤트는 한 건도 없다

ANU 는 "PR을 열면 pull_request 로 1회, 머지하면 push 로 1회" 라고 했다. 실측은 다르다.

- 중복으로 지목된 21개 head_sha 의 42개 실행 **전부 `event=pull_request`**
- `push` 이벤트 실행은 총 10건이며 **전부 `ci.yml` 이고 전부 서로 다른 sha**(main squash 커밋). PR head sha 와 겹치는 것이 하나도 없다
- squash 머지라 main 커밋 sha 는 PR head sha 와 애초에 다르다. 따라서 "같은 head_sha 에 PR 실행과 push 실행이 함께 잡힌다" 는 구조적으로 성립할 수 없다

`e2e.yml` 과 `diagnostic-pytest.yml` 은 **push:main 트리거 자체가 없다**(t3033 이 각각 제거·schedule 이관). 이 둘에서는 PR/push 중복이 정의상 불가능하다.

## 3. 반증 ③ — 제안된 그룹 키는 분할을 전혀 바꾸지 않는다 (no-op 증명)

제안: `group: <prefix>-${{ github.event.pull_request.number || github.ref }}`

실제 실행 100건 중 대상 76건(ci/e2e/diagnostic)에 구·신 두 표현식을 각각 적용해 그룹 분할을 비교했다. Actions 를 소모하지 않는 결정론적 대조다.

```
대상 실행 76건 (ci/e2e/diagnostic)
  구(舊) 그룹 수: 11
  신(新) 그룹 수: 11
  ★ 분할(partition) 동일 여부: True
  한 그룹 최대 동시 실행수  구=17  신=17

  구 그룹키 샘플: ci-refs/pull/276/merge · e2e-refs/pull/276/merge · diagnostic-refs/pull/276/merge
  신 그룹키 샘플: ci-276              · e2e-276              · diagnostic-276
```

이유는 단순하다. `pull_request` 이벤트에서 `github.ref` 는 항상 `refs/pull/<N>/merge` 이므로 PR 번호 `N` 과 **일대일 대응**이다. 키 문자열만 바뀌고 묶이는 집합은 같다. `push`·`schedule`·`workflow_dispatch` 는 `pull_request.number` 가 null 이라 `github.ref` 로 폴백하니 현재와 글자까지 동일하다.

**절감 = 0분.** "PR 실행과 push 실행이 같은 그룹이 되어 뒤엣것이 앞엣것을 취소한다" 는 명세의 기대는 성립하지 않는다. push:main 실행은 PR 번호가 없어 `ci-refs/heads/main` 그룹에 그대로 남고, PR 실행은 `ci-276` 에 남아 **수정 후에도 여전히 다른 그룹**이다.

## 4. 명세 검증 항목별 판정

| 명세 요구 | 판정 | 근거 |
|---|---|---|
| 현재 중복 재현 (head_sha별 집계) | **재현 실패 — 중복 없음** | path 기준 4개 워크플로우 전부 최대 1회 |
| 수정 후 1회 실측 | **불필요 — 이미 1회** | 위와 동일. 2→1 이 아니라 1→1 |
| 취소가 올바른지 (뒤가 앞을 취소, 최종 1개 완료) | **해당 없음** | 취소할 중복이 존재하지 않음 |
| 워크플로우 간 간섭 없음 (prefix 유지) | **현재 이미 무간섭** | 그룹 수 11개가 prefix별로 분리 유지 |
| 필수 체크 8건 무손상 | **무손상 (파일 0줄 변경)** | 8건 전부 taskctl-ci.yml 소속·미변경. 5·6절 |
| actionlint YAML 문법 | **해당 없음** | 변경 파일 없음 |
| 봉인 변이 2종 | **해당 없음** | 봉인할 수정이 없음. 대신 3절에서 no-op 을 직접 증명 |

## 5. taskctl-ci.yml 판단 — concurrency 를 넣지 않는다 (ANU 권고와 동일)

ruleset `main-protection`(active) 의 필수 체크 8건을 확인했다.

```
cancel-kill-switch · qc-check · hidden-path-audit · lock-in-check
merge-safety-check · gemini-review-gate · ci/guard · guard
→ 8건 전부 taskctl-ci.yml 의 잡이다
ci.yml · e2e.yml · diagnostic-pytest.yml 은 required 목록에 없다
```

넣지 않는 근거는 세 가지다.

1. **절감할 것이 없다.** taskctl-ci.yml 도 head_sha 당 최대 1회다. 취소 대상이 아예 없다
2. **비용이 무의미하다.** 22회 합계 3.9분, 평균 **0.18분(약 11초)**. 전체 소비의 1.2% 다
3. **위험만 남는다.** 취소된 필수 체크는 `success` 가 아니라 `cancelled` 로 보고돼 머지가 막힌다. 이득 0 에 머지 차단 위험만 얻는 거래다

## 6. 인프라 장애 — base 대조 완료, 우리 원인 아님

명세가 지시한 base 대조를 수행했다.

**장애 경계가 선명하다.**

```
2026-08-29T17:50:45Z  ci.yml 319s · diagnostic 301s · taskctl-ci 12s   ← 정상
2026-08-29T17:59:46Z  ci 3s · diagnostic 3s · e2e 3s · taskctl-ci 4s   ← 전 워크플로우 즉사
      이후 현재까지 계속 (2026-08-30 11:43 KST 기준 약 17.7시간)
```

**base(main) 에서도 동일 재현된다.** PR 과 무관한 main push 실행이 같은 증상을 보인다.

```
main 22f74be7  failure  4s    ← ★ PR 없음. 최신 main. 같은 즉사
main f789fc57  failure  344s  ← 장애 이전, 정상 소요
main 98cc8804  success  453s  ← 장애 이전, 정상 성공
```

PR #276·#250 은 11건 전부 `failure` 이며 **시작~종료가 2~4초**, 실패 스텝이 없다. 필수 체크 8건도 함께 죽어 있다. 워크플로우·이벤트·브랜치를 가리지 않고 동시에 죽었으므로 **GitHub 인프라 장애**로 판단한다. 우리가 변경한 파일은 0개이므로 인과 자체가 없다.

★ 이 장애 때문에 라이브 실행 실측은 지금 불가능하다. 다만 본 태스크의 결론은 **과거 100건 실데이터** 로만 도출했고 신규 실행이 필요 없으므로, 장애가 결론에 영향을 주지 않는다.

## L1 스모크테스트

코드 변경이 0줄이라 애플리케이션 스모크 대상은 없다. 대신 **본 태스크의 결론을 떠받치는
외부 사실(실행 이력·필수 체크 구성)을 실서버 GitHub API 실호출로 직접 확인**했다.
보고서의 모든 수치는 이 경로로 얻은 것이며 캐시·기억이 아니다.

```
L1-1  워크플로우 실행 이력 조회 (중복 집계 근거)
      GET https://api.github.com/repos/Jeon-Jonghyuk/InsuRo/actions/runs?per_page=1
      → HTTP/2 200      ★ 성공

L1-2  ruleset 15953275 (main-protection) 조회 (필수 체크 무손상 근거)
      GET https://api.github.com/repos/Jeon-Jonghyuk/InsuRo/rulesets/15953275
      → HTTP/2 200      ★ 성공

L1-3  필수 체크 건수 계수
      required_status_checks = 8 건   ★ 명세와 일치 확인
      (cancel-kill-switch·qc-check·hidden-path-audit·lock-in-check·
       merge-safety-check·gemini-review-gate·ci/guard·guard)
```

★ 200 OK 는 조회 경로가 살아 있다는 뜻일 뿐이고, 판정의 근거는 응답 본문이다.
`.path` 기준 재집계(1절)·분할 동일성 증명(3절)·base 대조(6절)가 전부 이 응답에서 나왔다.

★ 워크플로우 파일은 한 줄도 바꾸지 않았으므로 필수 체크 8건은 구성·정의 모두 무손상이다.
현재 8건이 실패 중인 것은 6절의 인프라 장애이며 base(main `22f74be7`)에서 동일 재현된다.

## 7. 그러면 비용은 어디서 나가는가 (참고 — 구현하지 않음)

최근 100건의 실 소요시간이다. private 저장소라 전액 과금 대상이다.

```
ci.yml                  160.5분  (31회, 평균 5.18분)   ← 최대 소비원
diagnostic-pytest.yml    96.2분  (24회, 평균 4.01분)
e2e.yml                  70.8분  (21회, 평균 3.37분)
taskctl-ci.yml            3.9분  (22회, 평균 0.18분)   ← 필수 체크 8건. 사실상 무료
extension-release.yml     0.5분  (2회)
────────────────────────────────
합계                    331.9분  (2026-08-28 ~ 08-30, 약 2일)
```

**concurrency 로는 이 중 0분도 줄지 않는다.** 실제로 줄일 수 있는 것은 다음 두 가지이며, 둘 다 이번 태스크의 금지 사항이거나 회장 미승인이라 **손대지 않았다.**

- `ci.yml` 의 `push:main` 10회 × 평균 5.18분 ≈ **52분** (전체의 16%). squash 머지라 main 커밋 내용은 PR head 와 동일하므로 재검사다. 이것이 ANU 3안 중 (b) 이고 회장이 채택하지 않았다. 트리거 변경은 명세 금지 사항이다
- `update-branch` 재실행. 1회당 워크플로우 3개(ci+diagnostic+e2e ≈ 12.6분)를 새 sha 로 다시 돌린다. ANU 3안 중 (c) 의 운영 습관 항목이며 코드 변경 사안이 아니다

★ t3033 보고서에도 `ci.yml` push:main 이 **699분/27일** 로 남은 최대 소비원이라는 동일 결론이 이미 ANU 대기 상태로 기록돼 있다. 같은 지점을 두 태스크가 가리키고 있다.

## 8. Why 분석

**1st Why** — 왜 concurrency 수정으로 비용이 절반이 될 것이라 판단했나?
→ 같은 head_sha 에 CI 가 2회 도는 것을 관측했기 때문이다.

**2nd Why** — 왜 2회로 관측됐나?
→ 실행을 워크플로우 **이름**(`.name`)으로 집계했는데, `ci.yml` 과 `taskctl-ci.yml` 이 둘 다 `name: CI` 라 두 워크플로우의 각 1회가 한 워크플로우의 2회로 합산됐다.

**3rd Why** — 왜 이름 충돌을 못 걸렀나?
→ GitHub Actions 는 워크플로우 이름의 유일성을 강제하지 않고, `runs` API 응답에서 `.name` 이 `.path` 보다 눈에 먼저 들어온다. 그리고 관측된 "전 커밋이 2회" 라는 규칙성이 오히려 확신을 키웠다 — 중복이라면 산발적이어야 하는데 **예외 없이 정확히 2회** 인 것 자체가 구조적 합산의 신호였다.

## 9. ANU 판단 요청

1. **본 태스크를 반증 종결로 처리할지** — 구현 대상이 사라졌다. 절감 0 인 변경을 위해 Actions 13분을 태우는 것은 태스크 목적과 모순된다
2. **`## 레벨: 코드 수정 없음` 해치 부여** — 부여 권한이 ANU 이므로 자가부여하지 않았다
3. **동명 `CI` 를 정리할지** — `taskctl-ci.yml` 의 `name` 을 `Taskctl CI` 등으로 바꾸면 재발이 막힌다. 다만 required 체크는 **잡 이름**(`guard` 등)으로 걸려 있어 워크플로우 `name` 변경이 안전한지 별도 확인이 필요하다. ★ 이번 태스크 범위 밖이라 손대지 않았다
4. **실제 절감안 재상신 여부** — 7절의 `push:main` 52분(16%)이 유일한 실질 절감 지점이다. 회장이 (b) 를 반려했으므로 재상신은 ANU·회장 판단이다
5. **인프라 장애** — 약 17.7시간 계속 중이며 필수 체크 8건이 전부 죽어 **모든 PR 머지가 차단**돼 있다. 이번 태스크와 무관하지만 별건으로 긴급하다

## 11. 종결 게이트 결과 · scope-guard 오탐 근거 박제

**QC**: `6 PASS · 15 SKIP · 2 WARN · 0 FAIL` → `.qc-done` 생성됨
(1차 실행의 `git_evidence`·`l1_smoketest_check` FAIL 2건은 보고서 워크트리 커밋 + L1 섹션 추가로 해소)

★ 보고서는 **워크트리에서 커밋**했다. 메인 워크스페이스에 다른 봇의 미커밋이
staged 80 / unstaged 125 건 쌓여 있어 `git add -A` 로 쓸어담으면 남의 작업이 혼입된다.
워크트리 `.worktrees/task-3065-dev2` 커밋 `39351fb1`, 커밋 후 트리 clean.

**scope-guard**: `FAIL — 머지 차단 + .escalate 생성`. **판별 결과 스냅샷 부재 오탐이다.**

```
판별1  capabilities 스냅샷      → 없음 (memory/capabilities/task-3065.json 부재)
판별2  task 파일 allowed_resources → 있음 (2건 매칭)
판별3  실제 scope-diff          → memory/reports/task-3065.md  단 1개
판별4  base 244e0d9c 대비 변경   → A  memory/reports/task-3065.md  (추가 1건뿐)
```

명세 `allowed_resources.paths` 에 `memory/reports/task-3065.md` 가 명시돼 있으므로
**실질 범위 위반은 0건**이다. 스냅샷이 없는 이유는 본 실행이 `dispatch.py` 경유가 아니라
스케줄(cron) 직접 기동이라 dispatch 시점 스냅샷 저장 단계를 거치지 않았기 때문이다.

★ 이 해소의 부여 권한은 ANU 다. **자가해소하지 않았다.** 근거만 남기고 판단을 넘긴다.

## 10. 결론

명세의 근인 진단 3개가 모두 반증됐다 — 중복 존재(반증), push/PR ref 분기가 원인(반증), 제안 키가 중복을 제거(no-op 증명). **파일을 한 줄도 바꾸지 않았고 PR 도 만들지 않았다.** 필수 체크 8건은 변경 대상이 아니었으므로 무손상이며, 현재 8건이 실패 중인 것은 base 에서도 동일 재현되는 GitHub 인프라 장애다.
