diff --git a/devlog/_plan/260808_bug_campaign/000_plan.md b/devlog/_plan/260808_bug_campaign/000_plan.md new file mode 100644 index 0000000000..4cb15cbbc8 --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/000_plan.md @@ -0,0 +1,146 @@ +# 260808 — 전량 버그 이슈/PR 캠페인: 로드맵 + +Base: `origin/dev@a259d63dc` (2026-08-08 커트오프, 감사 후 재동결). +Cycle: docs-first. 이 유닛은 계획만 쓴다. 프로덕션 코드는 다음 사이클부터. + +> 감사(A) 이력: 독립 감사를 5라운드 돌렸다(블로커 9, 5, 5, 4, 3건). 전부 +> 반영했고 반박한 항목은 없다. 주요 교정은 close 판정 2건 철회(#1176, #1024), +> 인벤토리 재동결과 라벨 기반 게이트 도입, WP3 스택 해체(#1255 머지), +> 활성화 시나리오 보강, 의존성 순서 정정이다. 인용 13건은 감사에서 전부 +> 정확한 것으로 확인됐다. +> +> 감사 중에도 라이브 상태가 계속 움직였다 — PR #1263, #1264, #1265, #1266이 +> 새로 열리고 #1255, #1257이 머지됐으며 이슈 3건이 닫혔다. 문서를 한 시점에 +> 얼려두는 대신 WP1의 라이브 갱신 게이트가 실행 직전에 차이를 흡수한다. + +## 이 유닛이 존재하는 이유 + +열린 이슈 중 버그 계열이 25건, 열린 PR 중 버그 계열이 28건이다(#1266 포함, +종결분 제외). 그중 #1265는 `main` 타겟 릴리스 경로라 배제하므로 **실제 처리 +대상은 27건**이다. 지난 +캠페인들이 개별 항목을 처리했지만 이번에는 커트오프 시점의 **전량**에 터미널 +처분을 내린다. 처분은 셋 중 하나다: 리베이스 후 공동커밋으로 재발행, 위양성 +판정 후 close, 업스트림 차단 등으로 tracking 유지. + +방식은 사용자가 지정했다. 기여자에게 체크리스트 완료를 요청해 기다리는 대신 +**maintainer가 직접 `origin/dev` 위로 리베이스하고 새 PR을 연다.** 원작자는 +`Co-authored-by` 트레일러와 PR 본문 멘션으로 보존한다. + +## 선행 발견: CI 승인 병목이 재발했다 + +처분을 논하기 전에 구조적 사실 하나를 기록한다. `action_required` 상태로 멈춘 +워크플로 런이 52개 브랜치에 걸쳐 쌓여 있고, 그중 **열린 PR 26건이 CI 승인 대기로 +막혀 있다.** + +이것이 "기여자들이 CI를 안 돌렸다"처럼 보이는 현상의 실제 원인이다. +`enforce-target`의 준비완료 게이트는 head의 `ci` 체크가 green임을 확인해야 +draft를 벗기는데, 애초에 실행 허가를 받지 못한 런은 green이 될 수 없다. 작성자가 +무엇을 하든 draft에서 나올 수 없는 구조다. + +따라서 **열린 PR에 속한 런의 승인이 모든 처분의 선행조건**이다. 52개 브랜치 +전부를 승인할 필요는 없다 — 대부분 이미 머지됐거나 버려진 브랜치다. + +막힌 PR 26건: #1260 #1259 #1258 #1256 #1249 #1244 #1240 #1235 #1228 #1226 #1224 +#1212 #1210 #1209 #1205 #1202 #1195 #1192 #1189 #1187 #1185 #1184 #1178 #1169 +#1109 #1010. + +## 처분 요약 + +모든 판정은 PR 설명이 아니라 diff와 현행 트리를 읽어 도출했다. 근거는 +`003_disposition_matrix.md`에 file:line으로 남긴다. + +### 재발행 대상 (리베이스 + 공동커밋) + +| 대상 | 원작자 | 근거 요약 | +|---|---|---| +| #1189 history index stream tail | luvs01 | `src/routing/history/indexer.ts:195` 이 미인덱스 tail 전체를 `Buffer.allocUnsafe`로 할당 | +| #1187 routing analytics malformed | luvs01 | `src/routing/analytics.ts:153` 가 비배열 `attempts`에 런타임 검증 없이 접근 | +| #1184 command-code own lookups | luvs01 | `src/adapters/command-code.ts:321,350` 이 프로토타입 상속 키를 그대로 해석 | +| #1258 reasoning-effort trace 경계 | luvs01 | `src/routing/trace.ts:468-474` 가 앞부분만 검증하고 전체 배열을 순회 | +| #1256 usage 시작 hydration 경계 | luvs01 | `src/usage/log.ts:658-664` 가 파일 전체를 `Buffer.alloc` | +| #1195 unbound quota 증거 | luvs01 | `src/router.ts:516-533` 이 미바인딩 계정을 대체 주입 | +| #1202 history lock 오탐 | Yuxin-Qiao | `src/codex/inject.ts:1036-1041` 이 모든 실패를 lock 문구로 수렴 | +| #1169 codex-shim readiness | TyroneXie | `src/cli/index.ts:1151-1155` 가 라우팅 확인 없이 green 출력 | +| #1192 bounded SSE 확장 | luvs01 | `src/server/responses-json-events.ts:24-51` 이 전 프레임을 한 문자열로 결합 | +| #1249 빈 data 프레임 | Yuxin-Qiao | `src/adapters/openai-chat.ts:951-963` 에 빈 페이로드 가드 부재 | +| #1163 combo 카탈로그 폴백 | eachann1024 | `src/codex/catalog/aggregation.ts:102-136` 이 결측 멤버와 빈 ladder를 구분 못함 | +| #1226 DeepSeek 컨텍스트 창 | iF2007 | `src/providers/registry.ts:1295-1306` 에 jawcodeBundle 부재 | +| #1224 프로바이더별 컨텍스트 캡 | iF2007 | `src/server/management/provider-routes.ts:644-655` 가 `setAll` 무관하게 전역 적용 | +| #1178 Antigravity 라이브 발견 | iF2007 | `src/providers/registry.ts:1290` 이 `liveModels: false` 고정 | +| #1244 desktop picker 라우팅 보존 | Wibias | `src/codex/catalog/sync.ts:543-549` 가 슬래시 유무로만 라우팅 행 인식 | +| #1185 Windows shard 어서션 | luvs01 | `tests/ci-workflows.test.ts:166-169` 의 약한 부분문자열 매칭 | + +### 재작업 필요 + +| 대상 | 문제 | +|---|---| +| #1240 SSE null 프레임 | 결함은 실재하나 **종료 동작이 틀렸다.** 리포터 정정에 따르면 `data: null` 은 유효 청크 사이에 나타난다. 종료하면 뒤따르는 finish 청크와 `[DONE]` 을 버린다. 비레코드 분기를 `continue` 로 바꿔야 한다 | +| #1259 CI 페이지네이션 증거 | 코드는 정상. `hygiene` 실패 사유는 `unsponsored_surface` — 보호된 워크플로 표면을 건드려 maintainer 보안 검토와 `maintainer-sponsored` 라벨이 필요하다 | + +### 위양성 — close 대상 + +| 대상 | 근거 | +|---|---| +| #1155 web-search buffered 정책 | 고치려는 경로가 현재 도달 불가. DeepSeek이 `0b8e608c0` 에서 bounded-JSON 정책을 폐기했고, 프로덕션 레지스트리에 opt-in 항목이 없다. `src/web-search/loop.ts:364` 는 항상 `stream: true` | +| #1119 routed reasoning 계약 (maintainer 본인 PR) | 유일한 코드 훅이 낡은 테스트 추가인데, 주장하는 계약이 이미 `tests/codex-catalog.test.ts:2391-2451` 에 존재 | +| 이슈 #1100 routed effort 미전파 | `tests/codex-catalog.test.ts:2391-2451` 에 회귀 커버리지 존재. 구현은 `aa8851f38`, `2f242bb7c`, `07e7525b8` | +| 이슈 #1128 remote compaction | `src/server/responses/compact.ts:651-665` 가 이미 내부적으로 `stream: false` | +| 이슈 #1102 wildcard bind | 이미 구현·전달됨. `src/server/index.ts:499-540`, 실소켓 테스트 `tests/loopback-listener-integration.test.ts:108-122` | + +### 직접 수정 대상 (PR 없는 이슈) + +| 이슈 | 수정 위치 | +|---|---| +| #1219 SSE null 프레임 | `openai-chat.ts:961-972`, `google.ts:500-510`, `anthropic.ts:987-995`, `web-search/parse.ts:158-163` — 4곳 모두 | +| #1213 Claude Desktop 카탈로그 교체 | `gui/src/pages/ClaudeDesktop.tsx:477` 에 사전 경고/확인 부재 | +| #1229 namespaced 라우팅 모델 거부 | `src/codex/inject.ts:107-114` 가 `model_provider = "openai"` 유지 | +| #1145 opencode-zen rate limit | `src/providers/registry.ts:2023` 키드 항목에 note 부재 | +| #241 desktop picker 누락 | #1244 가 구현 후보. `src/codex/convergence.ts:191-198` 도 슬래시 기준 | +| #1059 Windows 전체 스위트 | `.github/workflows/ci.yml:413-438` dispatch-only. 최근 실제 디스패치 `31095755263` 은 4개 shard 전부 실패 | + +### tracking 유지 + +| 이슈 | 사유 | +|---|---| +| #417 | 업스트림 `openai/codex#35161` 여전히 OPEN | +| #92 | 업스트림 `openai/codex#32031` 여전히 OPEN. dev는 조용한 전달 대신 명시적 실패로 완화만 함 | +| #1162 Cursor Claude 계열 | 정적 코드로는 핸드셰이크 원인 증명 불가. 대조 wire 캡처 필요 | +| #904, #796, #418 | 재현 캡처 부재. needs-info 유지 | + +## work-phase 맵 (의존성 순) + +phase 경계는 시스템의 빌드 순서를 따른다. 효율이나 난이도로 자르지 않는다. + +| WP | 내용 | 선행 | +|---|---|---| +| WP0 | 이 문서군 (docs-only) | — | +| WP1 | CI 승인 해제(27건) + 무충돌 소형 13건 재발행 | WP0 | +| WP2 | SSE/스트리밍: #1219 수정 위에 #1249, #1205 | WP1 | +| WP3 | CI 워크플로 독립 2건 (#1259, #1185) | WP1, #1265 확인 | +| WP4 | 카탈로그 순차 (#1224, #1226, #1178, #1244, #1163, #1228, #1266) | WP1 | +| WP5 | PR 없는 이슈 직접 수정 | WP1 (#1145만 WP4) | +| WP6 | 처분 집행 (close 3건, tracking 11건) | WP4 | + +WP3이 스택이 아닌 이유: 초안의 스택 루트 #1255가 `d55b903d8` 로 머지되어 현재 +`origin/dev` 그 자체가 됐다. 남은 둘은 훅을 공유하지 않아 각각 독립 PR이다. + +WP6의 close 3건: PR #1155, PR #1119, 이슈 #1128. 초안의 이슈 close 3건 중 +#1100과 #1102는 캠페인 중 외부에서 닫혀 대상에서 빠졌다. + +WP4가 순차인 이유: 일곱 PR이 `src/providers/registry.ts`, `src/codex/catalog/*`, +`src/types.ts`, `tests/codex-catalog.test.ts` 를 공유한다. 스택으로 쌓기보다 +한 건 착지 후 다음 건을 리베이스하는 편이 캐스케이드 사고를 줄인다. + +WP5의 선행 정정: 초안은 WP5 전체가 WP2를 기다린다고 했으나 실제 파일 겹침이 +없어 불필요한 직렬화였다. 실제 겹침은 050-6(#1145)이 `src/providers/registry.ts` +를 #1226/#1178과 공유하는 것 하나뿐이며, 이 항목만 WP4 뒤에 온다. + +050-5(#1218)도 `src/codex/catalog/metadata.ts` 를 #1244와 공유했으나, 해당 +이슈가 2026-08-08T03:40:15Z에 외부에서 닫혀 **실행 대상에서 제외**됐다. 따라서 +이 의존은 더 이상 존재하지 않는다. + +## 검증 선행조건 + +이 체크아웃에서 `bun run typecheck` 가 `bun-types` 부재로 exit 1이다(감사 실측). +어떤 work-phase든 검증 명령 전에 `bun install` 을 먼저 돌린다. 그것 없이 나온 +결과는 증거로 쓰지 않는다. diff --git a/devlog/_plan/260808_bug_campaign/001_inventory.md b/devlog/_plan/260808_bug_campaign/001_inventory.md new file mode 100644 index 0000000000..273e62eb9a --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/001_inventory.md @@ -0,0 +1,122 @@ +# 001 — 커트오프 인벤토리 + +수집 시각: 2026-08-08. Base `origin/dev@a259d63dc`. + +> **재동결 기록.** 최초 수집은 `ec8ceef00` 기준이었으나 감사(A) 도중 dev가 +> `a259d63dc` 로 이동하고 PR 2건(#1263, #1260)이 추가되었다. 감사 블로커 1번에 +> 따라 전 항목을 재수집해 아래로 교체한다. + +## 수집 명령과 원본 수치 + +``` +gh issue list --state open --limit 200 -> 64건 (전체 열린 이슈) + 라벨 bug 또는 provider-compatibility 필터 -> 25건 +gh pr list --state open --limit 100 -> 39건 (전체 열린 PR) + 제목이 fix( 또는 test( 로 시작 -> 27건 +gh run list --status action_required --limit 400 -> 52개 브랜치 + 그중 열린 PR의 head 브랜치 -> 26건 +``` + +## 버그 계열 PR 30건 (dev 범위 29 + main 제외 1) + +아래 표는 30행이다. 그중 #1265는 `main` 을 타겟하는 릴리스 경로 항목이라 캠페인 +실행 대상이 아니다. **실제 처리 대상은 29건**이며, #1265는 배제 사실을 명시하기 +위해 표에 남긴다. + +게이트 2차 실행(`013` 문서)에서 #1269, #1268이 추가됐다. 둘 다 우리 WP5 계획과 +겹치므로 직접 구현 대신 채택으로 전환했다. + +| PR | 제목 | 작성자 | draft | 처분 | WP | +|---|---|---|---|---|---| +| 1269 | check live proxy before journal recovery | Ingwannu | Y | 채택 + handleEnsure 보완 요청 | WP5 | +| 1268 | hide npm launcher proxy child | Ingwannu | Y | 채택 (050-2 대체) | WP5 | +| 1266 | replay Vertex thought signatures | Ingwannu | N | 재발행 | WP4 | +| 1265 | promote workflow comment-spam hardening to main (hotfix) | Wibias | N | **범위외**(main 핫픽스) | — | +| 1264 | reject null Claude toggle bodies | luvs01 | Y | 재발행 | WP1 | +| 1263 | reject profile FIFOs without blocking | luvs01 | Y | 채택 + 테스트 수정 요청 | WP1 | +| 1260 | restrict plaintext sideband overrides to numeric loopback | luvs01 | Y | 재발행(보안) | WP1 | +| 1259 | require paginated aggregate-check evidence | luvs01 | Y | 재작업(라벨 필요) | WP3 | +| 1258 | bound reasoning-effort trace hydration | luvs01 | Y | 재발행 | WP1 | +| 1256 | bound startup hydration tail reads | luvs01 | Y | 재발행 | WP1 | +| 1249 | ignore empty data: frames | Yuxin-Qiao | N | 재발행 | WP2 | +| 1244 | preserve routed models in desktop picker | Wibias | N | 재발행 | WP4 | +| 1240 | treat non-record data frame as malformed | snowyukitty | N | **채택** (작성자가 continue로 수정 완료) | WP2 | +| 1228 | Add native image support for Cursor | yansigit | Y | 재발행(대형단독) | WP4 | +| 1226 | restore DeepSeek V4 context window | iF2007 | N | 재발행(dirty 충돌) | WP4 | +| 1224 | keep per-provider context caps independent | iF2007 | N | 재발행 | WP4 | +| 1210 | move per-role model fallback into config | Yuxin-Qiao | Y | 재발행 | WP1 | +| 1205 | inject reasoning placeholder on replay miss | Yuxin-Qiao | Y | 재발행 | WP2 | +| 1202 | stop reporting every history failure as DB lock | Yuxin-Qiao | Y | 재발행 | WP1 | +| 1195 | keep unbound account quota unknown | luvs01 | Y | 재발행 | WP1 | +| 1192 | bound synthesized SSE expansion | luvs01 | Y | 재발행 | WP1 | +| 1189 | stream request index ingestion | luvs01 | Y | 재발행 | WP1 | +| 1187 | tolerate malformed historical attempts | luvs01 | Y | 재발행 | WP1 | +| 1185 | bind Windows shard assertion to executable command | luvs01 | Y | 재발행 | WP3 | +| 1184 | guard own-property model lookups | luvs01 | Y | 재발행 | WP1 | +| 1178 | discover Antigravity live models | iF2007 | N | 재발행(보안검토) | WP4 | +| 1169 | warn when codex-shim install cannot prove routing | TyroneXie | Y | 재발행 | WP1 | +| 1163 | synthesize incomplete combo members | eachann1024 | Y | 재발행 | WP4 | +| 1155 | preserve buffered upstream policy | myrosla | Y | **close(위양성)** | WP6 | +| 1119 | pin the routed reasoning joint contract | lidge-jun | N | **close(위양성)** | WP6 | + +감사 라운드 1에서 추가: #1263, #1260, #1257, #1228, #1210, #1205. #1163도 WP4로 +배정했다. 감사 라운드 3에서 추가: #1264(WP1 재발행), #1265(범위외). + +#1265는 `main` 을 타겟하는 워크플로 핫픽스다. 이 캠페인은 `dev` 대상 버그 +처리이고 `main` 승격은 maintainer 릴리스 경로이므로 범위 밖이다. 다만 #1255와 +같은 워크플로 표면을 건드리므로 **WP3 착수 전에 그 착지 여부를 확인**해야 +한다 — 이미 `main` 에 올라간 내용을 `dev` 에서 다시 만들면 충돌한다. 보안 +검토는 릴리스 경로에서 별도로 수행된다. + +### 부록 — 캠페인 중 외부에서 종결된 항목 + +아래는 위 현재 집합(PR 표 30행 / 실제 처리 29건)에 **포함되지 않는다.** 이력 보존용이며 실행할 작업이 +없다. 표 행수를 셀 때 이 항목들을 더하지 말 것. + +| 항목 | 종결 | 원래 배정이었던 것 | +|---|---|---| +| PR #1257 | 머지 `db371021c`, 2026-08-08T05:50:40Z | WP1 GUI 재발행 | +| PR #1255 | 머지 `d55b903d8`, 2026-08-08T05:54:05Z | WP3 스택 루트 | +| 이슈 #1100 | CLOSED 2026-08-08T02:14:24Z | WP6 close | +| 이슈 #1102 | CLOSED 2026-08-08T02:14:44Z | WP6 close | +| 이슈 #1218 | CLOSED 2026-08-08T03:40:15Z | WP5 050-5 수정 | + +#1255의 머지가 WP3 구조를 바꿨다. 스택 루트가 dev에 흡수됐으므로 #1259와 #1185는 +각각 `origin/dev` 기반 독립 PR이 된다. 상세는 `030` 문서 참조. + +## 버그 계열 이슈 25건 (현재 열린 집합) + +열린 이슈만 담는다. 종결된 #1100, #1102, #1218은 부록에 있으며 이 행수에 +포함하지 않는다. + +| 이슈 | 제목 요약 | 대응 PR | 처분 | WP | +|---|---|---|---|---| +| 1245 | GUI Startup Safety stale error | 없음 | 직접수정 | WP5 | +| 1236 | Windows 콘솔창 팝업 (windowsHide) | 없음 | 직접수정 | WP5 | +| 1230 | 동시 start 시 journal 선복원 | 없음 | 직접수정 | WP5 | +| 1229 | ChatGPT auth가 namespaced 모델 거부 | 없음 | 직접수정 | WP5 | +| 1222 | Windows STATUS_STACK_BUFFER_OVERRUN | 없음 | tracking(반증실험 요청) | WP6 | +| 1219 | SSE null 프레임 크래시 | #1240 | #1240 착지 후 close | WP2 | +| 1213 | Claude Desktop 카탈로그 무단 교체 | 없음 | 직접수정 | WP5 | +| 1196 | issue-quality media 정규화 손상 | 없음 | 직접수정 | WP5 | +| 1193 | preserveReasoningContentModels 400 | #1205 | PR 착지 후 close | WP2 | +| 1191 | Windows DB locked 오탐 | #1202 | PR 착지 후 close | WP1 | +| 1190 | per-role model_fallback TOML 거부 | #1210 | PR 착지 후 close | WP1 | +| 1176 | DeepSeek V4 Flash 502 | 없음 | **tracking 유지** (감사 블로커 2) | WP6 | +| 1162 | Cursor Claude 계열 실패 | 없음 | tracking(wire 캡처 요청) | WP6 | +| 1145 | opencode-zen rate limit 무고지 | 없음 | 직접수정(범위 제한) | WP5 | +| 1128 | remote compaction 실패 | 없음 | close(해결됨) | WP6 | +| 1091 | ChatGPT OAuth 커스텀 업스트림 URL | 없음 | 범위외(enhancement) | — | +| 1059 | Windows 스위트 dispatch-only | 없음 | 상태규명 선행 | WP5 | +| 1024 | 커스텀 프로바이더 vision 모호 | 없음 | **tracking 유지** (감사 블로커 3) | WP6 | +| 904 | Kimi/Opus 한글 U+FFFD | 없음 | tracking(needs-info) | WP6 | +| 796 | Volcengine Ark 400 | 없음 | tracking(needs-info) | WP6 | +| 540 | WordPress Studio Code 프로바이더 | 없음 | 범위외(feature) | — | +| 418 | V2 delegation 실패 | 없음 | tracking(needs-info) | WP6 | +| 417 | 한국어 음성 U+FFFD | 없음 | tracking(업스트림) | WP6 | +| 241 | Desktop picker 라우팅 모델 누락 | #1244 | PR 착지 후 close | WP4 | +| 92 | V2 NEW_TASK body 소실 | 없음 | tracking(업스트림) | WP6 | + +#1091과 #540은 `provider-compatibility` 라벨 때문에 모수에 잡히지만 실제로는 +프로바이더 추가 요청(enhancement)이다. 이번 버그 캠페인 범위 밖임을 명시하고 +처분표에서 제외한다 — 감사 블로커 1번의 "orphan" 지적에 대한 답이다. diff --git a/devlog/_plan/260808_bug_campaign/002_disposition_matrix.md b/devlog/_plan/260808_bug_campaign/002_disposition_matrix.md new file mode 100644 index 0000000000..9f6b0e66f7 --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/002_disposition_matrix.md @@ -0,0 +1,258 @@ +# 002 — 처분 매트릭스 (근거 포함) + +조사 시점 base: `origin/dev@ec8ceef00`. 5개 독립 조사 레인이 그 시점의 실제 +소스를 읽어 도출했다. PR 설명만으로 내린 판정은 없다. + +> **출처 주의.** 이 문서의 file:line 인용은 `ec8ceef00` 시점 기준이다. 감사에서 +> 13건을 표본 검증했고 전부 그대로 유효했다. 이후 dev가 `a259d63dc` 를 거쳐 +> `d55b903d8` 로 이동했으므로, 실제 작업 착수 시점에는 각 인용을 현재 head에서 +> 다시 확인한다(`010` 문서의 라이브 갱신 게이트). +> +> 감사에서 뒤집힌 판정: #1176, #1024는 close에서 **tracking 유지**로 변경. +> #1257은 `db371021c` 로 dev에 머지되어 재발행 대상에서 제외. + +## A. 재발행 (리베이스 + Co-authored-by) + +### 무충돌 소형 — WP1 + +**#1189** `luvs01 <27862058+luvs01@users.noreply.github.com>` +`src/routing/history/indexer.ts:195-196` 이 `const length = size - fromOffset` +계산 후 `Buffer.allocUnsafe(length)` 를 호출하고, `:266` 이 미인덱스 tail 전체를 +넘긴다. PR은 청크 단위 수집으로 대체하며 부분 라인 오프셋 규칙을 보존한다. +커밋 `6e7269d05`, `d5242a231`. + +**#1187** `luvs01 <27862058+luvs01@users.noreply.github.com>` +`src/routing/analytics.ts:153-154` 이 `attemptsOf(entry) ?? []` 후 검증 없이 +`attempt.recoveryKinds.some(...)` 를 호출한다. `attempts` 가 배열이 아닌 JSONL +행에서 throw. 커밋 `8b413ac50`. + +**#1184** `luvs01 ` +`src/adapters/command-code.ts:321,350` 과 `src/providers/command-code-efforts.ts:34,62` +가 객체를 직접 인덱싱한다. `constructor`, `toString` 같은 ID가 상속 속성으로 +해석된다. `Object.hasOwn` 가드가 모든 조회 지점을 덮는다. 커밋 `cc01ba04e`. + +**#1258** `luvs01 ` +`src/routing/trace.ts:468-474` 이 잘라낸 접두부만 검증한 뒤 `:470` 에서 전체 +영속 배열을 순회한다. PR은 보존된 8개 항목만 읽고 sparse hole도 거부한다. +커밋 `1af3b74de`. + +**#1256** `luvs01 ` +`src/usage/log.ts:658-664` 가 "파일 전체까지" 확장한다고 명시하며 +`Buffer.alloc(size - start)` 를 호출하고, `:671` 이 창을 `size` 까지 키운다. +64 MiB 상한이 전체 원장 읽기를 막는다. 커밋 `50117895 6`. + +**#1195** `luvs01 <27862058+luvs01@users.noreply.github.com>` +`src/router.ts:516-529` 가 프로세스 활성 Codex 계정을, `:531-533` 이 활성 +Anthropic 계정을 주입한다. 관리 dry-run이 `src/server/management/routing-profile-routes.ts:118-135` +에서 같은 동작을 반복한다. 커밋 `e555f7b44`, `6eff3f6a5`. + +**#1202** `Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com>` +`src/codex/inject.ts:1036-1041`, `:1261-1263`, `src/cli/index.ts:900-903` 이 모든 +실패를 lock 문구로 수렴시킨다. 별개로 `src/codex/history-lock.ts:184-187` 이 +`realpathSync.native(databasePath) !== databasePath` 일 때 거부하는데 +`src/codex/user-identity.ts:157-163` 은 정규화되지 않은 Windows 루트를 반환한다. +커밋 4개: `d30ad97ab`, `fe1d1e539`, `e9d58d805`, `44b0a04b6`. 이슈 #1191 해소. + +**#1169** `TyroneXie <328347833@qq.com>` +`src/cli/index.ts:1151-1155` 가 `r.installed` 만으로 green을 출력하고, Codex가 +실제로 OpenCodex를 경유하는지 확인하지 않는다. 커밋 `d8968b7e6`. + +**#1192** `luvs01 <27862058+luvs01@users.noreply.github.com>` +`src/server/responses-json-events.ts:24-38` 이 출력 항목당 프레임 배열을 만들고 +`:49-51` 이 전부를 한 문자열로 join, `src/server/responses/core.ts:2452` 가 그 +전체 본문을 반환한다. 리베이스 충돌은 `structure/04_transports-and-sidecars.md` +2줄뿐 — 양쪽 문서 텍스트를 모두 보존한다. 커밋 `b50f23943`. + +### 스트리밍 — WP2 + +**#1249** `Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com>` +`src/adapters/openai-chat.ts:951-963` 이 `trim()` 직후 `[DONE]` 검사와 +`JSON.parse` 로 진행한다. 빈 `data:` 가 종료성 malformed 오류가 된다. +커밋 `20c7afb5`. + +### 카탈로그 — WP4 + +**#1224** `xinweigao ` (13커밋, head `3e23b4b`) +`src/server/management/provider-routes.ts:644-655` 의 PUT이 `setAll` 과 무관하게 +항상 `setGlobalContextCapValue` 를 호출하고 capped 프로바이더를 전부 지운다. + +**#1226** `xinweigao ` (커밋 `a74325a`, `ad4459a`) +`src/providers/registry.ts:1295-1306` 에 DeepSeek의 `jawcodeBundle` 이 없고 +`modelContextWindows` 가 `1_000_000` 이다. + +**#1178** `Xinwei Gao ` (4커밋) +`src/providers/registry.ts:1290` 이 `liveModels: false` 로 고정돼 있다. 발견, +캐시 동일성, OAuth 조정, 아웃바운드 POST 하드닝을 함께 바꾸므로 보안 민감 +슬라이스로 취급한다. + +**#1244** WZBbiao / Wibias (52커밋, head `67842aa`) +`src/codex/catalog/sync.ts:543-549` 의 보존 로직이 슬래시 유무로만 라우팅 행을 +인식한다. Desktop 호환 bare native-alias 행을 보존할 수 없다. + +**#1266** `Ingwannu ` +head `c0ffaef643aee3a6b73f93db834cc6e4749b5728` (2026-08-08T06:21:28Z 갱신). +원본 브랜치 `lidge-jun:agent/fix-1254-vertex-thought-signature`. WP4 §040-7, +#1178 바로 뒤에 배치(둘 다 Google/Antigravity 경로). + +> head 이력: 최초 기록은 `ae28b69ef` 였으나 기여자가 갱신했다. 파일 맵은 동일 +> 9개로 유지됐지만, **착수 시점에 SHA를 다시 확인하고 diff를 재확인한다.** +> 이 사례가 라이브 게이트에 SHA 대조 조건을 넣게 만든 계기다(`010` 참조). + +Vertex 경로에서 thought signature가 후속 턴에 재생되지 않는다. 수정 범위는 +`src/adapters/google.ts` 와 신규 `src/adapters/google-antigravity-replay.ts`, +문서 5개 로케일 `reference/adapters.md`, +`structure/04_transports-and-sidecars.md`, 회귀 +`tests/google-vertex-thought-signature.test.ts`. + +활성화 증거: signature를 포함한 응답의 후속 턴에서 재생된 signature가 요청에 +실리는지, signature 없는 응답에서는 기존 동작이 유지되는지 양쪽을 확인한다. + +감사 라운드 5의 라이브 게이트가 잡아낸 항목이다 — 게이트가 의도대로 작동한 사례. + +**#1163** eachann1024 (커밋 `39f677cb`, `99c63dbf`) +Co-authored-by: 关俊江 +`src/codex/catalog/provider-fetch.ts:1276-1284` 이 이미 발견된 멤버만 취하고, +`src/codex/catalog/aggregation.ts:102-115` 이 결측 멤버를 거부하며 `:134-136` 이 +없는 ladder를 빈 배열로 만든다. + +### CI 워크플로 — WP3 + +**#1255** ~~스택 루트~~ — **머지 완료, 조치 없음.** `b73f6a42` 가 `d55b903d8` 로 +dev에 착지했고 그 커밋이 현재 `origin/dev` 다. 이 항목의 재발행 계획은 무효이며 +WP3은 #1259와 #1185 두 독립 PR로 재구성됐다(`030` 문서 참조). + +**#1185** `luvs01 ` (커밋 `bff31d1e`) +`tests/ci-workflows.test.ts:166-169` 의 부분문자열 매칭을 정확한 실행 라인 +어서션으로 바꾼다. `echo` 나 주석이 어서션을 만족시키는 문제를 막는다. +부모가 낡음(`6d04574d`). + +## B. 재작업 필요 + +**#1240** `snowyukitty <270071858+snowyukitty@users.noreply.github.com>` +결함은 실재한다: `src/adapters/openai-chat.ts:961-967` 이 +`JSON.parse(payload) as Record` 로 캐스팅한 뒤 `:972` 에서 +`chunk.error` 를 역참조하므로 `JSON.parse("null")` 이 그대로 도달한다. 동일 결함이 +`src/adapters/google.ts:500-510`, `src/adapters/anthropic.ts:987-995`, +`src/web-search/parse.ts:158-163` 에 있다. + +그러나 **종료 동작이 틀렸다.** 이슈 #1219의 리포터 정정에 따르면 `data: null` 은 +유효 청크 사이에 나타난다. 종료하면 뒤따르는 finish 청크와 `[DONE]` 을 버린다. +OpenAI Chat과 Google의 비레코드 분기를 `return "continue"` 로 바꾸고, 구문적으로 +잘못된 JSON에만 종료 동작을 남긴다. + +**#1259** `luvs01 ` (커밋 `a0810bc3`) +`hygiene` 실패 사유는 코드가 아니다: + +``` +##[error] PR hygiene failed: unsponsored_surface +``` + +`.github/workflows/enforce-pr-target.yml` 이라는 보호된 워크플로 표면을 건드려서 +maintainer 보안 검토와 `maintainer-sponsored` 라벨이 필요하다. 코드 자체는 +정당하다 — 현재 base는 `enforce-pr-target.yml:647-666` 에서 한 페이지만 읽는다. +추가로 #1255와 `tests/helpers/enforce-pr-target-harness.ts` 의 페이지네이션 +로직이 겹치므로 수동 합성이 필요하다. + +## C. 위양성 — close + +**PR #1155** myrosla — 고치려는 경로가 도달 불가. +`src/providers/registry.ts:1318-1326` 이 "bounded-JSON force ... is retired" 를 +명시하고, `providerModelResponsesUpstreamStreaming()`(`:2248-2256`)에 opt-in 하는 +프로덕션 항목이 없다. `src/web-search/loop.ts:364-366` 은 항상 `stream: true`, +`:539-544` 는 항상 `parseStream` 을 바인딩한다. 문서 변경분은 현재 +`docs-site/.../sidecars.md:32-35` 와 모순된다. + +**PR #1119** lidge-jun (maintainer 본인) — 유일한 코드 훅이 낡은 +`tests/codex-catalog.test.ts` 추가인데, 주장하는 #1100 계약이 이미 +`tests/codex-catalog.test.ts:2391-2451` 에 있다. GitHub은 DIRTY로 보고한다. + +**이슈 #1100** — 회귀 커버리지가 `tests/codex-catalog.test.ts:2391-2451` 에 존재하며 +리포터의 커스텀 이름/BigModel Coding Plan 형태도 `:2454-2489` 가 덮는다. 구현 +이력은 `aa8851f38`, `2f242bb7c`, `07e7525b8`. + +**이슈 #1128** — `src/server/responses/compact.ts:651-665` 가 내부 Responses 요청을 +`stream: false` 로 구성하고 `:666-709` 가 JSON을 소비한다. 기원 커밋 `87e1d000b`. + +**이슈 #1102** — 이미 구현·전달. `src/server/index.ts:499-540` 의 선택적 루프백 +리스너, `src/codex/inject.ts:638-649` 의 전환, 실소켓 테스트 +`tests/loopback-listener-integration.test.ts:108-122` 가 public 401 대 loopback 200을 +증명한다. + +> **#1176과 #1024는 여기서 제외됐다.** 초안은 둘을 위양성 close로 분류했으나 +> 감사에서 뒤집혔다. #1176은 maintainer가 2026-08-08T02:15:06Z에 "v2.11.0에 타깃 +> 수정 없음" 을 남기고 열어둔 상태이고, #1024는 계획 자신이 제안한 대조 실험을 +> 수행하기 전이다. 두 건의 현재 처분은 **tracking 유지**이며 근거는 +> `060_wp6_dispositions.md` §060-3에 있다. 이 절에서 close 근거를 찾지 말 것. + +## D. 직접 수정 — WP5 + +**#1219** 네 파서 전부. `openai-chat.ts:961-972`, `google.ts:500-510`, +`anthropic.ts:987-995`, `web-search/parse.ts:158-163`. `unknown` 으로 파싱하고 +속성 접근 전에 비레코드를 거부하되, 유효 JSON 패딩 프레임은 종료가 아니라 건너뛴다. + +**#1245** `gui/src/pages/Startup.tsx:109` 이 갱신된 health를 받지만 `:250` 이 이전 +오류를 유지하고 `gui/src/pages/startup-sections.tsx:146` 이 계속 렌더한다. +`fetchStartup` 에서 `next` 파싱 직후 `status === "protected"` 일 때 실패한 +`installResult` 만 비운다. 회귀: `gui/tests/startup-install-result-reconciliation.test.tsx`. + +**#1236** `bin/ocx.mjs:482-483` 의 최종 Node→Bun launcher spawn에 `windowsHide` 가 +없다. 회귀: `tests/ocx-launcher-source.test.ts:16` 확장. + +**#1230** `src/cli/index.ts:225` 가 `:226` 의 live-proxy 검사보다 먼저 +`reconcileJournal()` 을 호출한다. `handleEnsure`(`:440-441`)도 같은 순서다. 양쪽 다 +PID/liveness 블록 뒤로 이동. 회귀: `tests/cli-start-journal-order.test.ts`. + +**#1196** `.github/scripts/issue-quality-core.cjs:83` 이 media 자식 여부 판정 전에 +모든 들여쓰기 라인을 마스킹하고, `:100-107` 이 라인 위치로 복원하며(멀티라인 HTML이 +접힌 뒤 위험), `:134` 가 정확한 placeholder까지 실질 텍스트로 본다. 토큰 기반 +보호/복원으로 교체. 회귀: `.github/scripts/issue-quality.test.cjs:366,1249` 확장. + +**#1218** — **이슈 종결됨(2026-08-08T03:40:15Z), 실행 대상 아님.** 아래는 기록용 +분석이다. `fa821deb4` 는 null/200k 폴백만 고쳤고 +(`src/claude/model-info.ts:133-139`), `src/codex/catalog/metadata.ts:56-64` 의 +`NATIVE_GPT56_CONTEXT_WINDOW = 372_000` 은 그대로다. 이 값이 틀렸다는 독립 근거가 +나오면 새 이슈로 제기한다. + +**#1213** `src/server/management/agent-settings-routes.ts:838-845` 이 정적 프로파일을 +호출하고 `src/claude/desktop-3p.ts:338-359` 가 전체 모델 목록을 쓴다. +`gui/src/pages/ClaudeDesktop.tsx:477-479` 에 파괴적 교체 확인이 없다. 복원 경로는 +이미 안전해졌다(`native-integration-routes.ts:627-641`, `agent-settings-routes.ts:183-196`). + +**#1229** `src/codex/inject.ts:107-114` 이 `openai_base_url` 만 덮고 +`model_provider = "openai"` 를 유지한다. 전용 프로바이더 호환 모드가 없다. + +**#1145** `src/providers/registry.ts:2023` 키드 Zen 항목에 note가 없다. 프리 티어는 +`:2040-2048` 에 자체 note가 있지만 키드 rate-limit 안내가 아니다. 헤더 관련 주장은 +라이브 429 없이는 미검증 — `src/server/responses/passthrough-error.ts:16-77` 은 유효 +`Retry-After` 를 이미 보존/합성한다. + +**#1059** `.github/workflows/ci.yml:413-438` 이 Windows를 dispatch-only로 둔다. 최근 +실제 디스패치 `31095755263` 은 4개 shard 전부 실패했고, 최신 dev push 런 +`31239522846` 은 Windows를 건너뛰었다. 현재 `ec8ceef` 에서 green은 **미검증**이다. + +## E. tracking 유지 + +| 이슈 | 사유 | 근거 | +|---|---|---| +| #417 | 업스트림 미해결 | `openai/codex#35161` OPEN. 릴레이는 `src/server/live.ts:79-127` 에서 바이트 투명, 회귀 `tests/server-live.test.ts:648-680` | +| #92 | 업스트림 미해결 | `openai/codex#32031` OPEN. dev는 `src/server/responses/core.ts:1560-1564` 로 명시적 실패만 추가 | +| #904 | 재현 캡처 부재 | 릴레이 포렌식 훅 `src/server/live.ts:79-127` 존재, 실패 프레임 조합 미확보 | +| #796 | 라이브 확인 불가 | 호스트 게이트 수정은 `d3abf4345`(`src/adapters/openai-chat.ts:547-582`)로 반영됐으나 회귀 자체가 라이브 Ark 미검증을 명시(`tests/volcengine-ark-assistant-content.test.ts:16-18`) | +| #418 | 동일런 트레이스 부재 | `src/server/responses/collaboration.ts:321-330` 이 개선됐으나 custom-parent→custom-child 트레이스 없음. #92와 별개 | +| #1162 | 정적 증명 불가 | `src/adapters/cursor/live-transport.ts:175-180`, `cursor-errors.ts:83-100` 이 증상은 설명하나 핸드셰이크 원인은 미증명 | +| #1222 | 재현 환경 부재 | Windows 네이티브 크래시. 후보 커밋: `0408dfdd7`(네이티브 프로파일 소유권, 최유력), `254db138c`(PowerShell `execFile` 프로브), `14cc0d421`, `9d271d091`/`8b6f16134`(저확률) | + +### #1222 반증 실험 설계 + +추정으로 패치하지 않는다. 리포터 환경에서 다음을 순서대로 확인한다. + +1. 네이티브 프로파일 상태가 없는 새 `CODEX_HOME` 으로 시작 — 안정적이면 원인을 + 네이티브 메인 소유권/복구로 좁힌다(`src/codex/native-profile-startup.ts:235`). +2. Codex 시작 동기화를 끈다 — 안정적이면 동기화 이후 app-server 프로브로 좁힌다 + (`src/cli/index.ts:380,389` → `src/codex/native-profile-processes.ts:60`). +3. listen 이후 대시보드/클라이언트 트래픽 없이 재현 — 그래도 크래시하면 eager-SSE + 후보를 기각한다. + +재현 후 회귀: Windows 전용 `tests/windows-proxy-start-stability.test.ts` 로 패키지 +launcher를 띄우고 35초 이상 `/healthz` 를 폴링해 단일 안정 Bun 자식 PID를 확인한다. +먼저 red를 만든 뒤 고친다. diff --git a/devlog/_plan/260808_bug_campaign/003_republish_protocol.md b/devlog/_plan/260808_bug_campaign/003_republish_protocol.md new file mode 100644 index 0000000000..08bbe41f49 --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/003_republish_protocol.md @@ -0,0 +1,287 @@ +# 003 — 재발행 프로토콜 (공통 절차) + +모든 재발행 work-phase가 이 절차를 따른다. phase 문서는 이 절차를 다시 쓰지 않고 +대상과 차이점만 기술한다. + +## 원칙 + +원작자의 작업물이다. maintainer는 이를 현재 `dev` 위로 옮겨 착지 가능하게 만들 뿐, +저작을 가져오지 않는다. 따라서 커밋에 `Co-authored-by` 트레일러를 남기고 PR 본문에 +원작자와 원본 PR 번호를 명시한다. + +## 브랜치 명명 + +``` +codex/260808- +``` + +`slug` 는 원본 브랜치의 의미를 유지한다. 예: `codex/260808-history-index-stream-tail`. + +## 절차 + +```bash +# 0. 착수 직전 head SHA를 확보한다 (감사 라운드 7) +REVIEWED_SHA=$(gh pr view --repo lidge-jun/opencodex --json headRefOid --jq .headRefOid) +echo "$REVIEWED_SHA" +# -> 002 문서에 기록된 SHA와 다르면 중단하고 diff를 다시 읽는다 +# -> 이 값을 발행 직전까지 보관한다 (아래 5단계에서 재사용) + +# 1. 원작자 fork를 remote로 확보 (이미 있으면 생략) +git remote add https://github.com//opencodex.git 2>/dev/null || true +git fetch +git rev-parse / # $REVIEWED_SHA 와 일치해야 한다 + +# 2. 최신 dev 확보 +git fetch origin dev +BASE_DEV_SHA=$(git rev-parse origin/dev) +echo "$BASE_DEV_SHA" # 이 값도 발행 직전까지 보관한다 + +# 3. dev 위에 새 브랜치 +git switch -c codex/260808- origin/dev + +# 4. 원본 변경을 적용 (squash로 가져오되 저작은 트레일러로 보존) +git cherry-pick --no-commit ... # 또는 git merge --squash / + +# 5. 충돌 해소 후 **잠정** 로컬 커밋 +# 리베이스를 하려면 커밋이 있어야 하므로 여기서 만든다. +# 아래 발행 루프에서 되돌려지거나 다시 만들어질 수 있다. +git commit +``` + +5단계 커밋은 잠정이다. 이 시점에는 아직 발행하지 않는다. 원격에 나가는 행위 +(push, PR 생성)는 아래 루프를 통과한 뒤에만 일어난다. 기여자 head가 바뀌어 +중단되면 이 로컬 커밋은 버린다. + +## 발행 직전 재확인 (STRICT, 감사 라운드 8) + +0단계의 확인만으로는 부족하다. 충돌을 해소하고 테스트를 돌리는 동안에도 기여자는 +push할 수 있다. 그 사이 로컬 fetch한 ref는 낡은 커밋 그대로이므로, 그대로 발행하면 +**기여자의 최신 작업을 조용히 빠뜨린 재발행**이 된다. + +커밋과 PR 생성 **직전에** 다시 확인한다. + +```bash +MAX_ATTEMPTS=3 +attempt=0 + +while :; do + attempt=$((attempt + 1)) + if [ "$attempt" -gt "$MAX_ATTEMPTS" ]; then + echo "ABORT: base unstable after $MAX_ATTEMPTS attempts" + echo " contributor head: $(gh pr view --repo lidge-jun/opencodex --json headRefOid --jq .headRefOid)" + echo " origin/dev: $(git rev-parse origin/dev)" + # 재계획 대상이다. 이 상태로 발행하지 않는다 + exit 1 + fi + + # (a) 기여자 head — 바뀌었으면 재검토 대상이지 재시도 대상이 아니다 + CURRENT_SHA=$(gh pr view --repo lidge-jun/opencodex --json headRefOid --jq .headRefOid) + if [ "$CURRENT_SHA" != "$REVIEWED_SHA" ]; then + echo "ABORT: head moved $REVIEWED_SHA -> $CURRENT_SHA" + # 새 diff를 읽고 파일 맵/활성화 테스트/보안 범위를 재검토한 뒤 0단계부터 + exit 1 + fi + + # (b) dev — 바뀌었으면 리베이스 후 재검증하고 루프를 다시 돈다 + git fetch origin dev + CURRENT_DEV_SHA=$(git rev-parse origin/dev) + if [ "$CURRENT_DEV_SHA" = "$BASE_DEV_SHA" ]; then + break # 둘 다 안정. 발행 가능 + fi + + echo "dev moved $BASE_DEV_SHA -> $CURRENT_DEV_SHA (attempt $attempt); rebasing" + git rebase --onto origin/dev "$BASE_DEV_SHA" || { + echo "ABORT: rebase conflict against new dev"; exit 1; } + BASE_DEV_SHA="$CURRENT_DEV_SHA" + + # 새 base에서 검증을 처음부터 다시 돌린다 — 이전 결과는 무효다 + bun install + bun run typecheck || exit 1 + bun test tests/<대상>.test.ts || exit 1 + # 이 유닛의 활성화 시나리오도 전부 다시 수집한다 (아래 재수집 규칙 참조) +done + +# 루프를 빠져나온 시점에만 push와 PR 생성을 진행한다. +# (5단계의 잠정 커밋은 이미 있고, 리베이스로 갱신됐을 수 있다. +# 필요하면 여기서 커밋 메시지를 정리한다 — Co-authored-by 트레일러 확인 포함) +``` + +### 루프 설계 근거 + +### 중단 시 로컬 상태 정리 (감사 라운드 12) + +중단(기여자 head 변경, 리베이스 충돌, 3회 소진)이 발생하면 `codex/260808-` +브랜치와 잠정 커밋이 남는다. 그대로 두면 재시작할 때 `git switch -c` 가 같은 +이름으로 브랜치를 만들지 못해 절차가 막힌다. + +**지우지 않는다.** 이름을 바꿔 보관한 뒤 원래 이름을 비운다. 중단 시점의 작업은 +왜 멈췄는지 조사할 근거이며, 특히 기여자 head가 바뀐 경우 우리가 무엇을 +검토했었는지 대조할 기준이 된다. + +```bash +STAMP=$(date +%Y%m%d-%H%M%S) +git switch --detach # 정리 대상 브랜치에서 벗어난다 +git branch -m codex/260808- codex/260808--aborted-$STAMP +``` + +정리는 이름 변경까지다. **여기서 브랜치를 다시 만들지 않는다.** 0단계부터 +절차를 다시 시작하면 3단계가 그때의 `origin/dev` 기준으로 브랜치를 만든다. +정리 단계에서 미리 만들어두면 3단계의 `git switch -c` 가 이름 충돌로 실패하고, +게다가 그 브랜치는 재시작 시점이 아니라 중단 시점의 dev를 가리키게 된다. + +보관된 `*-aborted-*` 브랜치는 로컬에만 둔다. 원격에 push하지 않는다 — 발행되지 +않은 중간 상태이며, 기여자 작업을 낡은 형태로 공개하는 셈이 된다. + +캠페인이 끝난 뒤 정리한다. 그전까지는 각 중단이 왜 일어났는지 기록으로 남는다. + +### 루프 설계 근거 + +두 확인의 처리가 다르다. + +- **기여자 head 변경은 중단이다.** 재시도로 해결되지 않는다. 내용이 달라졌으므로 + 사람이 다시 읽어야 한다. +- **dev 이동은 재시도다.** 우리 변경은 그대로이고 base만 옮기면 되므로 리베이스와 + 재검증으로 흡수된다. + +3회 상한을 두는 이유: dev가 그보다 자주 움직이는 상황이라면 리베이스 경주를 +계속하는 대신 사람이 개입할 시점이다. 상한 소진 시 현재 두 head를 기록하고 +중단하며, 재계획 후 다시 시작한다. + +### 재검증 시 활성화 증거 재수집 범위 + +base가 바뀌면 다음을 **다시 수집한다**: + +- 해당 유닛의 decade 문서에 표로 적힌 활성화 시나리오 전부 +- 특히 "수정 전 red 확인" 이 필요한 항목(가드, 차단, 거부 경로) + +**이월 가능한 것:** 원본 PR의 diff 검토 결과. 기여자 head에 종속되며 dev 이동과 +무관하다. 기여자 head가 바뀌면 무효다. + +**보안 검토는 자동 이월되지 않는다 (감사 라운드 11).** 깨끗한 리베이스라도 +합쳐진 결과물의 보안 경계는 달라질 수 있다. 우리 변경이 그대로여도 dev가 인접 +경로를 바꿨다면 둘의 조합이 새로운 표면을 만든다. 리베이스가 충돌 없이 끝났다는 +것은 텍스트가 겹치지 않았다는 뜻이지 의미가 안전하다는 뜻이 아니다. + +보안 민감 유닛에서 dev가 움직이면, 지명된 검토자가 **최종 리베이스된 diff**를 +다시 확인한다. 깨끗한 리베이스라면 초점을 좁힌 재확인으로 충분하고 전면 +재검토까지는 필요 없지만, 확인 없이 통과시키지는 않는다. + +해당 유닛: + +| 유닛 | 사유 | +|---|---| +| 010-11 (#1260) | 평문 sideband 호스트 검증 — 인증/자격증명 경계 | +| WP3 (#1259) | `.github/workflows/` 보호 표면 | +| 040-3 (#1178) | OAuth 흐름, 아웃바운드 POST 하드닝, 캐시 격리 | + +이 셋은 발행 직전 루프를 돌 때마다 재확인 기록을 남긴다. + +세 확인이 모두 통과해야 PR을 연다. 불일치는 예외 없이 중단 또는 재작업이다. +"거의 같으니 괜찮겠지" 로 넘어가면 이 절차 전체가 무의미해진다. + +**낡은 base에서 나온 검증 결과는 증거가 아니다.** dev가 움직였으면 typecheck와 +테스트를 새 base에서 다시 돌린다. 리베이스만 하고 이전 green을 재사용하는 것이 +이 규칙이 막으려는 행동이다. + +타이밍 요약: + +| 시점 | 확인 대상 | 불일치 시 | +|---|---|---| +| 0단계 (착수 전) | `002` 기록 SHA 대 라이브 head | 중단, diff 재검토 후 처음부터 | +| 2단계 | `BASE_DEV_SHA` 기록 | — (기준값 확보) | +| 발행 직전 | `$REVIEWED_SHA` 대 라이브 head | 중단, 처음부터 재시작 | +| 발행 직전 (루프) | `$BASE_DEV_SHA` 대 현재 `origin/dev` | 재리베이스 + **검증 전량 재실행** + 루프 재진입 (최대 3회) | + +마지막 행이 루프인 이유: 재리베이스와 재검증에도 시간이 걸리므로 그 사이 다시 +움직일 수 있다. 두 SHA가 모두 안정된 상태에서만 발행한다. 리베이스 충돌은 +중단이며 자동 해소를 시도하지 않는다. 3회를 소진하면 현재 두 head를 기록하고 +중단한 뒤 재계획한다 — 무한 재시도는 하지 않는다. + +## 커밋 메시지 형식 + +``` +fix(): <원본 제목의 요지> + +<무엇이 왜 문제였는지 — file:line 근거 포함> + +Supersedes #<원본 PR 번호>. + +Co-authored-by: +``` + +여러 커밋을 합칠 때는 모든 원작자를 각각 트레일러로 나열한다. + +## 확보된 트레일러 문자열 + +아래는 조사 시점의 커밋 저자 정보다. **커밋 SHA는 시간에 따라 바뀌지만 저자 +정보는 대체로 안정적이다.** 그래도 리베이스 직전에 실제 커밋에서 다시 읽어 +확인한다 — 기여자가 head를 갈아끼우면서 저자 정보가 달라질 수 있다. + +조사에서 확인한 커밋 저자 정보다. 그대로 사용한다. + +``` +Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com> +Co-authored-by: luvs01 +Co-authored-by: Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com> +Co-authored-by: TyroneXie <328347833@qq.com> +Co-authored-by: snowyukitty <270071858+snowyukitty@users.noreply.github.com> +Co-authored-by: Myroslav Dosiak +Co-authored-by: 关俊江 +Co-authored-by: xinweigao +Co-authored-by: Xinwei Gao +Co-authored-by: Wibias <37517432+Wibias@users.noreply.github.com> +Co-authored-by: WZBbiao <16611004+WZBbiao@users.noreply.github.com> +``` + +**주의:** luvs01은 커밋에 따라 두 이메일을 쓴다. 원본 커밋의 이메일을 그대로 쓴다. + +## PR 본문 템플릿 + +`.github/PULL_REQUEST_TEMPLATE.md` 의 세 섹션을 전부 채운다. `enforce-target` 이 +빈 설명과 얇은 설명을 거부한다. + +```markdown +## Summary + +Republishes #<원본> by @<원작자> on current `dev`. + +- <무엇을 고치는지> +- <왜 필요한지 — file:line 근거> + +The original branch was commits behind `dev` / blocked on CI approval, so this +carries the same change onto the current head with the author preserved as +co-author. Original PR: #<원본>. + +## Verification + +- `bun run typecheck` +- `bun test tests/<대상>.test.ts` +- <추가 게이트> + +## Checklist + +- [x] Scope stays focused and avoids unrelated cleanup. +- [x] Docs or release notes were updated when needed. +- [x] Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults. +``` + +GUI를 건드리는 PR은 제목이나 본문에 `gui` 가 들어가면 스크린샷이 **필수**다 +(`enforce-target` 이 거부한다). 해당 PR은 #1257, #1244, #1245 수정, #1213 수정이다. + +## 원본 PR 처리 + +재발행 PR이 열린 뒤 원본에 코멘트를 남긴다. 원본을 닫는 것은 재발행이 머지된 +뒤이며, 이번 캠페인에서는 **재발행 PR 생성까지만** 수행하고 원본 close와 머지는 +별도 승인 대상이다. + +## 검증 게이트 + +각 재발행 PR 생성 전 로컬에서: + +```bash +bun run typecheck +bun test tests/<관련>.test.ts +``` + +공유 서브시스템(라우팅, 어댑터, 설정, 서버)을 건드리면 전체 스위트를 돌린다. +GUI 변경은 `bun run lint:gui`, 워크플로 변경은 `bun test tests/ci-workflows.test.ts`. diff --git a/devlog/_plan/260808_bug_campaign/010_wp1_ci_unblock_and_small_republish.md b/devlog/_plan/260808_bug_campaign/010_wp1_ci_unblock_and_small_republish.md new file mode 100644 index 0000000000..67ae573538 --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/010_wp1_ci_unblock_and_small_republish.md @@ -0,0 +1,542 @@ +# 010 — WP1: CI 승인 해제 + 무충돌 소형 13건 재발행 + +선행: WP0(이 문서군). 절차: `003_republish_protocol.md`. + +## 왜 이것이 첫 구현 phase인가 + +CI 승인이 풀리지 않으면 어떤 재발행 PR도 green을 증명할 수 없고, 준비완료 게이트를 +통과할 수 없다. 나머지 모든 phase가 이 결과를 소비한다. + +## 파트 0 — 라이브 갱신 게이트 (필수 선행) + +dev가 캠페인 중에 세 번 움직였다(`ec8ceef00`, `a259d63dc`, `d55b903d8`). PR +#1257은 `db371021c` 로 머지되어 열린 집합에서 빠졌다. 따라서 WP1 착수 직전에 +반드시 다시 확인한다. + +```bash +git fetch origin dev +git rev-parse origin/dev + +# 제목 접두사 AND 라벨 양쪽으로 조회한다 (감사 라운드 3 블로커 1) +gh pr list --repo lidge-jun/opencodex --state open --limit 100 \ + --json number,title,labels,baseRefName,state \ + --jq '.[] | select((.title | test("^fix|^test")) or (.labels | map(.name) | any(. == "bug")))' + +gh run list --repo lidge-jun/opencodex --status action_required --limit 400 \ + --json databaseId,headBranch +``` + +**이슈도 함께 갱신한다 (감사 라운드 4 블로커 2).** PR만 갱신하면 이미 닫힌 +이슈에 대해 코드 작업이나 close 액션을 수행하게 된다. + +```bash +# 재발행 후보의 head SHA를 반드시 확보한다 (감사 라운드 7 블로커 1) +gh pr list --repo lidge-jun/opencodex --state open --limit 100 \ + --json number,title,headRefOid,updatedAt \ + --jq '.[] | "\(.number)\t\(.headRefOid)\t\(.updatedAt)\t\(.title)"' + +# 라벨로 걸러 캠페인 모수와 직접 비교한다 (전체 64건을 훑지 않는다) +gh issue list --repo lidge-jun/opencodex --state open --limit 200 \ + --json number,title,labels \ + --jq '.[] | select(.labels | map(.name) | any(. == "bug" or . == "provider-compatibility")) | "\(.number)\t\(.title)"' + +# WP5/WP6이 건드릴 이슈의 개별 상태를 확인한다 +for n in 1245 1236 1230 1229 1222 1219 1213 1196 1176 1162 1145 1128 1059 1024 904 796 418 417 241 92; do + gh issue view $n --repo lidge-jun/opencodex --json number,state --jq '"#\(.number) \(.state)"' +done +``` + +**통과 조건 (PR과 이슈 모두에 적용):** + +1. 위 라벨 필터 결과를 `001_inventory.md` 의 이슈 표와 대조한다. 표에 없는 + 번호가 하나라도 나오면 **작업을 시작하지 않는다.** 먼저 처분을 배정하고 + `001`과 `002`에 기록한 뒤 진행한다 +2. 상태가 바뀐 이슈(OPEN에서 CLOSED로, 또는 그 반대)도 같은 처리를 한다. + 종결된 이슈에는 코드 작업도 close 액션도 보내지 않는다 +3. 새 PR도 동일하다. 처분 없는 항목이 있는 채로 실행하지 않는다 +4. **CI 상태는 두 조회를 함께 쓴다.** `gh pr checks ` 는 `action_required` 런을 + 표시하지 않으므로, 승인 대기와 런 부재가 구분되지 않는다. 반드시 + `gh api "repos/lidge-jun/opencodex/actions/runs?head_sha="` 로 실제 + 런과 `conclusion` 을 확인한다. green은 **CI 런이 존재하고 결론이 success** 인 + 경우뿐이다 +5. **각 재발행 후보의 `headRefOid` 를 `002` 에 기록된 커밋과 대조한다.** + SHA가 다르면 그 PR의 작업을 **중단하고** 새 diff를 다시 읽는다. 파일 맵, + 활성화 테스트, 보안 범위를 재검토한 뒤에야 진행한다 + +### 왜 SHA 대조가 별도 조건인가 + +제목·라벨·base·state가 모두 그대로여도 기여자가 head를 갱신할 수 있다. 그러면 +우리가 검토한 커밋은 더 이상 그 PR의 내용이 아니다. 낡은 커밋을 리베이스하면 +기여자의 최신 작업을 되돌리는 셈이 된다. + +실제로 감사 라운드 7에서 이 일이 일어났다. #1266의 head가 `ae28b69ef` 에서 +`c0ffaef64` 로 바뀌었고(2026-08-08T06:21:28Z), 다른 게이트 조건은 하나도 변하지 +않아 통과했을 것이다. + +이것은 과거 캠페인에서 학습한 실패 유형이기도 하다. `updatedAt` 은 저자 활동의 +증거가 아니며, 정확한 원격 SHA만이 무엇을 리베이스하는지 확정한다. + +이 조건이 이 게이트의 존재 이유다. 라이브 상태는 계속 움직이므로 문서를 고정된 +시점에 얼려두는 대신, 실행 직전에 차이를 흡수한다. 실제로 감사 중에만 #1263, +#1264, #1265, #1266이 새로 생겼고 #1255, #1257이 머지됐으며 이슈 3건이 닫혔다. + +**이미 종료된 이슈 (2026-08-08 확인):** + +| 이슈 | 상태 | 영향 | +|---|---|---| +| #1100 | CLOSED 02:14:24Z | WP6 close 대상에서 제외. 이미 처리됨 | +| #1102 | CLOSED 02:14:44Z | WP6 close 대상에서 제외. 이미 처리됨 | +| #1218 | CLOSED 03:40:15Z | WP5 050-5 재검토. 외부에서 종결됨 | + +세 건 모두 이 캠페인 밖에서 종결됐다. WP5/WP6의 해당 항목은 실행하지 않는다. +독립적인 코드 근거가 여전히 수정을 요구하는 경우에만 새 이슈로 다시 연다. + +제목만으로 거르면 `bug` 라벨이 붙었지만 제목이 다른 PR을 놓친다. 실제로 감사 +라운드 3에서 이 방식으로 #1264와 #1265를 놓쳤다. + +확인 항목: + +1. 대상 PR이 아직 열려 있는가 (머지·종료된 것은 제외) +2. 새로 열린 버그 PR이 있는가 (있으면 처분 배정 후 진행) +3. `baseRefName` 이 `dev` 인가. `main` 타겟은 릴리스 경로이므로 이 캠페인 + 범위 밖으로 분류한다 (#1265가 그 예) +4. `002` 문서의 file:line 인용이 현재 head에서 유효한가 + +이 게이트를 통과하지 않은 재발행은 무효다. 낡은 base 위에서 리베이스하면 그 +자체가 다시 낡은 PR이 된다. + +**#1257 제외 확정:** `fix(gui): Cursor OAuth accounts stay visible` 는 +2026-08-08T05:50:40Z에 `db371021c` 로 머지됐다. 재발행하지 않는다. + +## 파트 1 — CI 승인 해제 + +열린 PR의 **현재 head SHA**에 속한 `action_required` 런만 승인한다. 전량 승인은 +하지 않는다 — 대부분 이미 머지됐거나 버려진 브랜치다. + +**브랜치 이름으로 고르면 안 된다 (감사 라운드 6).** force-push는 브랜치 이름을 +유지한 채 SHA만 바꾸므로, 브랜치 교집합으로 승인하면 **이미 대체된 커밋의 런을 +승인**하게 된다. 이 캠페인이 막으려는 바로 그 실수다. + +SHA 대조로 선별한다: + +```bash +REPO=lidge-jun/opencodex +LEDGER=/tmp/ocx_approval_ledger.tsv + +for n in <대상 PR 번호들>; do + sha=$(gh pr view "$n" --repo "$REPO" --json headRefOid --jq .headRefOid) + + # 그 SHA의 승인 대기 런만 고른다 + for rid in $(gh api "repos/$REPO/actions/runs?head_sha=$sha" \ + --jq '.workflow_runs[] | select(.conclusion=="action_required") | .id'); do + + # 승인 직전 재확인: 런의 head_sha와 PR의 현재 head를 다시 읽어 대조한다 + run_sha=$(gh api "repos/$REPO/actions/runs/$rid" --jq .head_sha) + now_sha=$(gh pr view "$n" --repo "$REPO" --json headRefOid --jq .headRefOid) + if [ "$run_sha" != "$now_sha" ]; then + printf '%s\t%s\t%s\tSKIP head moved %s -> %s\n' "$rid" "$n" "$run_sha" "$run_sha" "$now_sha" >> "$LEDGER" + continue + fi + + # 응답 상태까지 기록한다 + code=$(gh api --include -X POST "repos/$REPO/actions/runs/$rid/approve" 2>&1 | head -1) + printf '%s\t%s\t%s\t%s\n' "$rid" "$n" "$run_sha" "$code" >> "$LEDGER" + done +done +``` + +조회와 승인 사이에도 기여자가 push할 수 있으므로 재확인이 필수다. 건너뛴 항목도 +사유와 함께 원장에 남긴다. + +**승인 후 확인은 Actions API로 한다.** `gh pr checks` 는 `action_required` 런을 +표시하지 않으므로 전이를 관찰할 수 없다: + +```bash +gh api "repos/$REPO/actions/runs/$rid" --jq '"\(.id) \(.name) status=\(.status) concl=\(.conclusion)"' +``` + +**`status` 와 `conclusion` 은 다른 필드다.** `queued`/`in_progress` 는 `status` +값이고, 그때 `conclusion` 은 `null` 이다. 실제로 승인 직후 Cross-platform CI 런 +`31245339885` 은 `status=queued, conclusion=null` 이었다. + +판정 기준: + +| 관찰 | 의미 | +|---|---| +| `status` 가 `queued` 또는 `in_progress`, `conclusion` 이 `null` | 승인 반영됨, 실행 중 | +| `status=completed`, `conclusion=success` | 통과 (조회 전에 끝난 경우) | +| `status=completed`, `conclusion=action_required` | **아직 미승인** | +| `status=completed`, 그 외 conclusion | 실패한 런 | + +대상 27건: #1264 #1263 #1260 #1259 #1258 #1256 #1249 #1244 #1240 #1235 #1228 +#1226 #1224 #1212 #1210 #1209 #1205 #1202 #1195 #1192 #1189 #1187 #1185 #1184 +#1178 #1169 #1109 #1010. + +(#1257은 머지되어 제외. #1263은 감사 라운드 2, #1264는 라운드 3에서 추가. +#1265는 `main` 타겟이라 이 목록에 없다.) + +수용 기준: 위 PR들의 런이 Actions API 조회에서 `conclusion=action_required` 를 +벗어나 `status` 가 `queued`/`in_progress`(`conclusion=null`)이거나 이미 +`completed`+`success` 인 상태가 되고, `/tmp/ocx_approval_ledger.tsv` 에 런 ID·PR· +head SHA·응답이 기록된다. `gh pr checks` 로는 이 전이를 관찰할 수 없다. + +## 파트 2 — 무충돌 소형 13건 재발행 + +아래 13건은 서로 파일이 겹치지 않는다. 스택이 아니라 각각 `origin/dev` 에서 +분기한 독립 PR이다. + +구성: 010-1 ~ 010-9(초안 9건), 010-10 ~ 010-12(감사 라운드 2 추가), +010-13(감사 라운드 3 추가). + +### 010-1 · #1189 history index stream tail + +원작자 `luvs01 <27862058+luvs01@users.noreply.github.com>` +원본 브랜치 `luvs01:agent/fix-history-index-stream-tail` +원본 커밋 `6e7269d05`, `d5242a231` +새 브랜치 `codex/260808-history-index-stream-tail` + +MODIFY `src/routing/history/indexer.ts` + +현재 `:195-196`: + +```ts +const length = size - fromOffset; +const buffer = Buffer.allocUnsafe(length); +``` + +`:266` 이 미인덱스 tail 전체를 이 경로로 보낸다. 원장이 커지면 시작 시 그 크기만큼 +단일 할당이 일어난다. + +변경: 고정 크기 청크 반복 읽기로 대체하고, 청크 경계에 걸친 부분 라인은 다음 +반복으로 이월한다. 기존의 부분 라인 오프셋 규칙을 유지해야 인덱스 정확도가 보존된다. + +MODIFY `tests/request-history-index.test.ts` — 청크 경계에 라인이 걸치는 픽스처와 +대용량 tail에서 상한 할당이 지켜지는지 확인. + +활성화 증거(C-ACTIVATION-GROUNDING-01): 청크 경계 분할 케이스를 구동하는 테스트가 +실제로 이월 분기를 타는지 확인한다. 단순 green이 아니라 그 분기가 발화해야 한다. + +### 010-2 · #1187 routing analytics malformed attempts + +원작자 `luvs01 <27862058+luvs01@users.noreply.github.com>` +원본 브랜치 `luvs01:agent/fix-routing-analytics-malformed-attempts` +원본 커밋 `8b413ac50` +새 브랜치 `codex/260808-routing-analytics-malformed` + +MODIFY `src/routing/analytics.ts` + +현재 `:153-154`: + +```ts +const attempts = attemptsOf(entry) ?? []; +... attempt.recoveryKinds.some(...) +``` + +`attempts` 가 배열이 아니거나 개별 attempt가 기대 형태가 아니면 throw. 분석 +읽기가 손상된 JSONL 한 줄로 전체 실패한다. + +변경: `Array.isArray` 로 컨테이너를 검증하고, 각 attempt에 대해 `recoveryKinds` 가 +배열인지 확인한 뒤 순회한다. 검증 실패 행은 건너뛰되 나머지 행 처리는 계속한다. + +MODIFY `tests/routing-analytics.test.ts` — 비배열 `attempts`, 비객체 attempt, +`recoveryKinds` 누락 세 케이스. + +활성화 증거: 손상 행이 실제로 skip 분기를 타고, 같은 파일의 정상 행은 여전히 +집계된다는 것을 어서션으로 확인. + +### 010-3 · #1184 command-code own-property lookups + +원작자 `luvs01 ` +원본 브랜치 `luvs01:agent/fix-command-code-own-lookups` +원본 커밋 `cc01ba04e` +새 브랜치 `codex/260808-command-code-own-lookups` + +MODIFY `src/adapters/command-code.ts` — `:321`, `:350` 의 +`COMMAND_CODE_MODEL_ALIASES` 직접 인덱싱 +MODIFY `src/providers/command-code-efforts.ts` — `:34`, `:62` 의 객체 테이블 인덱싱 + +`constructor`, `toString`, `__proto__` 같은 모델 ID가 상속 속성으로 해석되어 +통과하지 못하고 엉뚱한 값을 얻는다. + +변경: 네 지점 모두 `Object.hasOwn(table, key)` 확인 후 접근. + +MODIFY `tests/command-code-provider.test.ts` — `constructor`, `toString`, +`hasOwnProperty` 를 모델 ID로 넣어 통과(pass-through)를 확인. + +활성화 증거: 가드가 없으면 red가 되는 케이스여야 한다. 먼저 가드를 빼고 red를 +확인한 뒤 넣는다. + +### 010-4 · #1258 reasoning-effort trace hydration bound + +원작자 `luvs01 ` +원본 브랜치 `luvs01:agent/fix-reasoning-effort-hydration-bound` +원본 커밋 `1af3b74de` +새 브랜치 `codex/260808-trace-hydration-bound` + +MODIFY `src/routing/trace.ts` + +현재 `:468-474` 가 잘라낸 접두부만 검증한 뒤 `:470` 에서 +`raw.reasoningEfforts.some(...)` 로 전체 영속 배열을 순회한다. 검증 범위와 순회 +범위가 어긋나 있다. + +변경: 보존 대상인 8개 항목만 읽고 검증한다. sparse hole(구멍 뚫린 인덱스)도 거부. + +MODIFY `tests/route-decision-trace.test.ts` — 8개 초과 배열, sparse 배열. + +### 010-5 · #1256 usage startup hydration tail bound + +원작자 `luvs01 ` +원본 브랜치 `luvs01:agent/fix-usage-tail-bound` +원본 커밋 `501178956` +새 브랜치 `codex/260808-usage-tail-bound` + +MODIFY `src/usage/log.ts` + +현재 `:658-664` 는 주석으로 "파일 전체까지" 확장한다고 명시하며 +`Buffer.alloc(size - start)` 를 호출하고, `:671` 이 창을 `size` 까지 키운다. + +변경: 64 MiB 상한을 도입해 그 이상은 읽지 않는다. 상한에 걸리면 가장 최근 +구간만 취한다. + +MODIFY `tests/usage-log.test.ts` — 상한 초과 원장에서 할당이 상한 이하인지 확인. + +활성화 증거: 상한 분기가 실제로 발화하는 크기의 픽스처를 써야 한다. 작은 파일만 +테스트하면 이 분기는 죽은 채로 남는다. + +### 010-6 · #1195 unbound account quota evidence + +원작자 `luvs01 <27862058+luvs01@users.noreply.github.com>` +원본 브랜치 `luvs01:agent/fix-unbound-quota-evidence` +원본 커밋 `e555f7b44`, `6eff3f6a5` +새 브랜치 `codex/260808-unbound-quota-evidence` + +MODIFY `src/router.ts` — `:516-529`(프로세스 활성 Codex 계정 주입), +`:531-533`(활성 Anthropic 계정 주입) +MODIFY `src/server/management/routing-profile-routes.ts` — `:118-135` 의 동일 동작 + +미바인딩 계정에 활성 계정을 대체 주입하면, 쿼터 근거가 없는 상태가 근거 있는 +것처럼 보인다. + +변경: 두 경로 모두에서 대체 주입을 제거하고 명시적 계정 근거만 사용한다. 근거가 +없으면 unknown으로 남긴다. + +MODIFY `tests/quota-scoring.test.ts` — 미바인딩 계정이 unknown으로 남는지, 라이브 +경로와 dry-run 경로가 동일하게 동작하는지. + +### 010-7 · #1202 history lock false positive + +원작자 `Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com>` +원본 브랜치 `Yuxin-Qiao:fix/1191-history-lock-false-positive` +원본 커밋 `d30ad97ab`, `fe1d1e539`, `e9d58d805`, `44b0a04b6` +새 브랜치 `codex/260808-history-lock-false-positive` +해소 이슈 **#1191** + +두 개의 독립 결함이다. + +진단 문구 수렴: `src/codex/inject.ts:1036-1041`, `:1261-1263`, +`src/cli/index.ts:900-903` 이 모든 history 실패를 "DB locked" 로 보고한다. 원인이 +무엇이든 같은 문구가 나와 사용자가 오진한다. + +Windows 경로 동일성: `src/codex/history-lock.ts:184-187` 이 +`realpathSync.native(databasePath) !== databasePath` 일 때 거부하는데, +`src/codex/user-identity.ts:157-163` 이 정규화되지 않은 Windows 루트를 반환한다. +대소문자나 8.3 축약이 다르면 정상 경로가 거부된다. + +변경: 실패 원인을 분리해 각각의 문구로 보고하고, Windows에서는 대소문자 무시 +비교를 사용한다. + +MODIFY `tests/codex-history-job.test.ts`, `tests/codex-user-identity.test.ts` +NEW `tests/codex-inject-history-wording.test.ts` — 현재 dev에 없다. 원본 PR이 +새로 만드는 파일이다. 픽스처: lock이 아닌 실패(권한 거부, 파일 부재, 손상 DB)를 +주입하고 각 문구가 서로 다른지 어서션. + +활성화 증거: lock이 아닌 실패(권한 오류 등)를 주입해 새 문구가 실제로 출력되는지 +확인한다. 전부 green만으로는 문구 분리를 증명하지 못한다. + +### 010-8 · #1169 codex-shim readiness warning + +원작자 `TyroneXie <328347833@qq.com>` +원본 브랜치 `TyroneXie:agent/codex-shim-readiness-warning` +원본 커밋 `d8968b7e6` +새 브랜치 `codex/260808-codex-shim-readiness` + +MODIFY `src/cli/codex-shim-readiness.ts` (NEW), `src/cli/index.ts:1151-1155` + +현재는 `r.installed` 만으로 green을 출력한다. Codex가 실제로 OpenCodex를 경유하는지, +프록시 설정이 백그라운드 실행 후에도 유지되는지 확인하지 않는다. + +변경: 읽기 전용 경고를 추가하되 설치 성공 동작 자체는 유지한다. **프로브가 throw해도 +정상 설치를 실패로 만들면 안 된다** — 원본 리뷰에서 지적된 지점이므로 프로브 전체를 +try/catch로 감싼다. + +NEW `tests/codex-shim-readiness.test.ts` — 현재 dev에 없다. 원본 PR이 새로 만드는 +파일이다. 픽스처: (a) 라우팅이 증명되지 않은 설치에서 경고가 출력되는지, +(b) 프로브가 throw해도 설치가 성공으로 보고되는지. + +### 010-9 · #1192 bounded synthesized SSE expansion + +원작자 `luvs01 <27862058+luvs01@users.noreply.github.com>` +원본 브랜치 `luvs01:agent/fix-bounded-json-sse-expansion` +원본 커밋 `b50f23943` +새 브랜치 `codex/260808-bounded-sse-expansion` + +MODIFY `src/server/responses-json-events.ts` — `:24-38`(항목당 프레임 배열 생성), +`:49-51`(전부 join) +MODIFY `src/server/responses/core.ts` — `:2452` 가 그 전체 본문을 반환 +MODIFY `structure/04_transports-and-sidecars.md` + +출력 항목이 많으면 모든 프레임이 한 문자열로 합쳐져 메모리에 올라간다. + +변경: 항목 수에 상한을 두고 HTTP 프레임을 스트리밍한다. + +**리베이스 주의:** 유일한 충돌이 `structure/04_transports-and-sidecars.md` 의 2줄 +추가다. 현재 dev 문서 텍스트와 새 텍스트를 **양쪽 다 보존**한다. + +MODIFY `tests/responses-json-events.test.ts`, +`tests/deepseek-responses-item-id-repair.test.ts` + +활성화 증거: 항목 수 상한과 스트리밍 경로 둘 다 발화시켜야 한다. 상한 미만 +픽스처만 쓰면 두 분기 모두 죽은 채로 남는다. 상한을 넘기는 출력 항목 수로 +(a) 상한이 적용되어 잘리는지, (b) 프레임이 한 문자열이 아니라 순차 전송되는지 +어서션한다. 후자는 전송 횟수나 청크 경계로 관찰한다. + +## 검증 전 선행조건 + +### 010-10 · #1263 네이티브 프로파일 FIFO 거부 + +원작자 `luvs01 <27862058+luvs01@users.noreply.github.com>` +원본 브랜치 `luvs01:agent/reject-native-profile-fifo` +원본 커밋 `b12244e81` +새 브랜치 `codex/260808-reject-profile-fifo` + +MODIFY `src/codex/native-profile-store.ts` + +프로파일 경로가 FIFO(명명 파이프)를 가리키면 읽기가 블로킹된다. 공격자나 사고로 +FIFO가 놓이면 프록시 시작이 무한 대기한다. 정규 파일이 아닌 경로를 거부한다. + +MODIFY `tests/native-profile-store.test.ts` + +활성화 시나리오: + +| 경로 | 트리거 | 관찰 | +|---|---|---| +| FIFO 거부 | `mkfifo` 로 만든 경로를 프로파일로 지정 | 블로킹 없이 즉시 거부. 테스트가 타임아웃으로 끝나지 않음 | +| 정규 파일 통과 | 일반 프로파일 파일 | 기존과 동일하게 정상 로드 | + +거부 경로가 없으면 테스트 자체가 행에 걸린다. 그것이 이 수정의 존재 이유다. + +### 010-11 · #1260 루프백 sideband 호스트 제한 (보안) + +원작자 `luvs01 <27862058+luvs01@users.noreply.github.com>` +원본 브랜치 `luvs01:agent/fix-live-loopback-host` +원본 커밋 `ed1d72974` +새 브랜치 `codex/260808-loopback-host-validation` + +MODIFY `src/server/live.ts` + +평문 Realtime sideband 예외가 `hostname.startsWith("127.")` 를 썼다. +`http://127.evil.example/v1` 같은 DNS 호스트가 이 검사를 통과한다. 원격 호스트로 +평문 sideband 연결이 만들어진다. + +변경: 127.0.0.0/8 범위의 숫자 IPv4만 허용한다. `localhost` 와 IPv6 루프백은 +유지하고, `127.` 로 시작하는 DNS 이름은 거부해 보안 Realtime 엔드포인트로 +폴백한다. + +MODIFY `tests/server-live.test.ts` + +**보안 검토 대상이다.** AGENTS.md의 인증/자격증명 경계에 해당한다. 이 항목은 +단순 재발행이 아니라 검토자 지정과 검토 기록이 필요하다. + +활성화 시나리오: + +| 경로 | 트리거 | 관찰 | +|---|---|---| +| DNS 우회 차단 | `http://127.evil.example/v1` | 거부되고 보안 엔드포인트로 폴백. 평문 연결 미생성 | +| 숫자 루프백 허용 | `http://127.0.0.1:PORT` | 기존대로 허용 | +| localhost 허용 | `http://localhost:PORT` | 허용 (회귀 없음) | +| IPv6 루프백 허용 | `http://[::1]:PORT` | 허용 (회귀 없음) | + +차단 케이스가 수정 전 코드에서 통과(=취약)했음을 먼저 보인 뒤 고친다. + +### 010-12 · #1210 per-role model fallback 설정 이전 + +원작자 `Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com>` +원본 브랜치 `Yuxin-Qiao:fix/1190-subagent-model-fallback-config` +원본 커밋 5개: `9281c3a5e`, `f666b0241`, `2b6a50f4f`, `7325fbac9`, `6b1ce0cd1` +새 브랜치 `codex/260808-subagent-fallback-config` +해소 이슈 **#1190** + +per-role `model_fallback` 이 Codex 0.146.0의 커스텀 에이전트 TOML 스키마에서 +거부된다. 해당 설정을 opencodex 자체 설정으로 옮겨 Codex가 이해하지 못하는 키를 +TOML에 쓰지 않게 한다. + +MODIFY `src/codex/subagent-model-fallback.ts`, `src/config.ts`, `src/types.ts`, +`src/cli/doctor.ts` +MODIFY `tests/subagent-model-fallback.test.ts` +MODIFY docs 5개 로케일의 `guides/sub-agent-surface.md`, +`reference/configuration/agents.md` + +활성화 시나리오: + +| 경로 | 트리거 | 관찰 | +|---|---|---| +| TOML 청결 | per-role fallback 설정 후 생성된 에이전트 TOML | `model_fallback` 키가 없음. Codex 0.146.0이 수용 | +| 폴백 동작 유지 | 1차 모델 실패 주입 | opencodex 설정에서 읽은 폴백 모델로 전환 | +| doctor 진단 | 낡은 TOML에 키가 남아 있는 상태 | `ocx doctor` 가 감지하고 안내 | + +`src/config.ts` 와 `src/types.ts` 를 건드리므로 PLAN-FIELD-CHAIN-01 적용: 새 설정 +필드의 생성(설정 파싱), 직렬화, 역직렬화(미지 값 처리), 소비자(4곳) 전 체인을 +리베이스 시 확인한다. + +### 010-13 · #1264 null Claude 토글 본문 거부 + +원작자 `luvs01 <27862058+luvs01@users.noreply.github.com>` +원본 브랜치 `luvs01:agent/fix-claude-null-toggle-body` +원본 커밋 `c85d792d8` +새 브랜치 `codex/260808-claude-null-toggle-body` + +MODIFY `src/server/management/native-integration-routes.ts` + +Claude 토글 관리 엔드포인트가 `null` 본문을 받으면 역참조 크래시가 난다. +`JSON.parse("null")` 이 예외 없이 `null` 을 돌려주는 것과 같은 계열의 결함이다 +(WP2의 #1219와 원인 구조가 동일하지만 파일과 경로가 달라 독립 처리). + +MODIFY `tests/native-claude-code-toggle.test.ts` + +활성화 시나리오: + +| 경로 | 트리거 | 관찰 | +|---|---|---| +| null 본문 거부 | 요청 본문에 리터럴 `null` | 400류 응답. 크래시 없음 | +| 비객체 본문 거부 | 배열이나 스칼라 본문 | 동일하게 거부 | +| 정상 본문 | 유효 토글 객체 | 기존과 동일 동작 (회귀 없음) | + +수정 전 코드에서 null 본문이 크래시를 내는지 먼저 확인한다. + +## 검증 전 선행조건 + +이 체크아웃에서 `bun run typecheck` 가 `bun-types` 부재로 exit 1이다(감사 실측, +0.808초). 각 재발행 브랜치에서 검증 명령을 돌리기 전에: + +```bash +bun install +``` + +를 먼저 실행한다. 이것 없이 나온 typecheck 결과는 증거가 아니다. + +## WP1 수용 기준 + +- 27개 PR의 CI 런이 승인되어 실행 상태로 전환 +- 13개 재발행 PR이 열리고 각각 `Co-authored-by` 트레일러 보유 +- 각 재발행 PR에 **두 번의 SHA 확인 기록**이 남는다: 착수 전(`002` 기록 대조)과 + 발행 직전(`$REVIEWED_SHA` 재대조). 절차는 `003` 문서의 타이밍 표 참조 +- 각 PR에서 `bun install` 후 `bun run typecheck` exit 0 +- 각 PR의 대상 테스트 파일 green +- 조건부 분기를 추가한 항목은 해당 분기가 발화하는 증거 확보: + 010-1(청크 이월), 010-2(손상 행 skip), 010-3(프로토타입 키 가드), + 010-5(64 MiB 상한), 010-7(lock 아닌 실패 문구), 010-9(항목 상한·스트리밍), + **010-10(FIFO 거부)**, **010-11(DNS 우회 차단)**, **010-12(TOML 청결·폴백·doctor)**, + **010-13(null 본문 거부)** +- **#1260(010-11)은 보안 검토 완료 전 PR을 열지 않는다.** 지명된 검토자와 검토 + 기록이 선행 조건이다. 평문 sideband 호스트 검증은 AGENTS.md의 인증/자격증명 + 경계에 해당한다 diff --git a/devlog/_plan/260808_bug_campaign/011_wp1_gate_run.md b/devlog/_plan/260808_bug_campaign/011_wp1_gate_run.md new file mode 100644 index 0000000000..6da20913cf --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/011_wp1_gate_run.md @@ -0,0 +1,163 @@ +# 011 — WP1 라이브 게이트 실행 기록 (2026-08-08) + +`010` 문서의 파트 0을 실제로 돌린 결과다. 게이트가 설계대로 작동했고, 그 결과 +계획이 두 군데 바뀌었다. + +## 실행 시점 상태 + +``` +origin/dev = fdc47db7bf0b9f6d6f4ef1a09eb6e1f3e0680c63 +``` + +`002` 문서의 조사 base(`ec8ceef00`)에서 세 번 더 이동했다. + +## SHA 대조 결과 + +| PR | 기록된 커밋 | 라이브 head | 판정 | +|---|---|---|---| +| #1263 | `b12244e81` | `61ec1491d` | **변경 — 재검토** | +| #1260 | `ed1d72974` | `d8c72ee5d` | **변경 — 재검토** | +| #1240 | `f155138c` | `965dd9901` | **변경 — 재검토** | +| #1266 | `c0ffaef64` | `c0ffaef64` | 통과 | +| #1264 | `c85d792d8` | `c85d792d8` | 통과 | +| #1189 | `d5242a231` | `d5242a231` | 통과 | +| #1187 | `8b413ac50` | `8b413ac50` | 통과 | +| #1184 | `cc01ba04e` | `cc01ba04e` | 통과 | + +세 건이 걸렸다. 제목·라벨·base·state는 전부 그대로였으므로, SHA 검사가 없었다면 +낡은 커밋을 리베이스했을 것이다. 감사 라운드 7이 이 검사를 요구한 이유가 실제로 +입증됐다. + +## CI 승인 (파트 1) — 완료 (집계만, 감사 미검증) + +실행 기록에 따르면 열린 PR의 head에 해당하는 런만 골라 승인했다. 아래 수치는 +**그 기록이 보고하는 값이며 검증되지 않았다**(사유는 다음 블록). + +- 수집 시각: 2026-08-08 (정확한 타임스탬프 미기록) +- `action_required` 총계: 257런 / 28 PR — *미검증* +- 그중 현재 head의 런만 선별: 71런 — *미검증* +- 승인 결과: 71건 성공, 실패 0 — *미검증* + +기록상 오래된 head의 런 186건은 승인하지 않았다. 이미 무의미한 커밋을 검증하는 +데 CI 자원을 쓸 이유가 없다. + +> **증거 한계 (감사 지적).** 위 수치는 실행 당시의 집계이며 **재구성이 +> 불가능하다.** 런 ID 목록, 개별 승인 응답, 승인 시점의 head SHA를 남기지 +> 않았기 때문이다. 승인 자체가 상태를 바꾸므로 사후에 같은 조회를 해도 같은 +> 결과가 나오지 않는다(감사 시점 재조회는 688런을 반환했고 그중 현재 head와 +> 일치하는 것은 없었다). +> +> 결론적으로 "현재 head만 승인했다" 는 주장은 이 문서로 감사되지 않는다. +> 위 집계는 **미검증 보고치**로 취급한다. 아래 관찰만이 독립 확인된다: +> 승인 직후 대상 PR들의 체크가 `action_required` 에서 `pending` 으로 전환됐고, +> 현재 열린 PR 중 승인 대기로 막힌 것은 없다. +> +> **향후 절차:** 승인 작업은 런 ID, PR 번호, 그 시점 head SHA, 응답 코드, +> 전후 상태를 줄 단위로 남긴다. 상태 변경 행위는 실행 중에 기록하지 않으면 +> 사후 재구성이 불가능하다. + +승인 직후 대상 PR들의 체크가 `action_required` 에서 `pending` 으로 전환된 것을 +확인했다(#1189, #1187, #1184, #1258, #1256 표본). + +## 재검토 결과 (변경된 3건) + +### #1263 — ADOPT-WITH-TEST-FIX + +재검토 시점 head `61ec1491dc85e7cf0f89dc8ddefa8141746b499a` +**감사 시점 head `7c3fa541` — 또 이동했다(2026-08-08T07:10:22Z). 착수 전 재검토 필수.** +`Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>` + +**좁은 범위의 POSIX 경쟁 안전성 동작 변경이다.** 초안은 "동작 변화 없음" 이라고 +적었으나 틀렸다 — `O_NONBLOCK` 추가가 writer 없는 FIFO에서 `openSync` 블로킹을 +없애는 것이 이 패치의 요점이다. + +결함은 dev에 그대로다. 다만 **트리거는 사전 배치된 FIFO가 아니라 TOCTOU +경쟁이다.** `src/codex/native-profile-store.ts:384-389` 가 `O_RDONLY | O_NOFOLLOW` +로 연 뒤에야 `fstat` 을 확인하므로, 상위 검증을 통과한 정규 파일이 `openSync` +직전에 FIFO로 바뀌면 블로킹된다. 대조 실험에서 패치 없는 dev는 타임아웃, +패치본은 3ms에 `VAULT_INVALID` 였다. 상세는 `012` 의 "정정 실험". + +**처분: ADOPT-WITH-TEST-FIX.** 코드는 유효하고 테스트만 트리거를 잘못 잡았다. + +현재 테스트(`tests/native-profile-store.test.ts:386-412`)는 **무효다.** FIFO를 +호출 전에 만들어 상위 가드에 먼저 걸리므로 `PROFILE_STORAGE_UNSAFE` 가 나오고, +`VAULT_INVALID` 를 기대해 red다. 패치가 고치는 경로를 통과하지 못한다. +감사 시점 head `7c3fa541` 도 이 낡은 테스트를 그대로 갖고 있다. + +**머지 차단:** 대체 테스트가 들어오기 전에는 머지하지 않는다. 설계는 `012` 참조. + +### #1260 — ADOPT-AS-IS (보안 검토 선행) + +head `d8c72ee5d8e5ddeb036e5b781771e99a848dc113` +`Co-authored-by: luvs01 <27862058+luvs01@users.noreply.github.com>` + +동작 변화 없음. 검증 로직과 테스트가 그대로다. + +결함은 dev에 그대로다. `src/server/live.ts:208-212` 의 `isLoopbackHost` 가 +`127.` 로 시작하는 모든 호스트명을 받아들이고, `:239-242` 가 그 판정으로 평문을 +허용한다. + +수정본은 `:208-217` 에서 네 개의 숫자 옥텟만 허용하도록 바꾼다. + +**보안 경계 평가** (감사가 요구한 우회 시나리오 전수): + +| 입력 | 결과 | +|---|---| +| `127.evil.example` | 거부 — 숫자 4옥텟 아님 | +| `127.0.0.1.evil.example` | 거부 | +| `localhost`, `[::1]` | 허용 유지 (회귀 없음) | +| `[::ffff:127.0.0.1]` | 거부 — Bun이 `[::ffff:7f00:1]` 로 정규화 | +| `0177.0.0.1`, `2130706433`, `0x7f000001` | 허용되나 `new URL()` 이 `127.0.0.1` 로 정규화 — 실제 루프백 | +| `127.0.0.1.` | `127.0.0.1` 로 정규화 — 루프백 | +| `localhost.` | 거부 (fail-closed) | +| userinfo (`user:pw@`) | `:241-242` 가 별도로 거부 | + +요청한 우회 경로에서 취약점 없음. `*.localhost` 허용의 환경별 해석 의미는 +독립 검증하지 못했다(기존 동작이며 이 PR이 도입한 것 아님). + +`MAINTAINERS.md` 가 요구하는 명시적 보안 검토는 여전히 선행 조건이다. + +### #1240 — ADOPT-WITH-CHANGES (계획 변경) + +head `965dd990114fc6203297475142a28fcd7cb44642` +`Co-authored-by: snowyukitty <270071858+snowyukitty@users.noreply.github.com>` + +**이것이 이번 게이트 실행의 가장 중요한 발견이다.** + +`020` 문서는 #1240을 "재작업 필요" 로 분류하고, 우리가 직접 `continue` 동작으로 +다시 구현하는 계획(§020-1)을 세웠다. 근거는 이전 head가 비레코드 프레임에서 +스트림을 종료시켜 후속 finish 청크와 `[DONE]` 을 버린다는 것이었다. + +**작성자가 이미 고쳤다.** 새 head는 `src/adapters/openai-chat.ts:978-980` 과 +`src/adapters/google.ts:513-515` 에서 정확히 우리가 요구한 형태로 바뀌었다: + +```ts +if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { + return "continue"; +} +``` + +Google은 `sawAnyFrame = true`(`:517`) **이전에** 처리해 빈 스트림 실패 가드를 +보존한다. 새 테스트가 유효 청크 사이의 `data: null` 스트림을 만들어 `PONG` 턴이 +완주하는지 확인하고(`tests/sse-null-data-frame.test.ts:55-62`, `:101-107`), +전부 비레코드인 스트림은 여전히 fail-closed 임을 별도로 증명한다(`:109-116`). + +작성자는 코멘트에서 이전 종료 동작이 잘못이었음을 명시적으로 인정하고 수정 경위를 +설명했다. + +**계획 변경:** `020` §020-1의 직접 재구현은 **불필요하다.** 우리가 처음부터 다시 +짜는 대신 이 PR을 채택한다. 코드 재작업 없음. + +> 초안은 "PR 본문의 낡은 설명을 고쳐달라" 고 요청하려 했으나, 감사에서 확인한 +> 결과 **본문도 이미 갱신되어 있다.** 현재 본문은 OpenAI Chat과 Google이 +> 비레코드 프레임에서 `continue` 를 반환하고 전량 비레코드 스트림은 여전히 +> fail-closed 임을 명시한다. 요청할 것이 없다. + +#1219는 이 PR 착지로 해소된다. + +## 이 실행이 남긴 교훈 + +기여자가 우리 리뷰를 읽고 스스로 고치는 경우가 있다. 낡은 판정을 근거로 +"재작업" 을 강행했다면 이미 올바르게 고쳐진 작업을 우리 것으로 다시 만드는 +셈이었다. SHA 검사는 우리를 낡은 코드로부터 지킬 뿐 아니라, 기여자의 최신 +작업을 존중하게 만든다. diff --git a/devlog/_plan/260808_bug_campaign/012_wp1_ci_first_results.md b/devlog/_plan/260808_bug_campaign/012_wp1_ci_first_results.md new file mode 100644 index 0000000000..e37f7bb290 --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/012_wp1_ci_first_results.md @@ -0,0 +1,223 @@ +# 012 — WP1 CI 첫 결과와 #1263 진단 + +`011` 의 실행 기록이 보고한 71개 런 승인 이후의 첫 결과다. 승인 집계 자체는 +미검증이나(사유는 `011`), 아래 CI 결과는 각 PR의 체크에서 직접 관찰한 것이다. + +## 결과 분류 + +| PR | 결과 | 원인 | +|---|---|---| +| #1264 | **PASS** | — | +| #1263 | **FAIL** | macOS 테스트 실패 — 아래 진단 | +| #1205 | FAIL | `hygiene: unsponsored_surface` — 라벨 필요 | +| #1178 | FAIL | `hygiene: unsponsored_surface` — 라벨 필요 | +| #1163 | FAIL | `react-doctor` — 로그 만료(BlobNotFound), 재실행 필요 | +| 나머지 | pending | 진행 중 | + +`unsponsored_surface` 두 건은 코드 결함이 아니다. 보호된 표면을 건드리는 PR에 +maintainer 보안 검토와 `maintainer-sponsored` 라벨이 필요하다는 게이트의 정상 +동작이며, `002`/`040`이 이미 선행 조건으로 기록한 사항이다. + +## #1263 진단 — **정정됨: 우리가 틀렸다** + +> **이 절의 결론은 감사에서 뒤집혔다.** 아래 원래 분석은 사전 배치된 FIFO만 +> 시험했고, 그 조건에서는 실제로 상위 가드가 0ms에 거부한다. 그러나 이 결함은 +> **TOCTOU 경쟁**이다. 검증을 통과한 정규 파일이 `openSync` 직전에 FIFO로 +> 바뀌면 상위 가드는 이미 지나간 뒤다. +> +> 대조 실험으로 확증했다(아래 "정정 실험" 참조). **패치 없는 dev는 무한 +> 블로킹(타임아웃 exit 124), 패치본은 3ms에 `VAULT_INVALID`.** +> +> **정정된 처분: ADOPT-WITH-TEST-FIX.** 패치는 실재하는 경쟁을 고친다. 틀린 것은 +> 기여자의 **테스트**이며, FIFO를 너무 일찍 만들어 상위 가드에 걸리는 바람에 +> 정작 패치가 고치는 경로를 통과하지 못한다. + +### 정정 실험 (2026-08-08) + +`readBounded` 는 `beforeOpen` 테스트 시임(`src/codex/native-profile-store.ts:69-77`, +`:383`)을 갖고 있다. 이 시임은 "검증 이후, open 이전" 시점에 개입하기 위해 +코드베이스가 의도적으로 둔 것이다. 정확히 TOCTOU를 재현하는 도구다. + +프로브: 정규 vault 파일을 만들어 `assertNativeProfileMetadataLayout`(`:329-345`)을 +통과시킨 뒤, 시임에서 그 파일을 지우고 같은 경로에 `mkfifo` 한다. + +| 대상 | 결과 | +|---|---| +| `origin/dev@fdc47db7b` (패치 없음) | **행 — 20초 타임아웃 (exit 124)** | +| `luvs01/agent/reject-native-profile-fifo` (패치본) | `threw VAULT_INVALID after 3 ms` | + +이것이 활성화 증거다. 패치가 추가한 `O_NONBLOCK` 분기가 실제로 발화하며, 없을 +때와 있을 때의 관찰 가능한 차이가 명확하다. + +### 왜 처음에 틀렸나 + +사전 배치된 FIFO만 시험했다. 그 경로에서는 `assertCanonicalFile`(`:257-260`)이 +먼저 거부하므로 "결함 없음" 으로 보였고, 그 관찰 자체는 정확했다. 오류는 그 +한 가지 트리거로 전체 결함 부재를 결론지은 것이다. + +`readBounded` 를 우회하는 호출 경로만 찾았고, **검증과 open 사이의 시간 창**은 +보지 못했다. 감사자가 시임의 존재를 근거로 그 창을 지목했다. + +교훈: "이 분기가 발화하는 시나리오가 없다" 는 결론은 시나리오를 한 종류만 +시험했을 때 내릴 수 없다. 특히 코드베이스가 그 분기를 위한 테스트 시임을 +제공하고 있다면, 그 시임이 곧 트리거 설계도다. + +### 기여자에게 요청할 것 + +테스트만 고치면 된다. 현재 테스트(`tests/native-profile-store.test.ts:386-412`)는 +FIFO를 미리 만들어 상위 가드에 걸리므로 `PROFILE_STORAGE_UNSAFE` 가 나오고, +`VAULT_INVALID` 를 기대해 red다. + +제안하는 대체 테스트 설계는 아래와 같다. **자식 프로세스 격리와 부모 타임아웃이 +필수다** — 패치 없는 코드는 행에 걸리므로 인프로세스로 돌리면 테스트 러너 전체가 +멈춘다. + +부모가 자식에게 넘길 것 (격리 필수): + +부모가 `mkdtempSync` 로 임시 루트를 만들고 그 안에 `codexHome` 과 `configDir` 를 +생성한 뒤, 전용 환경변수(예: `OCX_FIFO_FIXTURE`)에 JSON으로 넘긴다. 자식이 기본 +환경 경로로 폴백하면 사용자의 실제 `CODEX_HOME` 을 건드릴 수 있으므로 **경로를 +넘기지 않는 형태는 허용하지 않는다.** + +자식이 하는 일: + +1. `native-profile-store` 모듈을 import +2. `OCX_FIFO_FIXTURE` 를 파싱해 `resolveNativeProfileContext({ codexHome, configDir })` + 를 호출하고 `rootDir` 생성 (환경변수가 없으면 구분 가능한 코드로 즉시 종료) +3. **정규 파일**로 vault를 쓴다 (`mode: 0o600`) — 상위 검증을 통과시키기 위함 +4. 심볼 키 `Symbol.for("opencodex.native-profile-store.bounded-read-test-seam")` + 로 `beforeOpen` 시임을 컨텍스트에 설치한다. 시임 본문에서 경로가 vaultPath일 + 때 `unlinkSync` 후 `execFileSync("mkfifo", [vaultPath])` +5. `readNativeProfileVault(ctx)` 호출 +6. `VAULT_INVALID` 를 잡으면 정상 종료(exit 0), 아니면 구분 가능한 non-zero + +부모가 하는 일: + +- `spawnSync` 에 `timeout` 을 준다 (2초면 충분 — 패치본은 3ms대) +- `child.error` 가 undefined, `child.signal` 이 null, `child.status` 가 0 인지 확인 +- 실패 시 어서션 메시지에 `child.stderr` 를 포함한다. 그러지 않으면 자식이 왜 + 죽었는지 알 수 없어 디버깅이 불가능하다 + +이 형태여야 패치 없이 **타임아웃으로 red** 가 된다. 시그널/타임아웃을 확인하지 +않으면 "행에 걸렸다" 와 "빠르게 거부했다" 를 구분하지 못한다. + +플랫폼: `test.skipIf(process.platform === "win32")` — `mkfifo` 는 POSIX 전용이다. + +
+원래 분석 (사전 배치 FIFO만 시험 — 결론 무효) + +### 증상 + +`tests/native-profile-store.test.ts:412` 에서 자식 프로세스 exit code가 0이 아닌 +**92**. 92는 테스트가 심어둔 값으로 "던져진 오류의 `code` 가 `VAULT_INVALID` 가 +아니다" 를 뜻한다. + +로컬(macOS)에서 동일하게 재현했다. CI만의 환경 문제가 아니다. + +### 실제로 던져지는 것 + +FIFO를 vault 경로로 두고 `readNativeProfileVault` 를 직접 호출한 결과: + +``` +code=PROFILE_STORAGE_UNSAFE name=NativeProfileError +msg=Native-profile storage could not be inspected safely. +threw PROFILE_STORAGE_UNSAFE after 1ms +``` + +FIFO는 정확히 거부된다. 다만 코드가 다르다. + +### 결정적 확인 — 패치 없는 dev에서도 막힌다 + +별도 워크트리에 `origin/dev@fdc47db7b`(패치 미포함)를 체크아웃해 같은 프로브를 +돌렸다: + +``` +== origin/dev baseline (no patch) == +threw PROFILE_STORAGE_UNSAFE after 0ms +``` + +**0ms.** 블로킹이 없다. 즉 이 PR이 고치려는 "writer 없는 FIFO를 열다가 startup이 +멈춘다" 는 상황이 현재 dev에 존재하지 않는다. + +### 왜 막히는가 + +`readNativeProfileVault`(`src/codex/native-profile-store.ts:703`)는 +`readBounded`(`:379`)의 `openSync` 에 닿기 전에 상위 경로 검증을 거친다. +`assertCanonicalFile`(`:257`)이 `lstatSync` 후 `:260` 에서 + +```ts +if (!entry.isFile() || entry.isSymbolicLink()) storageUnsafe(`${label} is not a private regular file.`); +``` + +로 FIFO를 걸러낸다. FIFO는 `isFile()` 이 false이므로 여기서 끝난다. `openSync` 는 +호출되지 않으므로 `O_NONBLOCK` 을 더할 대상 자체가 실행되지 않는다. + +### 판정 + +PR의 `O_NONBLOCK` 추가는 심층 방어로는 무해하다. `readBounded` 가 다른 경로에서 +직접 불릴 가능성에 대비한다고 볼 수 있다. 그러나: + +1. 주장하는 결함이 현재 dev에 없다 — 0ms 거부 +2. 테스트가 틀린 코드(`VAULT_INVALID`)를 기대해 red다 +3. 테스트를 `PROFILE_STORAGE_UNSAFE` 로 고치면 통과하겠지만, 그때 그 테스트는 + **패치 없이도 통과한다**. 즉 패치를 검증하지 않는 테스트가 된다 + +3번이 핵심이다. C-ACTIVATION-GROUNDING-01 기준으로 이 변경은 활성화 증거를 만들 +수 없다. 추가한 분기가 발화하는 시나리오가 없기 때문이다. + +**(무효) 처분: NEEDS-REWORK.** 기여자에게 위 baseline 측정(0ms, `PROFILE_STORAGE_UNSAFE`)을 +공유하고, `readBounded` 가 상위 검증을 우회해 호출되는 실제 경로를 제시할 수 있는지 +묻는다. 그런 경로가 있다면 그것이 진짜 결함이고 테스트도 그 경로를 타야 한다. +없다면 이 PR은 닫는 것이 맞다. + +추정으로 테스트만 고쳐 green을 만들지 않는다. 그것은 아무것도 검증하지 않는 +테스트를 dev에 넣는 일이다. + +
+ +> 위 마지막 문단은 여전히 옳다. 다만 적용 방향이 반대다. 테스트를 +> `PROFILE_STORAGE_UNSAFE` 로 바꾸는 것이 "아무것도 검증하지 않는 테스트" 이고, +> 시임 기반 경쟁 재현이 진짜 검증이다. + +## CI 상태 판독 주의 (2026-08-08 관찰) + +`gh pr checks` 가 "실패 없음" 을 보인다고 통과가 아니다. #1263이 그 예다. + +head `7c3fa5419` 에 대해 `gh pr checks` 는 `CodeRabbit / hygiene / label` 세 개만 +보여주고 전부 pass다. 그래서 집계 스크립트는 PASS로 분류한다. + +**초안은 여기서 "워크플로가 아직 시작되지 않았다" 고 적었다. 틀렸다.** 런은 +존재하며 승인 대기 상태다. REST API로 조회하면 드러난다: + +``` +$ gh api "repos/lidge-jun/opencodex/actions/runs?head_sha=7c3fa5419268933392452fc16f5fec371907107a" +31245339885 Cross-platform CI status=completed conclusion=action_required +31245339913 React Doctor status=completed conclusion=action_required +31245338833 PR hygiene status=completed conclusion=success +31245338824 PR Labeler status=completed conclusion=success +``` + +즉 `gh pr checks` 는 `action_required` 런을 **아예 표시하지 않는다.** 승인 대기와 +런 부재가 그 출력에서 구분되지 않으며, 둘 다 "그냥 없음" 으로 보인다. + +동시에 그 head의 diff는 여전히 낡은 early-FIFO 테스트를 갖고 있다(감사 확인). +이 PR은 "테스트가 고쳐져서 통과" 도 "워크플로 미시작" 도 아니고, **새 head의 +CI가 승인 대기로 막혀 있는** 상태다. + +**판독 규칙 (정정):** 두 조회를 모두 쓴다. + +1. `gh pr checks ` — 표시되는 체크의 결론 +2. `gh api "repos/OWNER/REPO/actions/runs?head_sha="` — 실제 런 목록과 + `conclusion` (여기서만 `action_required` 가 보인다) + +green으로 인정하는 조건은 **CI 런이 존재하고 그 결론이 success** 인 경우뿐이다. +런 부재도, `action_required` 도 green이 아니다. head가 바뀔 때마다 그 head의 +런을 새로 승인해야 한다는 점도 이 관찰이 확인해 준다. + +## 이 사이클이 확인해 준 것 + +CI 승인은 단순한 사무 처리가 아니었다. 승인하자마자 실제 결함 하나(#1263 — +유효한 경쟁 수정이되 최초 회귀 테스트가 무효)와 절차 요구사항 둘(#1205·#1178 +보안 라벨)이 드러났다. +승인이 막혀 있는 동안에는 이 PR들이 "검증되지 않은 상태" 로 draft에 갇혀 있었고, +무엇이 문제인지 알 방법도 없었다. diff --git a/devlog/_plan/260808_bug_campaign/013_wp1_new_prs_overlap.md b/devlog/_plan/260808_bug_campaign/013_wp1_new_prs_overlap.md new file mode 100644 index 0000000000..04c10fff09 --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/013_wp1_new_prs_overlap.md @@ -0,0 +1,90 @@ +# 013 — 신규 PR과 WP5 계획의 중복 (게이트 2차 실행) + +게이트를 다시 돌린 결과 신규 PR 3건이 잡혔다. 그중 둘이 **우리 WP5 계획과 같은 +파일을 고친다.** 계획 변경이 필요하다. + +## 신규 항목 + +| PR | 제목 | 작성자 | head | 겹치는 계획 | +|---|---|---|---|---| +| #1269 | check live proxy before journal recovery | Ingwannu | `8b7831ead` | **050-3 (#1230)** | +| #1268 | hide npm launcher proxy child | Ingwannu | `4c40c569d` | **050-2 (#1236)** | +| #1266 | replay Vertex thought signatures | Ingwannu | `c0ffaef64` | 040-7 (이미 배정) | + +## #1268 — 우리 계획과 실질적으로 동일. 채택 + +`bin/ocx.mjs` 의 최종 Node→Bun spawn에 `windowsHide: true` 를 추가한다. 우리 +050-2가 계획한 것과 같은 한 줄이며, 주석으로 헤드리스 부모(Task Scheduler, +대시보드 재시작, 바로가기)에 상속할 콘솔이 없다는 점까지 설명한다. + +테스트도 우리 계획과 같은 접근이다 — `tests/ocx-launcher-source.test.ts` 에서 +최종 spawn 호출을 잘라내어 `windowsHide: true` 가 그 옵션 객체 안에 있는지 +확인한다. 다른 헬퍼 spawn과 혼동하지 않도록 범위를 좁힌 것도 동일하다. + +**처분: 050-2 폐기, #1268 채택.** 우리가 다시 만들 이유가 없다. + +## #1269 — 절반만 고친다. 보완 필요 + +`handleStart` 의 순서는 정확히 우리 계획대로 고친다. `reconcileJournal()` 을 +PID/liveness 블록 **뒤로** 옮겨, 경쟁에서 진 start가 살아있는 프록시의 Codex +설정을 되돌리지 못하게 한다. + +**그러나 `handleEnsure` 를 건드리지 않는다.** diff에서 `handleEnsure` 는 테스트의 +범위 지정용으로 한 번 언급될 뿐이다. + +현재 dev의 `src/cli/index.ts` 를 보면 같은 결함이 남아 있다: + +``` +440:async function handleEnsure(...) +441: if (!currentExternalCodexModelProvider()) reconcileJournal(); +... +447: const live = await findLiveProxy(); +``` + +`:441` 이 `:447` 의 liveness 확인보다 먼저다. 우리 050-3 문서가 이미 지적한 +지점이며, autostart 경로가 같은 파괴적 순서를 유지한다는 뜻이다. + +이슈 #1230은 "동시 `ocx start`" 를 제목으로 달았지만 `ocx ensure` 도 같은 코드 +경로 문제를 공유한다. `handleStart` 만 고치면 이슈의 절반만 닫힌다. + +**처분: #1269 ADOPT-WITH-CHANGES.** 채택하되 `handleEnsure` 보완을 요청한다. +근거는 위 라인 인용이다. 기여자가 원하지 않으면 후속 PR로 우리가 처리하되, +어느 쪽이든 **#1230은 두 함수가 모두 고쳐지기 전까지 닫지 않는다.** + +### 테스트 형태에 대한 관찰 + +#1269의 `tests/cli-start-journal-order.test.ts` 는 소스 문자열의 `indexOf` 순서를 +비교하는 정적 테스트다. 우리 050-3 계획은 격리된 `OPENCODEX_HOME` 에 죽은 journal +PID와 살아있는 프록시를 두고 실제로 `ocx start` 를 돌리는 동작 테스트였다. + +정적 순서 검사는 리팩터링에 약하다. 누군가 `reconcileJournal()` 호출을 헬퍼 +함수로 빼면 문자열이 사라져 테스트가 의미를 잃는다. 다만 이 결함이 순수한 +**순서** 문제라는 점에서 최소한의 회귀 가치는 있다. + +보완 요청 시 동작 테스트를 함께 제안한다. 특히 음성 대조군(죽은 PID + 리스너 +없음 → 여전히 조정됨)이 있어야 "조정을 없앤 게 아니라 순서만 바꿨다" 를 증명한다. + +## 계획 변경 요약 + +| 계획 항목 | 변경 | +|---|---| +| 050-2 (#1236 windowsHide) | **폐기** — #1268 채택 | +| 050-3 (#1230 journal 순서) | **축소** — #1269 채택 + `handleEnsure` 보완 | +| 040-7 (#1266) | 유지 | + +WP5 구성이 바뀌었다: **직접 구현 5건**(050-1, 050-4, 050-6, 050-7, 050-8), +**채택 2건**(050-2 → #1268, 050-3 → #1269), **보류 2건**(050-5는 이슈 종결, +050-9는 디스패치 결과 대기). #1230의 `handleEnsure` 후속 PR은 기여자가 보완을 +거절할 때만 여섯 번째 직접 항목이 된다. + +범위 축소가 아니라 기여자가 먼저 해준 것이다. + +## 반복되는 패턴 + +게이트를 돌릴 때마다 기여자들이 우리 계획과 같은 일을 하고 있다는 사실이 +드러난다. #1240은 우리가 지적한 종료 동작을 스스로 고쳤고, 이번엔 #1268과 +#1269가 우리 WP5 항목을 먼저 처리했다. + +이것은 계획이 틀렸다는 뜻이 아니라 **같은 결함을 같은 근거로 보고 있다**는 뜻이다. +다만 실행 순서에는 영향이 있다: 직접 구현에 들어가기 전에 항상 게이트를 먼저 +돌려야 하며, 그러지 않으면 이미 존재하는 기여를 중복 생산하게 된다. diff --git a/devlog/_plan/260808_bug_campaign/014_wp1_ci_final_tally.md b/devlog/_plan/260808_bug_campaign/014_wp1_ci_final_tally.md new file mode 100644 index 0000000000..c717d07ae6 --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/014_wp1_ci_final_tally.md @@ -0,0 +1,75 @@ +# 014 — WP1 CI 최종 집계 (정확한 판독법 적용) + +`012` 에서 확립한 규칙(`gh pr checks` 대신 Actions API, `status`/`conclusion` +구분)으로 다시 집계했다. **초안 집계와 결과가 다르다.** + +## 집계 방법 + +```bash +sha=$(gh pr view --repo lidge-jun/opencodex --json headRefOid --jq .headRefOid) +gh api "repos/lidge-jun/opencodex/actions/runs?head_sha=$sha" \ + --jq '[.workflow_runs[] | select(.name=="Cross-platform CI")] + | if length==0 then "no-run" else .[0] | "\(.status)/\(.conclusion)" end' +``` + +`Cross-platform CI` 만 본다. 이것이 실제 테스트를 도는 워크플로다. + +## 결과 + +| PR | Cross-platform CI | 판정 | +|---|---|---| +| #1189 | completed/success | **통과** | +| #1187 | completed/success | **통과** | +| #1184 | completed/success | **통과** | +| #1195 | completed/success | **통과** | +| #1202 | completed/success | **통과** | +| #1169 | completed/success | **통과** | +| #1240 | completed/success | **통과** | +| #1226 | completed/success | **통과** | +| #1224 | completed/success | **통과** | +| #1244 | completed/success | **통과** | +| #1266 | completed/success | **통과** | +| #1249 | completed/**failure** | 아래 참조 | +| #1192 | completed/cancelled | 재실행 필요 | +| #1228 | completed/cancelled | 재실행 필요 | +| #1256 | queued/null | 실행 중 | +| #1264 | queued/null | 실행 중 | +| #1263 | queued/null | 실행 중 | +| #1258 | **no-run** | 새 head의 런 없음 — 승인 필요 | + +통과 11건. 초안 집계에서 PASS로 셌던 #1249는 실제로 failure였고, #1258은 런이 +아예 없었다. `gh pr checks` 만 봤다면 둘 다 놓쳤다. + +## #1249 실패는 코드 결함이 아니다 + +`test 3/4` 잡의 로그 말미: + +``` +panic: Segmentation fault at address 0xFFFFFFFFFFFFFFF8 +oh no: Bun has crashed. This indicates a bug in Bun, not your code. +... +Illegal instruction (core dumped) bun test --isolate tests ... --shard=3/4 +Process completed with exit code 132 +``` + +exit 132는 SIGILL이다. Bun 1.3.14 런타임 크래시이며 테스트 어서션 실패가 아니다. +63초를 정상 실행한 뒤 세그폴트했고, Bun 자체가 "이것은 당신 코드의 버그가 +아니다" 라고 출력한다. + +**처분: 재실행.** 재현되면 shard 3/4의 특정 테스트와 Bun 버전 조합 문제로 +별도 추적한다. #1249의 빈 `data:` 프레임 수정과는 무관하다. + +## 다음 행동 + +1. `#1258` 의 새 head 런 승인 (`010` 파트 1 절차) +2. `#1192`, `#1228` 재실행 +3. `#1249` 재실행 후 세그폴트 재현 여부 확인 +4. 통과 11건은 머지 승인 대상 — **사용자 승인 필요** + +## 이 집계가 확인해 준 것 + +판독 규칙을 고치지 않았다면 #1249를 통과로 보고하고 머지 후보에 올렸을 것이다. +`gh pr checks` 는 그 PR에 대해 실패를 보여주지 않았다. + +동시에 반대 방향 오류도 막았다. #1258은 "체크 없음" 이었는데, 규칙 없이는 +"실패 없으니 통과" 로 셌을 것이다. 실제로는 승인이 필요한 상태다. diff --git a/devlog/_plan/260808_bug_campaign/015_wp6_close_execution.md b/devlog/_plan/260808_bug_campaign/015_wp6_close_execution.md new file mode 100644 index 0000000000..71349c6d06 --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/015_wp6_close_execution.md @@ -0,0 +1,160 @@ +# 015 — WP6 close 집행 계획 (사용자 승인 범위) + +사용자가 "위양성은 close" 로 명시 승인한 범위다. 착수 직전 각 대상의 상태와 +근거를 다시 확인했고, **한 건이 대상에서 빠졌다.** + +## 착수 전 상태 재확인 + +``` +PR #1155 OPEN head=307045c55 updated=2026-08-07T07:19:17Z +PR #1119 OPEN head=e00ce78be updated=2026-08-06T12:31:45Z CONFLICTING +issue #1128 OPEN updated=2026-08-08T07:09:36Z ← 방금 갱신됨 +``` + +## #1128 — close 대상에서 제외 (재확인 결과) + +`002`/`060` 은 이 이슈를 "해결됨" 으로 분류했다. 그러나 착수 직전 확인에서 두 +가지가 드러났다. + +1. 다른 사용자의 추가 보고가 있다 — awillheartwu, 2026-08-06: "same here, + compact failed" +2. **maintainer가 2026-08-08T07:09:36Z에 직접 코멘트를 남기고 열어두었다.** + 요지: 이 보고는 2.10.1/2.10.2에서는 유효했고 `0b8e608c`(v2.11.0)로 전제가 + 바뀌었으니 **v2.11.0에서 재시험해달라**, 여전히 실패하면 그 버전의 terminal + event 시퀀스를 첨부해달라, 그때는 2.10.x의 bounded-JSON 정책 부재가 아니라 + 현재 compact 릴레이 버그다. "Leaving this open pending that current-version + control." + +#1176과 정확히 같은 상황이다. 코드 분석("정책이 폐기됐으니 그 경로는 없다")은 +맞지만, 그것이 리포터가 겪은 실패가 사라졌다는 증명은 아니다. 같은 날 열어둔 +판단을 몇 시간 뒤 뒤집는 것은 근거 없는 번복이다. + +**처분 변경: close → tracking 유지.** 리포터 회신 대기. + +따라서 **WP6의 이슈 close 대상은 0건**이 된다. + +## close 집행 대상 — **0건** (감사에서 둘 다 막힘) + +착수 전 감사가 두 close를 모두 기각했다. 근거를 직접 재확인했고 둘 다 타당하다. + +### PR #1155 (myrosla) — **close 철회. 도달 가능한 경로였다** + +> **우리 판정이 틀렸다.** "도달 불가" 근거는 레지스트리 opt-in이 없다는 것이었고 +> 그 부분은 맞다(`modelResponsesUpstreamStreaming` 은 레지스트리 전용이며 +> 프로덕션 항목 중 `false` 로 설정한 것이 없다. 유일한 false는 테스트 픽스처 +> `tests/deepseek-inbound-wire.test.ts:244-267`). +> +> **그러나 이 PR은 그 힌트만 보존하는 게 아니다.** 핵심 훅은 다음이다: +> +> ```diff +> const wsResponse = await runWithWebSearch({ +> parsed, adapter, +> + upstreamStreaming: parsed.stream, +> ``` +> +> 사용자 요청이 직접 이 값을 정한다. 공개 Responses API는 `stream` 을 +> optional로 받고(`src/responses/schema.ts:133-144` 의 `stream: z.boolean().optional()`), +> 생략/`false` 는 `parsed.stream === false` 로 매핑되며 +> (`src/responses/parser.ts:678-686`), `planWebSearch()` 에는 스트림 요건이 +> 없다(`src/web-search/index.ts:148-150`). +> +> 즉 **`web_search` 를 켠 채 `stream` 을 생략하거나 `false` 로 보낸 라우팅 +> `/v1/responses` 요청**이 정확히 이 PR의 buffered 분기를 활성화한다. PR 자신의 +> 새 테스트도 `upstreamStreaming: false` 를 의도적으로 호출한다. +> +> 도달 불가 주장은 철회한다. 이 PR을 닫으면 실제로 도달하는 호환 경로를 버린다. + +**처분 변경: close → 열어둔 채 코멘트.** 다만 머지 준비가 된 것도 아니다: +buffered `openai-responses` 경로가 compaction 전용 파서를 호출해 tool-call만 +있는 응답에서 오류가 난다(`src/adapters/openai-responses.ts:1271-1293`). 자동 +리뷰가 지적한 미해결 사항과 일치한다. + +코멘트 내용: (1) 우리가 "도달 불가" 로 판단했다가 철회한다는 사실과 그 이유, +(2) 실제 활성화 경로(`stream` 생략 + `web_search`), (3) tool-call 전용 응답 +처리와 retained-event 회계를 보완하거나 분리해달라는 요청. + +
+철회된 close 근거 (기록용) + +근거를 현재 dev에서 재확인했다. + +`src/providers/registry.ts:1318-1326`: + +``` +// The #875-era bounded-JSON force (`modelResponsesUpstreamStreaming`) is retired +// for this entry: ... live probes (2026-08-07, including the tool-result replay +// shape that originally stalled) close on the terminal. ... forcing stream:false +// only delayed every byte until generation finished (28-46 s of silence on long +// turns). The registry knob itself remains for providers that need it +``` + +`src/web-search/loop.ts:364-366` 은 매 반복 `stream: true` 를 강제한다. + +즉 이 PR이 보존하려는 buffered upstream 정책은 프로덕션에서 도달하지 않는다. +정책 훅 자체는 남아 있으므로, 실제로 buffered를 요구하는 프로바이더가 생기면 +이 작업을 되살리는 것이 맞다. + +코멘트 요지: 경로 부재를 코드로 설명하고, 훅이 남아 있으니 필요해지면 재개를 +환영한다고 밝힌다. 조사에 감사를 표한다. + +
+ +### PR #1119 (본인) — **close 보류. 커버리지 손실이 있다** + +> **"완전 흡수" 주장이 틀렸다.** dev에 착지한 계약은 +> `tests/codex-catalog.test.ts:2391-2518` 이며 내장 레지스트리 기본값, destination +> enrichment, 명시적 `false`, `modelReasoningSummaryDelivery` 를 덮는다. +> +> 그러나 #1119는 **임의 커스텀 프로바이더의 명시적 +> `modelSupportsReasoningSummaries: true` 가 템플릿 경로와 routed-strip 순서를 +> 통과하는지**를 추가로 시험한다. 현재 dev 테스트는 그 경로를 덮지 않는다. +> absent-opt-in과 fallback 경로 어서션도 별개다. +> +> 지금 닫으면 최소 한 건의 실제 회귀 케이스를 잃는다. + +**처분 변경: 대체 후 close.** 순서를 바꾼다. + +1. 세 테스트 케이스를 현재 dev 위에 다시 만든다(또는 개별 동등성을 증명한다) +2. 그 대체본이 착지한 뒤 #1119를 superseded로 닫고 링크를 남긴다 + +devlog 16개 문서도 현재 dev에 없다. 보존 가치가 있는 것: 25항목 grade matrix, +provenance/isolation 설계, 기여자 attribution/lease 기록. 낡은 기획 묶음을 +그대로 머지하지 말고 정정된 이력 문서 유닛으로 큐레이션한다. + +
+원래 close 근거 (부분적으로만 유효) + +주장하는 #1100 계약의 **일부**는 이미 dev에 있다. `tests/codex-catalog.test.ts:2391` 부터: + +```ts +test("built-in DeepSeek and GLM effort models opt into Codex reasoning propagation (#1100)", ...) + { slug: "deepseek/deepseek-v4-flash", efforts: ["low", "high", "max", "ultra"] }, + ... + expect(routed?.supports_reasoning_summaries).toBe(true); +``` + +GitHub도 CONFLICTING으로 보고한다. 본인 PR이므로 외부 조율이 필요 없다. + +devlog 16개 파일은 살릴 가치가 있으면 분리해 재발행한다. + +
+ +## 최종 결과 — close 0건 + +사용자가 승인한 것은 "위양성은 close" 였다. 감사 결과 **위양성이 아니었다.** +승인 범위 안에 있다고 해서 근거 없이 실행하지 않는다. + +| 대상 | 초안 | 최종 | 사유 | +|---|---|---|---| +| PR #1155 | close | **열어둠 + 코멘트** | 도달 가능한 경로. 다만 머지 준비 미완 | +| PR #1119 | close | **대체 후 close** | 커버리지 한 건 손실. 대체본 선행 | +| 이슈 #1128 | close | **tracking** | maintainer가 당일 재시험 요청하며 열어둠 | + +## 남은 작업 (다음 사이클) + +1. #1155에 철회 코멘트 — 우리 판단 오류를 밝히고 보완 요청 +2. #1119의 세 테스트 케이스를 현재 dev에 재작성 +3. 그 착지 후 #1119를 superseded로 close +4. #1119의 devlog 16문서 중 보존 가치 있는 것 큐레이션 + +**close 실행은 이번 사이클에서 하지 않는다.** diff --git a/devlog/_plan/260808_bug_campaign/016_wp8_1119_replacement.md b/devlog/_plan/260808_bug_campaign/016_wp8_1119_replacement.md new file mode 100644 index 0000000000..ee6c7c4d7b --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/016_wp8_1119_replacement.md @@ -0,0 +1,119 @@ +# 016 — WP8: #1119 대체 회귀 테스트 (close 선행조건) + +감사가 "#1119를 그냥 닫으면 커버리지를 잃는다" 고 지적했고, 그 대체본을 만들었다. + +브랜치 `codex/260808-1100-custom-optin-contract`, 커밋 `8bde6c422` +(리베이스 후. 이전 `09e95ebb2` → `00ecc61ea` → 현재). +`Co-authored-by: bitkyc08-arch ` + +## 커버리지 공백 확인 + +현재 dev의 `tests/codex-catalog.test.ts` 에 있는 것: + +| 위치 | 덮는 것 | +|---|---| +| `:2351` (#323) | 커스텀 프로바이더의 명시적 **`false`** opt-out | +| `:2371` (#538) | `modelReasoningSummaryDelivery` 경로 | +| `:2391` (#1100) | **내장 레지스트리** 행의 effort ladder와 summary 지원 | +| `:2454~` | destination enrichment, 저장 설정 미오염 | + +없는 것: **커스텀 프로바이더의 명시적 `true` opt-in이 템플릿 경로를 통과하는지.** + +결정적으로 `#323`과 `#538` 테스트는 둘 다 `buildCatalogEntries(null, ...)` 을 +쓴다. 이는 폴백 분기(`src/codex/catalog/sync.ts:291~`)이며 **routed strip을 아예 +실행하지 않는다.** 따라서 순서 회귀가 나도 두 테스트는 계속 통과한다. + +## 무엇이 위험한가 + +`src/codex/catalog/sync.ts:266-269`: + +```ts +applyReasoningLevels(e, model?.reasoningEfforts, model?.defaultReasoningEffort, preserveExact); +normalizeRoutedCatalogEntry(e, model?.parallelToolCalls === true); // flag를 지운다 +if (model) applyCatalogMetadata(e, model.provider, model.id, model.contextCap); +applyCatalogModelMetadata(e, model); // flag를 되살린다 +``` + +opt-in은 **뒤의 호출이 앞의 삭제를 되돌리기 때문에만** 살아남는다. 순서를 +뒤집으면 opt-in한 모든 라우팅 프로바이더가 Codex로부터 `reasoning.effort` 를 +조용히 못 받게 되고, 그동안 picker는 effort ladder를 계속 표시한다. 이것이 +#1100의 원래 증상이다. + +## 추가한 테스트 3개 + +`tests/codex-catalog.test.ts` 의 #538 테스트 뒤에 삽입. + +1. **`routed strip does not defeat an explicit custom-provider summary opt-in`** + — `nativeTemplate()` 을 넘겨 템플릿 경로를 타고, ladder와 + `supports_reasoning_summaries: true` 를 함께 확인 +2. **`custom routed rows without an opt-in stay conservative`** — opt-in이 없으면 + `false` 유지. 임의 엔드포인트에 OpenAI 전용 summary 전달을 주장하지 않는 + 의도적 보수성을 못박는다 +3. **`the no-template fallback never applies the routed summary strip`** — 폴백 + 경로가 strip을 건너뛴다는 비대칭 자체를 명시. 두 경로를 통합할 때 눈에 보이게 + +## 활성화 증거 (C-ACTIVATION-GROUNDING-01) + +통과만으로는 회귀를 잡는지 알 수 없다. 순서를 실제로 뒤집어 확인했다. + +``` +ABLATION: order swapped (strip now runs AFTER metadata) +(fail) routed strip does not defeat an explicit custom-provider summary opt-in (#1100) +(fail) built-in DeepSeek and GLM effort models opt into Codex reasoning propagation (#1100) + 6 pass, 2 fail +``` + +되돌린 뒤: + +``` + 8 pass, 0 fail +``` + +새 테스트가 순서 회귀를 잡는다. 나머지 두 테스트(보수성, 폴백)는 ablation에서도 +통과하는데, 그것들은 순서가 아니라 다른 계약을 지키므로 정상이다. + +## 검증 + +``` +$ bun test tests/codex-catalog.test.ts + 132 pass, 0 fail, 600 expect() calls + +$ bun run typecheck +(clean) +``` + +## 리뷰 반영 +독립 리뷰가 ablation을 재현해 확인했다(전체 파일 130 pass / 2 fail, 복구 후 +132 pass). 블로커 1건은 주석의 휘발성 라인 번호였다 — `:2391` 과 +`sync.ts:266-269` 는 이미 어긋나 있었다. 라인 번호 대신 테스트 이름과 함수명으로 +가리키도록 고쳤다. 리팩터링에도 주석이 유효하게 남는다. + +## 리베이스 재검증 (dev 이동 대응) + +검증 도중 dev가 `fdc47db7b` 에서 `517f44604` 로 이동했다. `003` 프로토콜대로 +`git rebase --onto origin/dev` 후 **검증을 처음부터 다시 돌렸다** — 낡은 base의 +결과는 증거가 아니다. + +새 base 결과: + +``` +$ bun test tests/codex-catalog.test.ts 132 pass / 0 fail +$ bun run typecheck clean +$ (ablation) 순서 뒤집기 6 pass / 2 fail +$ (복구 후) 8 pass / 0 fail +``` + +diff 범위는 그대로 `tests/codex-catalog.test.ts` 한 파일 83줄이며, +`Co-authored-by` 트레일러도 리베이스를 통과했다. + +## #1119 처분에 미치는 영향 + +감사가 건 조건("대체본 착지 후에만 close")의 코드 부분이 준비됐다. 남은 것: + +1. 이 브랜치를 PR로 열어 착지 — **push/PR 생성은 사용자 승인 필요** +2. 착지 후 #1119를 superseded로 close하며 이 PR 링크 +3. #1119의 devlog 16문서 중 보존 가치 있는 것 큐레이션 (25항목 grade matrix, + provenance/isolation 설계, attribution/lease 기록) + +테스트 자체는 원작자 트레일러를 달았다. 계약을 발견하고 문서화한 것은 #1119의 +작업이며, 우리는 그것을 현재 dev 위로 옮겼을 뿐이다. diff --git a/devlog/_plan/260808_bug_campaign/017_wp9_1245_fix.md b/devlog/_plan/260808_bug_campaign/017_wp9_1245_fix.md new file mode 100644 index 0000000000..8757782f57 --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/017_wp9_1245_fix.md @@ -0,0 +1,202 @@ +# 017 — WP9: #1245 GUI Startup Safety stale error 수정 + +브랜치 `codex/260808-1245-startup-stale-error`, 커밋 `44a004013`. +base `origin/dev@517f44604`. 파일 2개, 288줄. 회귀 테스트 5건. + +## 결함 + +`installResult` 는 페이지가 액션을 **시작할 때만** 초기화된다 +(`gui/src/pages/Startup.tsx` 의 `runInstallAction` 첫 줄). 새로고침은 health +데이터를 갈아끼우지만 이 상태는 건드리지 않는다. + +따라서 설치가 실패한 뒤 사용자가 다른 경로로 문제를 해결하면 — CLI, 서비스 +관리자, 페이지가 시작하지 않은 재시도 — 같은 화면이 **"Restart protected" 와 +"Installation failed" 를 동시에** 보여준다. 사용자가 지울 방법도 없다. + +## 재현 (수정 전) + +회귀 테스트가 실제 순서를 그대로 구동한다: 실패하는 `startup-action` → 새로고침이 +`protected` 반환. 수정 전 렌더 결과: + +``` +... Restart protected ... opencodex will be available after restart ... +... Installation failed: service install failed ... +``` + +두 문장이 한 화면에 공존한다. 이것이 리포터가 보고한 상태다. + +## 수정 + +MODIFY `gui/src/pages/Startup.tsx` — `fetchStartup` 에서 `next` 파싱 직후: + +```ts +if (next.status !== "at-risk") { + setInstallResult(current => (current?.kind === "error" ? null : current)); +} +``` + +**실패만 지운다.** 성공 확인은 사용자가 방금 실행한 액션의 영수증이므로 남긴다. +그것까지 지우면 모순 제거가 아니라 피드백 제거가 된다. + +조건을 `status !== "at-risk"` 로 잡은 이유: `StartupStatus` 는 +`"native" | "protected" | "at-risk"` 이며, 실패 알림이 모순되는 상태는 위험이 +해소된 경우 전부다. `protected` 만 검사하면 `native` 로 복원한 사용자가 낡은 +실패 문구를 계속 보게 된다. + +NEW `gui/tests/startup-install-result-reconciliation.test.tsx` (129줄) — +`gui/tests/startup-revisit-cache.test.tsx` 의 Happy DOM 픽스처를 따른다. + +## 활성화 증거 (C-ACTIVATION-GROUNDING-01) + +새 조건부 분기이므로 발화 증거가 필요하다. 테스트를 먼저 써서 red를 확인했다: + +``` +수정 전: 1 fail — "Expected to not contain: service install failed" + (렌더 텍스트에 Restart protected와 Installation failed가 함께 존재) +수정 후: 1 pass +``` + +분기를 목킹으로 우회하지 않고 페이지의 실제 버튼 클릭과 fetch 응답으로 구동했다. + +## 검증 + +``` +$ bun test tests/startup* (gui) 4 pass / 0 fail +$ bun test tests (gui) 680 pass / 0 fail +$ bun run lint:gui clean +$ bun run typecheck clean +``` + +## 리뷰 반영 — 조건이 너무 넓었다 + +첫 구현은 `next.status !== "at-risk"` 였다. 리뷰가 이걸 막았고, 재현까지 해서 +보여줬다: **서비스로 protected인 상태에서 shim 설치가 실패하면 그 실패가 지워진다.** +shim은 여전히 설치되지 않았으므로 그 정보는 참이고 사용자에게 필요하다. + +`status` 는 재시작 안전성 **전체**를 말하지, 사용자가 방금 시도한 액션이 +성공했다는 증거가 아니다. 무관한 성공 뒤에 진짜 문제를 숨기는 셈이었다. + +수정된 조건 — 액션별로 그 액션이 바꾸려던 health 필드에 대해서만 판정한다: + +```ts +setInstallResult(current => { + if (current?.kind !== "error") return current; + if (next.status === "native") return null; + const satisfied = current.action === "install-service" + ? next.serviceInstalled && next.serviceRunning + : next.shimInstalled && next.shimHealthy; + return satisfied ? null : current; +}); +``` + +`native` 를 별도로 두는 이유: 네이티브 라우팅으로 복원하면 두 설치 모두 미해결 +상태가 아니게 된다. + +### 두 번째 테스트와 그 ablation + +리뷰 지적대로 엣지 케이스 테스트를 추가했다 — "서비스만 건강함을 증명하는 +새로고침에서 shim 실패는 살아남는다". + +넓은 조건으로 되돌려 확인: + +``` +ABLATION (status !== "at-risk"): 1 pass / 1 fail + (fail) a failed shim install survives a refresh that only proves the service is healthy +복구 후: 2 pass / 0 fail +``` + +새 테스트가 정확히 그 회귀를 잡는다. + +### 선택자 견고화 + +두 설치 버튼의 표시 텍스트가 **둘 다 "Install"** 이라 `!/shim/i` 로는 구분되지 +않았고, DOM 순서에 의존하고 있었다. `aria-label` 기반 접근 가능 이름으로 바꿨고, +찾지 못하면 실제 버튼 이름 목록을 오류에 담아 디버깅이 가능하게 했다. 고정 +sleep도 마이크로태스크 드레인 헬퍼로 교체했다. + +## 리뷰 2라운드 — 조건을 두 번 더 좁혔다 + +리뷰가 두 시나리오를 더 재현했고 둘 다 타당했다. + +**(1) `serviceInstalled && serviceRunning` 은 성공 증거가 아니다.** stale하거나 +충돌하는 서비스는 둘 다 참이면서 여전히 불건강할 수 있다. 실제로 실패한 Repair가 +새로고침 후 사라지는데 페이지는 계속 "Action required" 와 "Stale" 을 표시했다. + +UI 자신이 `data.serviceViable` 을 건강 판정에 쓴다 +(`gui/src/pages/startup-sections.tsx:108`). 같은 기준으로 맞췄다. + +**(2) `native` 무조건 정리는 선택적 shim 실패를 숨긴다.** 네이티브 머신에도 +shim Install 버튼이 나온다(`startup-sections.tsx:134`). 라우팅 의존성이 없다는 +것과 shim 설치 실패가 무효라는 것은 다른 얘기다. + +액션 시점의 라우팅 상황을 결과에 기록하도록 바꿨다: + +```ts +setInstallResult({ kind: "error", action, ..., forLocalRouting: data?.localRoutingDependency === true }); +``` + +그리고 `native` 정리는 그 플래그가 참일 때만 적용한다. + +### 최종 판정식 + +```ts +setInstallResult(current => { + if (current?.kind !== "error") return current; + if (next.status === "native" && current.forLocalRouting === true) return null; + const satisfied = current.action === "install-service" + ? next.serviceViable + : next.shimInstalled && next.shimHealthy; + return satisfied ? null : current; +}); +``` + +### 회귀 4건과 ablation + +| 테스트 | 주장 | +|---|---| +| 서비스 실패가 서비스 정상화 후 사라진다 | 정리 동작 | +| shim 실패가 서비스만 정상인 새로고침에서 살아남는다 | 액션별 범위 | +| Repair 실패가 서비스 stale 상태에서 살아남는다 | `serviceViable` 기준 | +| native 머신의 shim 실패가 native 새로고침에서 살아남는다 | `forLocalRouting` 조건 (음성) | +| 로컬 라우팅 중 실패가 native 복원 후 사라진다 | `forLocalRouting` 조건 (양성) | + +두 좁힘을 되돌린 ablation: + +``` +ABLATION: 2 pass / 2 fail + (fail) a failed service repair survives a refresh where the service is still stale + (fail) a failed shim install on an already-native machine survives a native refresh +복구 후: 4 pass / 0 fail +``` + +각 좁힘이 정확히 자기 테스트를 지킨다. + +### 리뷰 3라운드 — 분기 하나에 양성 증거가 없었다 + +`forLocalRouting` 분기에는 음성 케이스(native 머신의 shim 실패는 남는다)만 있고 +**양성 케이스가 없었다.** 즉 그 분기를 통째로 지워도 테스트가 통과했다. + +다섯 번째 테스트를 추가했다: 로컬 라우팅 의존 상태에서 설치 실패 → native로 +복원 → 실패 알림이 사라진다. + +ablation으로 확인: + +``` +분기 제거: 4 pass / 1 fail + (fail) a failed install clears when the user restores native routing instead +복구 후: 5 pass / 0 fail +``` + +이제 **모든 분기에 그 분기를 지우거나 넓히면 실패하는 테스트가 있다.** + +## 계획 대비 차이 + +`050` §050-1은 조건을 `next.status === "protected"` 로 적었다. 구현은 두 번 +움직였다: 처음엔 `!== "at-risk"` 로 넓혔다가(리뷰에서 기각), 최종적으로는 +**액션별 health 필드 판정**으로 좁혔다. 계획보다 좁으면서 동시에 정확하다 — +`protected` 여부가 아니라 "그 액션이 이루려던 상태가 됐는가" 를 본다. + +## GUI 스크린샷 + +`enforce-target` 은 제목이나 본문에 `gui` 가 있으면 스크린샷을 요구한다. PR 생성 +시점에 첨부한다 — **PR 생성은 사용자 승인 대기 중**이다. diff --git a/devlog/_plan/260808_bug_campaign/018_wp10_1196_fix.md b/devlog/_plan/260808_bug_campaign/018_wp10_1196_fix.md new file mode 100644 index 0000000000..37fede56af --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/018_wp10_1196_fix.md @@ -0,0 +1,114 @@ +# 018 — WP10: #1196 issue-quality media placeholder — **보류 (리뷰 FAIL)** + +> **이 브랜치는 발행하지 않는다.** 독립 리뷰가 블로커 3건으로 기각했고 전부 +> 타당하다. `.github/scripts/` 는 이슈를 **자동 close** 하는 게이트를 먹이므로 +> 위양성 하나가 정당한 제보를 닫는다. 확신이 없는 상태로 올릴 표면이 아니다. +> +> **기각 사유 (요약)** +> +> 1. **축소 근거가 틀렸다.** 멀티라인 `` 의 자식이 4칸/탭 들여쓰기면 +> `isMediaOnly` 가 여전히 `false` 다. 내가 2칸으로만 확인하고 "이미 해결됨" +> 이라고 결론지었다. 계획의 나머지 절반을 근거 없이 버린 셈이다. +> 2. **`isPlaceholderOnlyValue` 가 이 문맥에 너무 넓다.** 폼 전체 필드용이라 +> `N/A`, `None`, `Todo`, `TBD` 까지 잡는다. 리뷰가 재현했다 — +> `` 를 예시로 쓴 정상 feature request가 base에서는 +> 통과하는데 이 커밋에서는 무효가 되어 **자동 close 된다.** 원래 버그보다 +> 나쁘다. +> 3. **폼이 그 HTML을 낸다는 근거가 리포지토리에 없다.** `.github/ISSUE_TEMPLATE/` +> 네 템플릿 어디에도 media 필드가 없다. 내 테스트는 HTML을 지어내서 헬퍼만 +> 검증했고, 실제 `validateIssue` → close 결정 경로는 건드리지 않았다. +> +> **다시 하려면:** #1196 원본 이슈 본문(정제한 픽스처)을 확보하고, media 전용 +> 술어를 쓰고, 들여쓰기 변형까지 덮고, accepted→closed 전이를 전체 폼 검증으로 +> 증명해야 한다. 그 근거 없이 자동 close 의미를 넓히지 않는다. +> +> 아래 원래 기록은 무효 판정과 함께 남긴다. + + + +브랜치 `codex/260808-1196-media-placeholder`, 커밋 `5548d8400`. +base `origin/dev@517f44604`. 파일 2개, 33줄. + +## 계획 대비 축소 — 두 결함 중 하나는 이미 고쳐져 있었다 + +`050` §050-4는 두 가지를 보고했다. 현재 dev에서 실제로 확인해보니: + +| 보고된 결함 | 현재 dev 상태 | +|---|---| +| `clean("")` 가 HTML 그대로 남음 | **재현됨** | +| 들여쓴 ``/`` 를 가진 멀티라인 `` 가 media-only로 인식 안 됨 | **이미 해결됨** (`isMediaOnly` 가 `true` 반환) | + +두 번째는 그사이 다른 작업으로 고쳐졌다. 계획대로 토큰 기반 보호/복원을 전면 +재작성했다면 이미 동작하는 코드를 불필요하게 갈아엎을 뻔했다. + +따라서 수정 범위를 남은 하나로 좁혔다. 계획 문서가 요구한 `protectCodeSpans` +재작성은 **하지 않는다** — 그것이 풀려던 문제가 남아 있지 않다. + +## 결함 + +이슈 폼은 응답하지 않은 media 필드를 `` 로 렌더한다. + +`.github/scripts/issue-quality-core.cjs` 의 `stripHtmlMedia` 는 media 블록의 +내부 텍스트가 **비었을 때만** 치환했다: + +```js +return innerStripped.length === 0 ? " " : match; +``` + +placeholder는 비어 있지 않으므로 통과했고, 리터럴 HTML이 실질 내용으로 계수됐다. +리포터가 비워둔 섹션이 "답변됨" 으로 읽혀 품질 게이트를 통과했다. + +## 수정 + +```js +if (innerStripped.length === 0 || isPlaceholderOnlyValue(innerStripped)) return " "; +return match; +``` + +placeholder는 폼이 "제공된 것이 없다" 고 말하는 방식이므로 빈 것과 같다. +`isPlaceholderOnlyValue` 를 재사용해 placeholder 문구 목록이 한 곳에만 있게 했다 +— 여기서 정규식을 다시 쓰면 두 정의가 갈라진다. + +### 보존해야 하는 것 + +| 입력 | 결과 | 이유 | +|---|---|---| +| `` | **유지** | 리포터가 실제로 제공한 캡션. 지우면 증거 손실 | +| 펜스 코드 안의 `` | **유지** | 마크업을 인용한 것이지 삽입한 것이 아니다 | + +## 활성화 증거 (C-ACTIVATION-GROUNDING-01) + +테스트를 먼저 써서 red를 확인했다: + +``` +수정 전: ✖ reports a placeholder-only media section as media-only + AssertionError: false !== true +수정 후: 116 pass / 0 fail (exit 0) +``` + +ablation — placeholder 검사만 제거: + +``` +exit=1, 114 pass / 2 fail +복구 후: exit=0, 116 pass / 0 fail +``` + +## 검증 + +``` +$ node --test .github/scripts/issue-quality.test.cjs +ℹ tests 116 +ℹ pass 116 +ℹ fail 0 + +$ 동작 확인 +wrapped placeholder -> "" mediaOnly=true +real caption -> "" +fenced preserved -> true +``` + +## 남은 것 + +`050` §050-4가 제안한 나머지(토큰 기반 보호, 라인 인덱스 복원 제거)는 그 근거였던 +멀티라인 media 오인식이 해소되어 **범위에서 제외한다.** 별도의 재현 가능한 결함이 +나오면 그때 다시 제기한다. diff --git a/devlog/_plan/260808_bug_campaign/019_wp11_publish.md b/devlog/_plan/260808_bug_campaign/019_wp11_publish.md new file mode 100644 index 0000000000..46c94bdbf9 --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/019_wp11_publish.md @@ -0,0 +1,125 @@ +# 019 — WP11: PR 발행과 머지 집행 (사용자 승인) + +사용자가 "PR 올리면서 진행", "머지도 판단대로" 를 명시 승인한 범위의 집행 기록. + +## 집행 결과 + +### 머지된 것 — 6건 + +| PR | 머지 커밋 | 내용 | +|---|---|---| +| #1240 | `2f0dc7cb6` | SSE 비레코드 프레임을 건너뛰기로 처리 (snowyukitty) | +| #1202 | `a81f9423a` | history 실패를 lock으로 뭉뚱그리지 않음 + Windows 경로 동일성 (Yuxin-Qiao) | +| #1224 | `903b69b4b` | 프로바이더별 컨텍스트 캡 독립 (iF2007) | +| #1274 | `c95c0690c` | 커스텀 프로바이더 reasoning-summary 계약 고정 (본 캠페인) | +| #1275 | `671a0df77` | #1245 GUI stale install failure 수정 (본 캠페인) | +| #1266 | `28ba79377` | Vertex thought signature 재생 (Ingwannu) | + +`origin/dev` 가 `517f44604` 에서 `28ba79377` 로 이동했다. + +### 생성된 PR — 2건 + +- **#1274** 대체 회귀 테스트 → **머지 완료** +- **#1275** #1245 GUI 수정 → **머지 완료** + +### close된 것 — 4건 + +| 항목 | 근거 | +|---|---| +| 이슈 #1219 | #1240 착지. 네 파서 모두 비레코드 프레임 방어 | +| 이슈 #1191 | #1202 착지. 두 결함(문구 수렴, Windows 경로) 모두 해소 | +| 이슈 #1245 | #1275 착지. GUI 모순 표시 해소 | +| PR #1119 | #1274로 superseded. 커버리지 손실 없음을 확인한 뒤 실행 | + +### 코멘트 — 3건 + +- **#1155** 판단 철회. "도달 불가" 근거가 틀렸음을 코드로 설명하고, 머지 준비 + 미완 사항(tool-call 전용 응답 파서)을 함께 전달 +- **#1263** 테스트 교체 요청. 대조 실험 결과(패치 없음 행 vs 패치본 3ms)와 + 대체 테스트 설계를 구체적으로 제시 +- **#1269** `handleEnsure` 보완 요청. 라인 근거와 동작 테스트 제안 + +## 감사 지적 2건 — 둘 다 내 잘못 + +### (1) #1202를 CI green 없이 머지했다 + +`gh pr merge` 전에 확인한 것은 승인 직후 상태였고, 그 head의 **Cross-platform CI가 +`cancelled`** 로 끝난 것을 확인하지 않았다. 집계 `ci` 체크는 `test 3/4` 취소 때문에 +`failure` 였다. + +내가 직접 만든 판독 규칙(`012`, `014`) — "green은 CI 런이 존재하고 결론이 +success인 경우뿐" — 을 정작 머지 시점에 적용하지 않았다. 규칙을 쓰고도 서두를 때 +안 보는 게 정확히 이 실패의 모양이다. + +**사후 검증:** dev 전체 스위트를 직접 돌렸다. + +``` +$ bun run test (origin/dev@671a0df77) + 9908 pass + 0 fail +Ran 9915 tests across 619 files +``` + +관련 테스트 17건(`codex-history-job`, `codex-user-identity`, +`codex-inject-history-wording`)도 개별 통과. 결과적으로 dev는 깨지지 않았지만 +**그것은 운이고 절차는 위반됐다.** 다음 머지부터 exact-head CI 결론을 확인한다. + +**미해결 관찰:** 첫 전체 실행이 6 fail로 끝났고 재실행은 0 fail이었다. 나는 이걸 +"플레이키" 라고 적었는데, 그건 **입증되지 않은 단정**이다. 6 대 0은 분류되지 않은 +transient를 보여줄 뿐이며, 한 번의 clean run이 flakiness를 증명하지 않는다. + +더 나쁜 것은 **첫 실행의 실패 테스트명을 보존하지 못했다.** 같은 파일로 재실행하며 +덮어썼다. 무엇이 실패했는지 모르는 상태라 격리 재현조차 불가능하다. + +정직한 현재 상태: `origin/dev` 전체 스위트가 9908 pass / 0 fail로 관찰됐고, +그 이전 실행의 6 fail은 **원인 미상으로 남았다.** 다음 전체 실행 시 실패가 +재현되면 테스트명을 반드시 보존하고 격리 재현한다. + +### (2) #1155 코멘트에 틀린 기술 주장을 썼다 + +"`stream` 을 **생략**해도 buffered 분기에 도달한다" 고 썼는데 틀렸다. PR 자신의 +코드가 `const upstreamStreaming = deps.upstreamStreaming ?? true` 이므로 생략은 +스트리밍으로 귀결된다. **명시적 `stream: false`** 만 그 경로에 닿는다. + +기여자에게 공개적으로 남긴 잘못된 주장이므로 즉시 정정 코멘트를 달았다. 철회의 +본질(도달 가능하므로 close 부적절)은 유지되고 범위만 좁아진다. + +## 배운 것 — 기여자 attestation을 대신 체크하려 했다 + +draft 상태인 6건(#1189 #1187 #1184 #1195 #1169 #1266)을 머지하려다 게이트에 +막혔고, 체크리스트 2박스가 비어 있는 것을 보고 **내가 대신 체크했다.** + +`enforce-pr-target.yml:516-522` 를 읽고 나서 되돌렸다: + +```js +// The readiness gate applies to contributors (no push permission). +const checklistRequired = !authorIsMaintainer; +``` + +이 체크리스트는 **작성자 본인의 확인**이다 — "내 로컬에서 CI가 green이다", +"리뷰 지적을 다 반영했다", "리뷰 받을 준비가 됐다". maintainer가 대신 체크하면 +그건 확인이 아니라 위조다. 게이트를 통과시키려고 게이트가 지키려는 것을 없애는 +셈이다. + +6건 모두 원래 상태로 되돌렸다. 이들은 작성자가 직접 체크해야 진행된다. + +## 남은 것 + +| 대상 | 상태 | 필요한 것 | +|---|---|---| +| #1189 #1187 #1184 #1195 #1169 | draft | 작성자의 체크리스트 완료 | +| #1226 #1244 | CONFLICTING | 리베이스 | +| #1263 | 테스트 red | 작성자의 테스트 교체 | +| #1269 | 부분 수정 | `handleEnsure` 보완 | +| #1155 | 열림 | 작성자의 보완 | + +## 스크린샷 처리 + +`enforce-target` 은 GUI PR에 스크린샷을 요구한다. 리포지토리 관례를 따라 +orphan 브랜치 `pr-assets-1245-startup-stale` 에 이미지를 올리고 raw URL로 +참조했다(#1244가 쓴 방식과 동일). + +스크린샷은 목업이 아니다. 스텁 startup-health API를 붙여 GUI를 실제로 띄우고, +페이지의 Install 버튼과 Refresh 버튼을 브라우저에서 눌러 전후를 캡처했다. +before는 "Restart protected" 와 "Installation failed" 가 함께 있는 상태, +after는 실패 알림이 사라지고 shim이 "Not installed" 로 정확히 남은 상태다. diff --git a/devlog/_plan/260808_bug_campaign/020_wp2_sse_frame_contract.md b/devlog/_plan/260808_bug_campaign/020_wp2_sse_frame_contract.md new file mode 100644 index 0000000000..71b1cffd2d --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/020_wp2_sse_frame_contract.md @@ -0,0 +1,226 @@ +# 020 — WP2: SSE 프레임 파싱 계약 (#1219 + #1249) + +선행: WP1. 절차: `003_republish_protocol.md`. + +## 문제의 정확한 형태 + +이슈 #1219는 "SSE 프레임이 `null` 로 파싱되면 세 어댑터가 모두 크래시한다" 고 +보고했다. 현재 `origin/dev` 에서 그대로 재현된다. + +`src/adapters/openai-chat.ts:961-967`: + +```ts +chunk = JSON.parse(payload) as Record; +``` + +`:972`: + +```ts +if (chunk.error !== undefined && chunk.error !== null) +``` + +`JSON.parse("null")` 은 예외를 던지지 않고 `null` 을 반환한다. 캐스팅은 타입 체커만 +속일 뿐 런타임에는 아무것도 하지 않으므로 `null.error` 역참조가 일어난다. + +같은 결함이 세 곳 더 있다: `src/adapters/google.ts:500-510`, +`src/adapters/anthropic.ts:987-995`, `src/web-search/parse.ts:158-163`. + +## PR #1240 — 재작업 불필요로 정정됨 (2026-08-08 게이트 실행) + +> **이 절의 원래 결론은 뒤집혔다.** WP1 라이브 게이트가 #1240의 head 변경을 +> 잡아냈고(`f155138c` → `965dd9901`), 재검토 결과 **작성자가 이미 종료 동작을 +> `continue` 로 고쳤다.** 아래 분석은 왜 종료가 틀렸는지에 대한 기록으로 남기되, +> 우리가 직접 재구현하는 §020-1 계획은 **폐기한다.** 상세는 `011` 문서 참조. +> +> 새 계획: #1240을 채택한다. 코드 재작업 없음. PR 본문의 낡은 설명만 정정 요청. + +### 원래 분석 (기록용) + +#1240(snowyukitty)은 이 결함을 정확히 찾았지만 **처리 방식이 틀렸다.** 비레코드 +프레임을 malformed로 보고 스트림을 종료시킨다. + +이슈 스레드의 리포터 정정에 따르면 `data: null` 은 스트림 **중간에** 나타난다. +일종의 패딩/킵얼라이브다. 여기서 종료하면 뒤따르는 finish 청크와 `[DONE]` 을 +통째로 버린다. 즉 크래시를 응답 절단으로 바꾸는 셈이다. + +올바른 동작은 건너뛰기다. + +## 020-1 · #1219 — #1240 채택으로 대체 (직접 구현 폐기) + +원래 계획은 네 파서를 우리가 직접 고치는 것이었다. #1240의 새 head가 그 일을 +이미 정확히 해냈으므로 폐기한다. + +채택 대상: head `965dd990114fc6203297475142a28fcd7cb44642` +`Co-authored-by: snowyukitty <270071858+snowyukitty@users.noreply.github.com>` + +확인된 구현(재검토 근거): + +- `src/adapters/openai-chat.ts:978-980`, `src/adapters/google.ts:513-515` 가 + 비레코드 프레임에서 `return "continue"` +- Google은 `sawAnyFrame = true`(`:517`) 이전에 처리해 빈 스트림 가드 보존 +- Anthropic `:994-1001`, web-search `parse.ts:162-174` 도 건너뛰기로 일관 +- `tests/sse-null-data-frame.test.ts` 가 유효 청크 사이 `data: null` 후 완주를 + 확인(`:55-62`, `:101-107`)하고 전량 비레코드는 fail-closed 확인(`:109-116`) + +해야 할 일: PR 본문의 "emit ... error and terminate" 설명을 현재 동작에 맞게 +정정하도록 요청한다. `#1219` `Closes` 링크 확인. + +
+폐기된 직접 구현 계획 (기록용) + +새 브랜치 `codex/260808-sse-non-record-frames` +원작자 보존: `Co-authored-by: snowyukitty <270071858+snowyukitty@users.noreply.github.com>` +해소 이슈 **#1219**, supersedes **#1240** + +### MODIFY `src/adapters/openai-chat.ts` + +현재 `:961-972` 를 다음 형태로: + +```ts +let parsed: unknown; +try { + parsed = JSON.parse(payload); +} catch { + // 구문적으로 잘못된 JSON은 여전히 종료성 malformed 오류 + return malformedFrameError(payload); +} + +// 유효 JSON이지만 레코드가 아닌 프레임(null, 배열, 스칼라)은 패딩으로 보고 건너뛴다 +if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { + return "continue"; +} + +const chunk = parsed as Record; +``` + +핵심은 두 경우를 구분하는 것이다. **구문 오류는 종료**(진짜 손상된 스트림), +**유효 JSON 비레코드는 건너뛰기**(패딩). + +### MODIFY `src/adapters/google.ts` + +`:500-510` 에 동일 패턴. Google도 스트림 중간 패딩이 가능하므로 `continue`. + +### MODIFY `src/adapters/anthropic.ts` + +`:987-995`. Anthropic 경로는 기존대로 malformed/비레코드 프레임을 **건너뛴다** +(종료하지 않는다). 역참조 전에 형태를 검증하는 것만 추가한다. + +### MODIFY `src/web-search/parse.ts` + +`:158-163`. 사이드카도 건너뛰기 유지. + +### NEW `tests/sse-non-record-frames.test.ts` + +네 파서 각각에 대해: + +- `data: null` 이 유효 청크 **사이에** 있을 때 → 후속 청크와 `[DONE]` 이 온전히 + 처리된다 (이것이 #1240 대비 핵심 회귀) +- `data: []`, `data: 42`, `data: "text"` → 동일하게 건너뛴다 +- `data: {broken` → 종료성 오류 유지 + +활성화 증거(C-ACTIVATION-GROUNDING-01): 단순히 "크래시 안 함" 이 아니라, null +프레임 **이후** 청크가 실제로 소비되었음을 어서션한다. 종료 동작이었다면 red가 +되는 테스트여야 한다. 먼저 #1240 방식으로 구현해 red를 확인한 뒤 `continue` 로 +바꿔 green을 만든다. + +
+ +## 020-2 · #1249 빈 data 프레임 (스택 상단) + +원작자 `Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com>` +원본 브랜치 `Yuxin-Qiao:fix/openai-chat-empty-data-frame` +원본 커밋 `20c7afb5` +새 브랜치 `codex/260808-sse-empty-data-frame` (base: `codex/260808-sse-non-record-frames`) + +### MODIFY `src/adapters/openai-chat.ts` + +현재 `:951-963` 이 `trim()` 직후 `[DONE]` 검사와 `JSON.parse` 로 진행한다. 빈 +`data:` 는 `JSON.parse("")` 로 가서 종료성 오류가 된다. + +가드를 `[DONE]` 검사 **앞에** 넣는다: + +```ts +const payload = rawPayload.trim(); +if (payload.length === 0) return "continue"; +if (payload === "[DONE]") { ... } +``` + +### 두 변경의 최종 합성 결과 + +```ts +const payload = rawPayload.trim(); +if (payload.length === 0) return "continue"; // 020-2 +if (payload === "[DONE]") { ... } + +let parsed: unknown; // 020-1 +try { parsed = JSON.parse(payload); } +catch { return malformedFrameError(payload); } + +if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) { + return "continue"; +} +const chunk = parsed as Record; +``` + +두 훅은 같은 줄을 건드리지 않는다(020-2는 현재 953행 뒤 삽입, 020-1은 961행부터 +변경). 스택으로 쌓아도 충돌하지 않는다. + +### MODIFY `tests/sse-unspaced-data-fields.test.ts` + +빈 페이로드 케이스 추가. + +## 020-3 · #1205 reasoning placeholder 주입 (스택 상단) + +원작자 `Yuxin Qiao <104957188+Yuxin-Qiao@users.noreply.github.com>` +원본 브랜치 `Yuxin-Qiao:fix/issue-1193-reasoning-placeholder` +원본 커밋 `77ee3325b`, `48961a91e`, `61283a42f` +새 브랜치 `codex/260808-reasoning-replay-placeholder` (base: `codex/260808-sse-empty-data-frame`) +해소 이슈 **#1193** + +### 결함 + +`preserveReasoningContentModels` 가 replay 캐시에 의존하는데, 긴 세션에서 캐시가 +미스나면 reasoning 없이 bare `tool_call` continuation을 보낸다. DeepSeek thinking +모드가 이를 400으로 거부한다. + +### 수정 + +MODIFY `src/adapters/openai-chat.ts` — 캐시 미스 시 placeholder reasoning을 +주입해 계약을 만족시킨다. 같은 파일을 020-1, 020-2가 이미 건드리므로 이 순서로 +스택 상단에 놓는다. +MODIFY `src/providers/registry.ts`, `src/providers/derive.ts`, `src/router.ts`, +`src/types.ts` +MODIFY `src/oauth/index.ts`, `src/oauth/login-cli.ts`, `src/server/auth-cors.ts` +MODIFY `tests/deepseek-reasoning-replay-gaps.test.ts`, +`tests/oauth-provider-reconcile.test.ts` +MODIFY docs 5개 로케일 `reference/configuration/providers.md` + +**범위 확인 필요:** OAuth와 CORS 파일이 포함된 이유가 불명확하다. reasoning +placeholder와 무관해 보이므로 리베이스 시 해당 훅이 정말 필요한지 확인하고, +무관하면 제외해 범위를 좁힌다(`enforce-target` 의 focused-scope 체크리스트). + +활성화 시나리오: + +| 경로 | 트리거 | 관찰 | +|---|---|---| +| 캐시 미스 | replay 캐시를 비운 채 tool_call continuation 요청 | placeholder reasoning이 실제로 주입됨. 400 아님 | +| 캐시 히트 | 정상 캐시 상태 | 기존 reasoning 그대로 사용. placeholder 미주입 | + +캐시 히트 케이스가 중요하다. 항상 placeholder를 넣으면 원본 reasoning을 덮어쓴다. + +## WP2 수용 기준 (채택 기준으로 전환) + +§020-1이 폐기되어 신규 브랜치 생성 요구를 제거했다. 남은 것은 기여자 PR의 +착지 조건이다. + +- **#1240 채택**: CI green 확인 후 머지 승인 요청. 코드 수정 없음. 착지 시 + #1219 close +- **#1249 채택**: #1240 착지 후 리베이스가 필요한지 확인. 같은 파일의 다른 + 줄이므로 충돌은 없을 것으로 예상하되 실제로 확인한다 +- **#1205 채택 검토**: OAuth/CORS 훅이 reasoning placeholder와 무관해 보이므로 + 범위 확인. 무관하면 분리 요청. 착지 시 #1193 close +- 각 PR의 CI가 green이어야 한다. 우리가 새로 돌릴 로컬 검증은 없다 — + 기여자 브랜치의 CI가 그 역할을 한다 +- 공유 어댑터(`src/adapters/openai-chat.ts`)를 셋이 함께 건드리므로 **착지 + 순서를 정하고 각 착지 후 dev 전체 스위트 green을 확인**한다 +- null 프레임 이후 청크 소비 증거 확보 (종료 동작에서 red였음을 보인 기록 포함) diff --git a/devlog/_plan/260808_bug_campaign/030_wp3_ci_workflow_stack.md b/devlog/_plan/260808_bug_campaign/030_wp3_ci_workflow_stack.md new file mode 100644 index 0000000000..7a04ddf55c --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/030_wp3_ci_workflow_stack.md @@ -0,0 +1,136 @@ +# 030 — WP3: CI 워크플로 (#1259, #1185) + +선행: WP1. 절차: `003_republish_protocol.md`. + +## 스택이 해체된 경위 + +초안은 `#1255 → #1259 → #1185` 3단 스택이었다. **#1255가 2026-08-08T05:54:05Z에 +`d55b903d8` 로 머지되어 현재 `origin/dev` 그 자체가 되었다.** 스택 루트가 +dev에 흡수됐으므로 남은 둘은 각각 `origin/dev` 에서 분기하는 독립 PR이다. + +두 PR은 서로 훅을 공유하지 않는다(#1259는 `enforce-pr-target.yml` 의 CI-claim +검사와 하네스, #1185는 `tests/ci-workflows.test.ts` 의 어서션). 따라서 스택으로 +묶을 이유가 없다. + +다만 #1259는 여전히 하네스의 페이지네이션 로직을 건드리는데, #1255가 이미 그 +영역을 바꿔놓았다. 리베이스 시 **양쪽 메커니즘을 모두 보존**해야 한다 — 이제는 +자동 리베이스가 아니라 착지한 dev 코드와의 수동 합성이다. + +## 착수 전 차단 조건: #1265 상태 확인 (필수) + +#1259를 건드리기 전에 반드시 확인한다. + +```bash +gh pr view 1265 --repo lidge-jun/opencodex --json state,baseRefName,mergedAt,mergeCommit,labels +git fetch origin main dev +git log --oneline origin/main -5 +``` + +확인 항목: + +1. #1265의 상태와 타겟 (`main` 대상 핫픽스) +2. 그 내용이 `main` 에 착지했는지, 그리고 `dev` 와의 ancestry 관계 +3. 보안 검토가 완료됐는지 (워크플로 표면이므로 필수) + +#1265는 #1255와 같은 워크플로 파일을 건드린다. 이미 `main` 에 올라간 내용을 +`dev` 쪽에서 다시 만들면 승격 시 충돌한다. 이 확인 없이 #1259를 진행하지 않는다. + +## 보안 경계 (최우선) + +`.github/workflows/` 와 `enforce-pr-target` 은 AGENTS.md가 명시한 보안 검토 필수 +표면이다. 이 phase 전체가 security-sensitive다. + +검토 항목: 워크플로 권한 상승, 변경 가능한 서드파티 액션 ref, 시크릿 노출, +토큰 로깅. 셋 중 하나라도 걸리면 릴리스 블로커로 취급한다. + +## 030-1 · #1255 — 조치 없음 (머지 완료) + +`b73f6a42` 가 `d55b903d8` 로 dev에 착지했다. 재발행하지 않는다. + +착지한 내용: 코멘트 기반 워크플로 깨우기를 신뢰된 `status` 기반으로 교체 +(`.github/workflows/enforce-issue-quality.yml`, `enforce-pr-target.yml`, +`tests/helpers/enforce-pr-target-harness.ts` 등). + +**후속 확인 항목:** 이 변경에 두 방향 활성화 증거가 있는지 착지본에서 확인한다. +음성(임의 `issue_comment` 가 디스패치하지 않음)과 양성(신뢰된 `status` 는 +디스패치함) 양쪽이 있어야 "막았다" 와 "다 막아버렸다" 를 구분할 수 있다. 없으면 +#1185 PR에 회귀 테스트로 함께 추가한다. + +## 030-2 · #1259 페이지네이션 증거 (재작업) + +원작자 `luvs01 ` +원본 브랜치 `luvs01:agent/fix-ci-readiness-evidence` +원본 커밋 `a0810bc3` +새 브랜치 `codex/260808-ci-readiness-pagination` (base: `origin/dev`) + +### hygiene 실패의 진짜 원인 + +실패 로그: + +``` +##[error] PR hygiene failed: unsponsored_surface +``` + +테스트나 페이지네이션 문제가 아니다. `.github/workflows/enforce-pr-target.yml` 이라는 +**보호된 표면**을 건드려서, maintainer 보안 검토와 `maintainer-sponsored` 라벨이 +필요하다는 게이트의 정상 동작이다. + +처리: 코드를 고치는 게 아니라 보안 검토를 수행하고 라벨을 부여한다. 라벨 부여는 +maintainer 권한 행위이므로 이 캠페인 범위 안에 있다. + +### 코드 변경 + +MODIFY `.github/workflows/enforce-pr-target.yml` + +현재 `:647-666` 이 한 페이지만 읽는다: + +```js +checks.listForRef(... per_page: 100) // 한 번만 + .find(...) +``` + +체크가 100개를 넘으면 `ci` 체크가 두 번째 페이지에 있을 수 있고, 그러면 green인데도 +찾지 못해 준비완료 주장이 기각된다. + +변경: 전체 페이지를 순회한다. + +MODIFY `tests/helpers/enforce-pr-target-harness.ts` — **착지한 #1255 코드와 수동 +합성 필요.** #1255가 이미 dev에서 페이지네이션 픽스처/카운트 동작을 바꿔놓았다. +자동 리베이스에 맡기지 않고 양쪽 메커니즘을 모두 보존하도록 직접 합친다. + +MODIFY `tests/ci-workflows.test.ts` — 2페이지 커버리지. + +활성화 증거: 체크가 100개를 넘는 픽스처로 두 번째 페이지 순회 분기가 실제로 +발화하는지 확인. 100개 이하만 테스트하면 이 분기는 죽은 채로 남는다. + +## 030-3 · #1185 Windows shard 어서션 (독립) + +원작자 `luvs01 ` +원본 브랜치 `luvs01:agent/test-windows-ci-shard-command` +원본 커밋 `bff31d1e` +새 브랜치 `codex/260808-windows-shard-assertion` (base: `origin/dev`) + +MODIFY `tests/ci-workflows.test.ts` + +현재 `:166-169` 가 부분문자열 매칭을 쓴다. 워크플로 안의 `echo` 나 주석이 어서션을 +만족시켜, 실제로는 shard 명령이 없어도 테스트가 통과한다. + +변경: 정확한 실행 라인 어서션으로 교체. + +부모가 낡았으므로(`6d04574d`) 리베이스 필요. #1259와 훅을 공유하지 않아 깨끗하게 +적용되며, 스택이 아니라 독립 PR이다. + +활성화 증거: 워크플로에서 실제 shard 명령을 주석 처리한 상태로 테스트를 돌려 +red가 되는지 확인한다. 이게 이 변경의 존재 이유이므로 반드시 보여야 한다. + +## WP3 수용 기준 + +- **선행:** #1265 상태·타겟·ancestry·보안검토 확인 완료 +- #1255는 조치 없음 (머지 확인만) +- #1259와 #1185 두 PR이 각각 `origin/dev` 기반 독립 PR로 열림 +- #1259에 보안 검토 완료 및 `maintainer-sponsored` 부여 (없으면 hygiene이 + `unsponsored_surface` 로 계속 실패한다) +- #1259의 하네스 훅이 착지한 #1255 코드와 수동 합성되어 양쪽 메커니즘 보존 +- `bun install` 후 `bun test tests/ci-workflows.test.ts` green +- 워크플로 권한/시크릿/액션 ref 검토 기록 +- 030-2, 030-3의 활성화 증거 확보 diff --git a/devlog/_plan/260808_bug_campaign/040_wp4_catalog_sequential.md b/devlog/_plan/260808_bug_campaign/040_wp4_catalog_sequential.md new file mode 100644 index 0000000000..b1332f5cb5 --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/040_wp4_catalog_sequential.md @@ -0,0 +1,283 @@ +# 040 — WP4: 카탈로그/레지스트리 순차 처리 + +선행: WP1. 절차: `003_republish_protocol.md`. + +## 스택이 아니라 순차인 이유 + +일곱 PR이 `src/providers/registry.ts`, `src/codex/catalog/*`, `src/types.ts`, +`tests/codex-catalog.test.ts` 를 공유한다. 스택으로 쌓으면 하단이 바뀔 때마다 +상단 전체를 다시 쌓아야 하는데, #1244가 52커밋 57파일이라 캐스케이드 비용이 +감당이 안 된다. + +대신 한 건 착지 후 dev 리베이스, 그다음 건 순으로 간다. 각 단계가 독립적으로 +검증되므로 중간에 멈춰도 상태가 일관된다. + +순서: `#1224`, `#1226`, `#1178`, `#1266`, `#1244`, `#1163`, `#1228` + +앞의 셋이 카탈로그 코어와 프로바이더 발견을 정리하고, #1266이 그 위에서 Google +Vertex 재생 경로를, #1244가 Desktop picker 보존을, #1163이 combo 합성을 얹는다. +#1228을 맨 뒤에 두는 이유는 24파일 규모이면서 `registry.ts` 와 `types.ts` 를 +앞선 전원과 공유하기 때문이다. #1266이 #1178 바로 뒤인 이유는 둘 다 +Google/Antigravity 경로를 건드리기 때문이다. + +### 충돌 매트릭스 + +| 쌍 | 공유 표면 | 처리 | +|---|---|---| +| 1224 x 1178 | `provider-routes.ts`, `types.ts` | 영역이 달라 텍스트 충돌은 낮지만 둘 다 프로바이더 관리 동작을 바꾼다. 1178을 1224 뒤에 | +| 1226 x 1178 | `registry.ts`, `codex-catalog.test.ts` | 별개 프로바이더 항목과 테스트 블록. 텍스트 충돌 낮음, 의미 회귀 위험 중간 | +| 1178 x 1244 | `provider-fetch.ts`, convergence/sync, `codex-catalog.test.ts`, `types.ts` | 최고 위험. 1178은 라이브 발견/인증/캐시 흐름을, 1244는 수집·보존된 모델이 카탈로그 합성에서 살아남는 방식을 바꾼다 | +| 1226 x 1244 | 카탈로그 테스트와 메타데이터 가정 | 1244가 카탈로그 소유권을 재편하므로 DeepSeek 메타데이터 복원 테스트를 반드시 보존 | + +## 040-1 · #1224 프로바이더별 컨텍스트 캡 + +원작자 `xinweigao ` (13커밋, head `3e23b4b`) +원본 브랜치 `iF2007:fix/context-cap-per-provider` +새 브랜치 `codex/260808-context-cap-per-provider` + +MODIFY `src/server/management/provider-routes.ts` + +현재 `:644-655` 의 PUT이 `setAll` 값과 무관하게 항상 +`setGlobalContextCapValue(config, body.value)` 를 호출하고 capped 프로바이더를 +전부 지운다. 한 프로바이더의 캡만 바꾸려 해도 나머지 전부가 날아간다. + +변경: `setAll` 이 참일 때만 전역 적용, 아니면 지정 프로바이더만 갱신. + +MODIFY `src/providers/context-cap.ts`, `src/cli/models-runtime.ts`, +`gui/src/pages/Models.tsx` +MODIFY `docs-site` 5개 로케일의 `guides/model-routing.md`, +`reference/cli/providers-accounts.md`, `reference/configuration/providers.md` +MODIFY `tests/management-provider-validation.test.ts`, `tests/cli-headless-parity.test.ts` + +GUI 스크린샷 필수 (`gui/src/pages/Models.tsx` 변경). + +활성화 증거: `setAll: false` 로 한 프로바이더만 바꿨을 때 다른 프로바이더 캡이 +보존되는지 어서션. 현재 코드에서는 red가 되어야 한다. + +## 040-2 · #1226 DeepSeek V4 컨텍스트 창 + +원작자 `xinweigao ` (커밋 `a74325a`, `ad4459a`) +원본 브랜치 `iF2007:fix/deepseek-jawcode-metadata` +새 브랜치 `codex/260808-deepseek-jawcode-metadata` + +MODIFY `src/providers/registry.ts` — `:1295-1306` 의 DeepSeek 항목에 +`jawcodeBundle` 이 없고 `modelContextWindows` 가 `1_000_000` 이다. 라우팅된 +재빌드에서 정확한 컨텍스트 창이 소실된다. + +MODIFY `scripts/generate-jawcode-metadata.ts`, `src/generated/jawcode-model-metadata.ts` +MODIFY `tests/codex-catalog.test.ts`, `tests/provider-registry-parity.test.ts` + +워크트리 충돌 주의: 현재 체크아웃에 +`scripts/generate-jawcode-metadata.ts`, `src/generated/jawcode-model-metadata.ts`, +`tests/jawcode-metadata-sync.test.ts` 의 미커밋 변경과 미추적 +`scripts/jawcode-models.json` 이 있다. 사용자의 작업물이므로 건드리지 않는다. +이 PR은 별도 워크트리에서 작업하거나, 해당 파일 상태를 사용자에게 확인한 뒤 +진행한다. 이것이 이 phase의 유일한 차단 요인이다. + +## 040-3 · #1178 Antigravity 라이브 모델 발견 + +원작자 `Xinwei Gao ` (4커밋) +원본 브랜치 `iF2007:fix/antigravity-live-model-discovery` +새 브랜치 `codex/260808-antigravity-live-discovery` + +MODIFY `src/providers/registry.ts` — `:1290` 의 `liveModels: false` 를 해제 +MODIFY `src/providers/model-discovery.ts`, `model-discovery-limits.ts`, +`antigravity-models.ts`, `src/codex/model-cache.ts`, +`src/codex/catalog/provider-fetch.ts`, `src/codex/convergence-types.ts` +MODIFY `src/lib/pinned-http.ts`, `src/lib/provider-outbound.ts`, `src/oauth/index.ts`, +`src/server/management/oauth-account-routes.ts`, `provider-routes.ts`, +`src/server/responses/core.ts`, `src/config.ts`, `src/types.ts` +MODIFY docs 5개 로케일과 테스트 12개 파일 + +보안 민감 슬라이스다. OAuth 흐름, 아웃바운드 POST 하드닝, 캐시 동일성을 +한꺼번에 바꾼다. AGENTS.md 기준 명시적 보안 검토 대상이다. + +### 보안 게이트 (감사 블로커 5·9) + +"검토 기록" 과 `privacy:scan` 만으로는 불충분하다. `MAINTAINERS.md:48-59` 가 +요구하는 것은 명시적 검토, 담당자 지정, 출처 증거, 실패 경로 테스트다. PR을 +열기 **전에** 아래 산출물을 모두 확보한다. + +필수 산출물: + +1. 지명된 검토자와 검토 완료 기록 (누가, 무엇을, 언제) +2. Antigravity 모델 발견 엔드포인트의 1차 출처 증거 (공식 문서 또는 프로바이더 + 응답 캡처). 리버스 엔지니어링 추정만으로는 부족하다 +3. 아래 다섯 활성화 시나리오의 실행 증거 +4. `bun run privacy:scan` green + +활성화 시나리오 — 각각 트리거와 관찰 대상을 명시한다: + +| 경로 | 트리거 | 관찰 | +|---|---|---| +| SSRF 차단 | 사설 IP·메타데이터 주소·리다이렉트 체인을 발견 URL로 주입 | 요청이 거부되고 아웃바운드가 발생하지 않음 | +| 고정 대상 허용 | 정당한 Antigravity 엔드포인트 | 요청이 정상 통과 | +| 토큰 비직렬화 | OAuth 토큰 보유 상태로 캐시·로그·에러 경로 전부 통과 | 어느 출력에도 토큰 문자열이 없음 | +| 캐시 계정 격리 | 계정 A와 B로 각각 발견 수행 | 서로의 모델 목록이 섞이지 않음 | +| OAuth 실패 | 토큰 만료·거부 응답 주입 | 정적 목록으로 안전하게 폴백, 크래시 없음 | + +SSRF 차단과 토큰 비직렬화는 특히 중요하다. 둘 다 "정상 동작에서는 절대 발화하지 +않는" 경로이므로, 테스트가 트리거하지 않으면 코드가 있어도 죽어 있는지 알 수 없다. + +## 040-4 · #1244 Desktop picker 라우팅 모델 보존 + +원작자 `WZBbiao <16611004+WZBbiao@users.noreply.github.com>`, +`Wibias <37517432+Wibias@users.noreply.github.com>` (52커밋, head `67842aa`) +원본 브랜치 `lidge-jun:maintainer/supersede-1056-native-alias` +새 브랜치 `codex/260808-native-alias-picker` +해소 이슈 `#241` + +MODIFY `src/codex/catalog/sync.ts` + +현재 `:543-549` 의 보존 로직이 슬래시 유무로만 라우팅 행을 인식한다. Desktop +호환을 위해서는 bare native-alias 행(슬래시 없음)이 필요한데, 그런 행은 보존 +대상에서 탈락한다. `src/codex/convergence.ts:191-198` 도 같은 기준이다. + +변경: `opencodex_catalog_kind` 마커 기반으로 라우팅 행을 식별한다. 슬래시는 +더 이상 판별 기준이 아니다. + +57파일 전체 목록은 원본 PR 참조. 주요 축: `src/codex/catalog/*` 8파일, +`src/combos/*`, `src/server/management/*` 3파일, `gui/*` 3파일, docs 17파일, +tests 11파일, `structure/03_catalog-and-subagents.md`. + +GUI 스크린샷 필수. + +원본 리뷰에서 지적된 항목을 반드시 유지: 네이티브 복구와 백업 무결성(백업 오염과 +alias 소실 방지), native-alias 행이 라우팅으로 계수되는지. + +활성화 증거: bare native-alias 행이 remote `available_models` 필터링 이후에도 +살아남는지 확인하는 테스트. 현재 코드에서 red여야 한다. + +## 교차 work-phase 의존 (감사 블로커 8) + +## 040-5 · #1163 combo 카탈로그 폴백 + +원작자 eachann1024 +Co-authored-by: 关俊江 +원본 브랜치 `eachann1024:feat/combo-catalog-fallback` +원본 커밋 `39f677cb`, `99c63dbf` +새 브랜치 `codex/260808-combo-catalog-fallback` +순서: #1244 뒤 (둘 다 `src/codex/catalog/provider-fetch.ts`, `aggregation.ts` 공유) + +### 결함 + +`src/codex/catalog/provider-fetch.ts:1276-1284` 이 이미 발견된 멤버만 취한다: + +```ts +.map(target => memberByKey.get(targetKey(target))) +``` + +`src/codex/catalog/aggregation.ts:102-115` 이 target/member 짝과 양수 +`contextWindow` 를 요구하므로 결측 멤버가 있으면 combo 전체가 탈락한다. 또한 +`:134-136` 의 `member.reasoningEfforts ?? []` 가 없는 ladder를 **빈 배열**로 +만드는데, 이는 "제한 없음" 이 아니라 "아무것도 허용 안 함" 으로 해석된다. + +### 수정 + +MODIFY `src/codex/catalog.ts`, `src/codex/catalog/aggregation.ts`, +`src/codex/catalog/provider-fetch.ts` — 프로바이더 설정으로 불완전 멤버를 +합성하고, ladder 부재는 빈 배열이 아니라 와일드카드로 처리한다. +MODIFY `tests/codex-catalog.test.ts` +MODIFY `docs-site` 의 `reference/configuration/routing.md` (en, zh-cn) + +활성화 시나리오: + +| 경로 | 트리거 | 관찰 | +|---|---|---| +| 결측 멤버 합성 | combo 멤버 하나가 발견 목록에 없는 상태 | combo가 탈락하지 않고 설정 기반으로 합성됨 | +| ladder 부재 | `reasoningEfforts` 없는 멤버 | 빈 배열이 아니라 와일드카드. 모든 effort가 통과 | +| 정상 combo | 모든 멤버 완비 | 기존과 동일 (회귀 없음) | + +## 040-6 · #1228 Cursor 네이티브 이미지 지원 (대형 단독) + +원작자 `SB Yoon <44089734+yansigit@users.noreply.github.com>` (10커밋) +원본 브랜치 `yansigit:audit/cursor-dev` +새 브랜치 `codex/260808-cursor-native-images` +순서: WP4 **마지막**. 24파일로 대형이며 `src/providers/registry.ts` 와 +`src/types.ts` 를 앞선 항목들과 공유한다. + +### 내용 + +Cursor 어댑터에 네이티브 이미지 입력을 추가한다. 현재는 이미지가 사이드카 +경로로만 처리된다. + +MODIFY `src/adapters/cursor.ts` 및 `src/adapters/cursor/` 하위 7파일 +(`discovery.ts`, `effort-map.ts`, `images.ts`, `live-transport.ts`, +`protobuf-request.ts`, `request-builder.ts`, `types.ts`) +MODIFY `src/providers/registry.ts`, `src/types.ts` +MODIFY `docs-site/src/content/docs/reference/configuration/providers.md` +MODIFY 테스트 11파일 + 픽스처 `tests/helpers/cursor-grumpy-fixture.png` + +### 주의 + +protobuf 요청 빌더를 건드린다. Cursor는 Connect 프로토콜을 쓰므로 wire 형식이 +틀리면 런타임에만 드러난다. 정적 타입 통과가 정확성을 보장하지 않는다. + +활성화 시나리오: + +| 경로 | 트리거 | 관찰 | +|---|---|---| +| 네이티브 이미지 경로 | 비전 지원 Cursor 모델에 이미지 첨부 | protobuf 요청에 이미지 블롭이 실제로 실림. wire 하네스로 확인 | +| 비전 미지원 폴백 | 비전 미지원 모델에 이미지 | 기존 사이드카 경로 유지 | +| 이미지 없음 | 텍스트 전용 요청 | 기존과 동일 (회귀 없음) | + +`tests/cursor-vision-wire-harness.test.ts` 가 실제 wire 페이로드를 검사하므로 +이것이 핵심 증거다. 어댑터 단위 테스트만으로는 부족하다. + +## 교차 work-phase 의존 (감사 블로커 8) + +WP4가 WP5와 파일을 공유한다. 초안이 놓친 부분이다. + +| 충돌 | 공유 파일 | 순서 제약 | +|---|---|---| +| WP4 #1226/#1178 x WP5 #1145 | `src/providers/registry.ts` | #1226과 #1178이 먼저. #1145는 마지막 | + +따라서 WP5에서 WP4를 기다려야 하는 항목은 050-6(#1145) 하나다. 나머지 WP5 +항목은 WP4와 파일이 겹치지 않아 병렬 가능하다. + +초안에 있던 `#1244 x #1218` 제약은 삭제됐다. #1218이 2026-08-08T03:40:15Z에 +외부에서 CLOSED 되어 050-5가 실행 대상에서 빠졌기 때문이다. + +## 040-7 · #1266 Vertex thought signature 재생 + +원작자 `Ingwannu ` +head `c0ffaef643aee3a6b73f93db834cc6e4749b5728` (2026-08-08T06:21:28Z 기준. +최초 기록 `ae28b69ef` 에서 갱신됨 — 착수 전 재확인 필수) +원본 브랜치 `lidge-jun:agent/fix-1254-vertex-thought-signature` +새 브랜치 `codex/260808-vertex-thought-signature` +순서: #1178 뒤 (둘 다 Google/Antigravity 경로를 건드린다) + +감사 라운드 5의 라이브 게이트가 잡아낸 신규 PR이다. Vertex 경로에서 thought +signature가 재생되지 않는 문제를 다룬다. + +MODIFY `src/adapters/google.ts` +NEW `src/adapters/google-antigravity-replay.ts` +MODIFY `structure/04_transports-and-sidecars.md` +MODIFY `docs-site` 5개 로케일 `reference/adapters.md` +MODIFY `tests/google-vertex-thought-signature.test.ts` + +#1178이 `src/adapters/google.ts` 인접 영역과 Antigravity 발견 경로를 바꾸므로 +그 뒤에 리베이스한다. + +활성화 시나리오: + +| 경로 | 트리거 | 관찰 | +|---|---|---| +| signature 재생 | thought signature를 포함한 Vertex 응답 후속 턴 | 재생된 signature가 요청에 실림 | +| signature 부재 | signature 없는 응답 | 기존 동작 유지 (회귀 없음) | + +## WP4 수용 기준 + +- 일곱 PR이 순차로 열리고, 각각 직전 착지 head 위에 리베이스됨 + (#1224, #1226, #1178, #1266, #1244, #1163, #1228 순) +- #1228 단계는 `bun test tests/cursor-vision-wire-harness.test.ts` 를 **필수**로 + 포함한다. 이것이 protobuf wire 형식의 유일한 실증이며, `codex-catalog.test.ts` + 만으로는 Cursor 이미지 경로를 관찰하지 못한다. 함께 돌릴 것: + `tests/cursor-images.test.ts`, `tests/cursor-request-builder.test.ts`, + `tests/cursor-adapter.test.ts` +- 각 단계마다 `bun install` 후 `bun run typecheck` 와 + `bun test tests/codex-catalog.test.ts` green +- 1178 단계에서 위 보안 산출물 4종 전부 확보 +- GUI 변경 PR(1224, 1244)에 스크린샷 첨부 +- 1226은 워크트리 dirty 충돌 해소 후 진행 (사용자 확인 필요) +- 1228(Cursor 이미지, 24파일)은 대형 단독으로 마지막에 처리 (§040-6) diff --git a/devlog/_plan/260808_bug_campaign/050_wp5_orphan_issue_fixes.md b/devlog/_plan/260808_bug_campaign/050_wp5_orphan_issue_fixes.md new file mode 100644 index 0000000000..44ac5e252e --- /dev/null +++ b/devlog/_plan/260808_bug_campaign/050_wp5_orphan_issue_fixes.md @@ -0,0 +1,381 @@ +# 050 — WP5: PR 없는 버그 이슈 직접 수정 + +선행: WP2 (SSE 계약이 먼저 정리되어야 어댑터 계열이 안정된다). + +대상은 리포터가 증거를 냈는데 아무도 PR을 열지 않은 결함들이다. 각각 독립이라 +병렬 PR로 연다. + +## 050-1 · #1245 GUI Startup Safety stale error + +새 브랜치 `codex/260808-startup-install-result-reconcile` + +### 결함 + +새로고침이 health 데이터는 교체하지만 이전 `installResult` 를 조정하지 않는다. +대체 설치가 성공한 뒤에도 실패 메시지가 무기한 남는다. + +- `gui/src/pages/Startup.tsx:109` 이 갱신된 health를 받는다 +- `:250` 이 이전 오류를 그대로 유지한다 +- `gui/src/pages/startup-sections.tsx:146` 이 그것을 항상 렌더한다 + +### 수정 + +MODIFY `gui/src/pages/Startup.tsx` — `fetchStartup` 에서 `next` 파싱 직후: + +```diff + const next = await res.json() as StartupHealthData; ++if (next.status === "protected") { ++ setInstallResult(current => current?.kind === "error" ? null : current); ++} +``` + +성공 확인 메시지는 보존하고, health가 독립적으로 보호 상태를 증명했을 때만 +낡은 실패 UI를 지운다. + +NEW `gui/tests/startup-install-result-reconciliation.test.tsx` — +`gui/tests/startup-revisit-cache.test.tsx` 의 Happy DOM 픽스처 스타일을 따른다. +`install-shim` 실패를 만든 뒤 `status: "protected"` 를 반환하는 새로고침을 +시뮬레이션하고, 실패 텍스트가 사라지는지 어서션. + +GUI 스크린샷 필수. `bun run lint:gui` 필수. + +활성화 증거: 조건부 정리 분기가 실제로 발화해야 한다. 수정 전 red 확인. + +## 050-2 · #1236 — 폐기, PR #1268 채택 + +> 게이트 2차 실행에서 **PR #1268**(Ingwannu, head `4c40c569d`)이 같은 수정을 +> 이미 하고 있음이 확인됐다. `bin/ocx.mjs` 최종 spawn에 `windowsHide: true` 를 +> 넣고, `tests/ocx-launcher-source.test.ts` 에서 그 spawn 호출을 잘라내어 +> 확인하는 것까지 우리 계획과 동일하다. **직접 구현하지 않는다.** 상세는 `013`. + +
+폐기된 직접 구현 계획 (기록용) + +새 브랜치 `codex/260808-launcher-windows-hide` + +### 결함 + +최종 Node에서 Bun으로의 launcher spawn에 `windowsHide` 가 없다. 헤드리스 부모에서 +프록시를 시작하면 콘솔 창이 보인다. + +`bin/ocx.mjs:482-483` 이 `stdio: "inherit"` 를 쓰는데 옵션 객체에 `windowsHide` 가 +없다. + +### 수정 + +MODIFY `bin/ocx.mjs`: + +```diff + const child = spawn(bun, [...], { + stdio: "inherit", ++ windowsHide: true, + env: { +``` + +MODIFY `tests/ocx-launcher-source.test.ts` — `:16` 의 기존 테스트가 하듯 최종 +`spawn(bun, [cliPath...])` 호출을 잘라내어 그 옵션 객체 안에 `windowsHide: true` +가 있는지 어서션. + +한 줄 변경이지만 Windows 사용자 체감이 큰 항목이다. + +
+ +## 050-3 · #1230 — 축소, PR #1269 채택 + handleEnsure 보완 + +> 게이트 2차 실행에서 **PR #1269**(Ingwannu, head `8b7831ead`)가 `handleStart` 의 +> 순서를 정확히 우리 계획대로 고치고 있음이 확인됐다. 다만 **`handleEnsure` 를 +> 빠뜨렸다** — dev의 `src/cli/index.ts:441` 이 `:447` 의 liveness 확인보다 먼저 +> `reconcileJournal()` 을 호출하는 문제가 그대로 남는다. +> +> **새 계획:** #1269를 채택하되 `handleEnsure` 보완을 요청한다. 기여자가 원치 +> 않으면 후속 PR로 처리한다. 어느 쪽이든 **#1230은 두 함수가 모두 고쳐지기 +> 전까지 닫지 않는다.** 상세는 `013`. +> +> 회귀 테스트도 제안한다: #1269는 소스 문자열 순서를 보는 정적 테스트라 +> 리팩터링에 약하다. 아래 동작 테스트, 특히 음성 대조군을 함께 권한다. + +
+원래 직접 구현 계획 (보완 요청의 근거로 유지) + +새 브랜치 `codex/260808-start-journal-order` + +### 결함 + +`start` 와 `ensure` 모두 liveness 감지 **전에** journal을 조정한다. 이미 정상 +동작 중인 프록시가 있어도 journal 복원이 먼저 일어나 `config.toml` 을 되돌리거나 +카탈로그를 지운다. + +- `src/cli/index.ts:225` 가 `:226` 의 live-proxy 검사보다 먼저 `reconcileJournal()` +- `handleEnsure` 의 `:440-441` 도 같은 순서 + +### 수정 + +MODIFY `src/cli/index.ts` — `handleStart`: + +```diff + const requestedPort = parsePortOption(); +-if (!currentExternalCodexModelProvider()) reconcileJournal(); + const existingPid = readPid(); + if (existingPid) { + const live = await findLiveProxy(); + if (live) { ... exit ... } + removePid(existingPid); + } ++if (!currentExternalCodexModelProvider()) reconcileJournal(); +``` + +`handleEnsure` 의 `:441` 도 `findLiveProxy()` 조기 반환 블록 아래로 이동한다. +그러지 않으면 autostart 시도가 같은 파괴적 순서를 유지한다. + +NEW `tests/cli-start-journal-order.test.ts` — 격리된 `OPENCODEX_HOME`/`CODEX_HOME`, +죽은 journal PID, 별도로 띄운 정상 프록시 PID를 준비한다. 두 번째 `ocx start` 와 +`ocx ensure` 가 `config.toml` 복원이나 카탈로그 삭제 없이 "이미 실행 중" 으로 +종료하는지 어서션. 음성 대조군도 포함: 죽은 PID에 리스너가 없으면 여전히 조정된다. + +활성화 증거: 음성 대조군이 핵심이다. 조정 자체를 없앤 게 아니라 순서만 바꿨음을 +증명해야 한다. + +
+ +## 050-4 · #1196 issue-quality media 정규화 + +새 브랜치 `codex/260808-issue-quality-media-normalization` + +### 결함 (실측 확인됨) + +조사 중 현재 코어에 픽스처를 직접 돌려 확인했다. + +- `clean("")` 가 HTML 문자열 그대로 남는다 +- 들여쓴 ``/`` 를 가진 멀티라인 `` 가 media-only로 인식되지 않는다 + +원인 셋: + +- `.github/scripts/issue-quality-core.cjs:83` 이 media 자식 여부를 판정하기 전에 + 모든 들여쓰기 라인을 마스킹한다 +- `:100-107` 이 라인 위치로 복원한다. 멀티라인 HTML이 접힌 뒤에는 위치가 어긋나 + 보호된 코드가 손상될 수 있다 +- `:134` 가 비어 있지 않은 fallback 텍스트를 전부 실질 텍스트로 본다. 정확한 + placeholder도 포함된다 + +### 수정 + +MODIFY `.github/scripts/issue-quality-core.cjs`: + +```diff +-const protectedText = protectIndentedCodeLines(text); ++const protectedText = protectCodeSpans(text); + ... +-return restoreIndentedCodeLines(referenceStripped, protectedText.lines); ++return restoreCodeSpans(referenceStripped, protectedText.spans); +``` + +`protectCodeSpans` 요구사항: + +1. 펜스 코드 라인과 일반 들여쓰기 코드 라인을 각각 고유한 불투명 토큰으로 치환하고 + 토큰에서 원본 라인으로 가는 맵을 유지한다 +2. 들여쓰지 않은 ``, `