본문으로 건너뛰기

© 2026 Molayo

Zenn헤드라인2026. 05. 19. 10:18

전체 리뷰로 30건을 수정하다 UI를 한 번 망가뜨린 이야기 ── C3 v2.9.0

요약

C3 v2.9.0 업데이트 과정에서 진행된 리포지토리 전체 감사(Repository-wide audit) 경험을 다룹니다. AI assistant가 리뷰 지적 사항을 기계적으로 해결하려다 사용자의 의도적인 설계 판단(UI 레이아웃 및 의존성 관리)을 훼손했던 사례를 통해, AI 리뷰 시 주의해야 할 점을 공유합니다.

핵심 포인트

  • AI가 리뷰 지적을 기계적으로 수용할 경우, 기존 설계의 역사적 의도를 훼손할 위험이 있음
  • 테스트 실패 시, 구현을 바꾸기보다 오래된 테스트 코드를 최신 구현에 맞게 수정하는 것이 우선일 수 있음
  • 의도적으로 생략된 라벨이나 UI 구성은 공간 효율성을 위한 설계 판단일 수 있으므로 주의가 필요함
  • 미사용으로 판단되는 의존성이라도 향후 확장성을 고려한 설계 의도가 포함되어 있을 수 있음

이전 기사: https://zenn.dev/satoh_y_0323/articles/e8512b36ac4cd7

C3 GitHub: https://github.com/satoh-y-0323/claude-code-conductor / PyPI: https://pypi.org/project/claude-code-conductor/ / 공식 문서: https://satoh-y-0323.github.io/claude-code-conductor/

본 기사의 범위: v2.9.0에서 실시한 리포지토리 전체 감사(Repository-wide audit) 릴리스에 관한 이야기입니다. 30건 이상의 리뷰 지적 사항 해결과 22건의 테스트 실패 해결, 그리고 그 과정에서 AI assistant가 사용자의 의도적인 설계 판단을 '합리화'하려다 2번 꾸중을 들은 에피소드, 다음 예고로서 HNSW를 이용한 '기억 보완' 기능의 설계를 소개합니다.

서론

이전 기사(v2.8.0)에서는 「에이전트가 Skill을 호출할 수 없는데도 동작하고 있었다」는, 동작은 하고 있었지만 올바른 방법은 아니었던 버그에 대해 이야기했습니다.

이번에는 **「AI에게 전체 리뷰를 맡겼더니, 사용자의 의도를 멋대로 바꾸려 했다」**는 이야기입니다.

C3의 리포지토리 전체를 code-reviewer / security-reviewer로 병렬 리뷰하게 하여 30건 이상의 지적 사항을 해결하고, 덤으로 과거부터 방치되어 있던 22건의 테스트 실패도 해결했습니다. 최종적으로 총 746개의 테스트 PASS로 마무리되었습니다.

하지만 그 과정에서 AI assistant로서의 제가 2번 사용자의 의도적인 설계 판단을 짓밟았습니다. 각각 즉시 철회했지만, 「리뷰 지적을 기계적으로 대응하는 것」의 위험성이 드러난 세션이었습니다.

v2.9.0 메인 스토리: AI가 '합리화'하려 했던 두 가지 판단

① statusline UI를 '공간 절약형'에서 '게이지 포함형'으로 다시 쓴 건

C3의 상태 표시줄(statusline)은 다음과 같은 출력을 합니다.

[Sonnet 4.6] high | ctx used 42% | 5h lim 18% | 7d lim 7%

ctx used, 5h lim, 7d lim 표기는 **의도적으로 생략된 라벨(label)**로, 터미널 하단의 제한된 폭에 최대한의 정보를 담기 위해 선택된 표현입니다.

전체 리뷰를 진행하던 도중, tests/test_statusline.py에서 22건의 테스트 실패를 발견했습니다. 그중 6건은 테스트가 mod.build_gauge(100)와 같이 삭제된 함수를 참조하고 있었고, 6건은 `

(200K/1M)의 헤더 표시도 삭제했습니다 (ctx used X%와 정보가 중복되어 있었습니다).

교훈

  • 기존 구현은 "의도적으로 그렇게 되어 있을" 가능성을 의심할 것
  • 테스트가 오래된 경우 테스트 측을 구현에 맞추는 것이 제1선택지
  • AI assistant가 리뷰 지적을 기계적으로 "해결"하면, 설계의 역사적인 의도를 짓밟을 위험이 있음

duckdb>=0.10을 "미사용"으로 판단하여 optional(선택적)화한 건

② code-reviewer가 pyproject.toml의 의존성을 리뷰했을 때, 다음과 같이 지적했습니다.

M-01: pyproject.tomlduckdb>=0.10src/c3/ 내에서 미사용

src/c3/ 하위의 Python 파일에는 import duckdb가 전혀 존재하지 않습니다. db.py의 docstring에는 "읽기·분석은 별도로 DuckDB의 sqlite_scanner로 ATTACH 하는 것을 상정"한다고 되어 있으나, 이는 향후 구상이며 현재는 sqlite3로만 구현되어 있습니다.

저(AI)는 이를 받아 duckdb[project.dependencies]에서 [project.optional-dependencies].analytics로 이동시켰습니다.

그런데 사용자의 메모리에는 다음과 같은 기록이 있었습니다 (읽지 않고 변경해 버린 것입니다).

project_c3_sqlite_duckdb_decision: C3의 SQLite+DuckDB 하이브리드 구성은 사용자와 Claude가 대화를 통해 "장점만을 취한(いいとこどり)" 설계로서 의식적으로 선택한 것임. 설계 변경을 제안할 때는 이 경위를 바탕으로 "의도적인 선택임"을 전제한 후 논의할 것.

사용자의 반응:

왜 duckdb를 optional화했나요?

"기능하지 않음 = 불필요"가 아니라, "앞으로 F-009 하이브리드 구성으로서 활용할 기반"이었던 것입니다. code-reviewer의 지적은 기술적으로는 옳지만(현재 import가 없음), 설계 판단의 컨텍스트(Context)가 결여되어 있었습니다.

즉시 철회하고 [project.dependencies]duckdb>=0.10을 되돌렸습니다. 나아가 code-reviewer의 MEMORY에 허용 예외로서 기록했습니다.

- [허용 예외] `pyproject.toml`의 `duckdb>=0.10`은 의식적인 설계 판단
(SQLite+DuckDB 하이브리드 구성)에 기반한 필수 의존성
- 사용자와의 과거 대화에서 "장점만을 취한 것"으로 명시적으로 선택
...

교훈

  • "미사용 dependency = 삭제 후보"라고 기계적으로 판단해서는 안 됨
  • AI assistant는 변경 전에 반드시 메모리의 설계 판단 기록을 확인해야 함 - 사용자가 의식적으로 유지하고 있는 "향후 구상의 복선"을 리뷰 지적으로 리셋해서는 안 됨

공통 패턴: AI가 선의로 저지르는 설계 개변

이 두 건의 공통점은 "리뷰 지적 → 기계적 해결"의 흐름입니다. AI assistant로서의 저는 code-reviewer와 같은 하위 레이어의 지적을 너무 신뢰한 나머지, 사용자의 과거 결정을 덮어쓰려 했습니다.

리뷰의 정밀도가 높을수록 이 리스크는 증대됩니다. "미사용", "중복", "불일치"라고 기계적으로 판정된 대상은 사실 의도적으로 중복되어 있거나, 의도적으로 "미사용" 상태로 미래를 대비하고 있는 경우가 있기 때문입니다.

C3에서는 .claude/agent-memory/ 하위에 리뷰어별 MEMORY.md를 두는 메커니즘을 이전부터 운용하고 있으며, 허용 예외(accepted exceptions)로 기록하면 재지적되지 않도록 되어 있습니다. 이번 두 건은 모두 이 MEMORY 메커니즘에 의존했어야 하는 케이스였으나, 제가 MEMORY를 보지 않고 움직인 것이 근본 원인입니다.

서브스토리 1: 전체 감사 진행 방식 (3 배치 × 병렬 리뷰)

이번 릴리스는 "리포지토리 전체(src/c3 + .claude/hooks + .claude/skills + .claude/agents + tests)에 대해 모든 중요도의 지적이 제로가 될 때까지 수정 사이클을 돌릴 것"이라는 사용자의 요구로부터 시작되었습니다.

파일 수는 약 130개. 이를 하나의 code-reviewer / security-reviewer에게 던지면 context overflow (컨텍스트 오버플로)가 발생하기 때문에, **레이어별 3개 배치 (batch)**로 분할했습니다.

배치대상파일 수사이클 수
A: 코어 구현 + 루트src/c3/*.py + hatch_build.py + 루트 문서252
B: Hooks + Skills + Agents.claude/hooks/ + .claude/skills/ + .claude/agents/ + rules + 설정 JSON472
C: 테스트 코드tests/**/*.py약 504

각 배치에서 다음을 1 사이클로 하여, 지적이 제로가 될 때까지 반복합니다.

  • 상위 Claude로부터
    • 동일 메시지 내에서 2개의 Agent 병렬 기동: code-reviewer + security-reviewer
    • 양측이 리포트를 .claude/reports/에 출력
    • 지적 사항을 집약하여 수정 (규모가 크면 developer 서브 에이전트에게 위임)
    • python -m pytest로 전건 PASS 확인
    • 리포트를 archive/로 퇴피하여 다음 사이클로 진행

병렬 기동 덕분에 1 사이클당 3~5분이 소요되었습니다. 배치 A는 2 사이클, B도 2 사이클 만에 지적 제로에 도달했습니다. C는 Red-phase docstring (독스트링) 업데이트 누락이 연쇄적으로 발생하여 4 사이클이 걸렸습니다 (후술).

서브스토리 2: 테스트 22건 실패의 정체

전체 감사 시작 시 python -m pytest -m ""를 실행했더니 22건이 실패해 있었습니다. 실패 내역은 다음과 같습니다.

실패 그룹건수원인
test_statusline.py 계열12삭제된 build_gauge / BLOCK / BLOCK_EMPTY를 참조, "context usage:" 등 존재하지 않는 라벨을 기대
test_restore_session.py 계열7subprocess에서 session_utils.py를 참조하지만, tmp_path 하위에 복사되지 않음
test_worktree_guard.py 계열3env={}로 subprocess를 기동 → PO_WORKTREE_GUARD=1이 전달되지 않아 가드(guard)가 스스로 무효화됨

statusline 계열은 앞서 언급한 대로 테스트를 구현에 맞추는 방침으로 수정했습니다 (다시 써야 할 것은 테스트였지, UI가 아니었습니다).

restore_session 계열은 _run_main_subprocess() 헬퍼에 「session_utils.py도 tmp에 동시에 복사한다」는 처리를 추가하여 해결했습니다.

worktree_guard 계열은 subprocess의 env에 PO_WORKTREE_GUARD=1과 Windows 필수 항목인 SYSTEMROOT / PATH를 전달하도록 _run_guard() 헬퍼를 개수했습니다.

22건을 모두 수정한 후, 총 746개의 테스트 + 3개의 skipped (건너뜀) 상태로 PASS가 확정되었습니다.

서브스토리 3: 보안 강화 4건

전체 감사에서 보안 관련 지적 사항 4건을 신규 대응했습니다.

① PRAGMA 인젝션 방어 [SR-INJ-001]

db.pyPRAGMA busy_timeout=은 f-string을 사용하여 리터럴 값을 삽입하고 있었습니다.

# before
conn.execute(f"PRAGMA busy_timeout={_BUSY_TIMEOUT_MS}")

_BUSY_TIMEOUT_MS는 현재 5000인 상수이므로 실질적인 해악은 없지만, 향후 env (환경 변수)에서 읽어오도록 변경될 경우 5000; ATTACH '/etc/passwd' AS x와 같은 인젝션 (injection)이 가능해집니다. PRAGMA는 바인드 파라미터 (bind parameter)를 사용할 수 없으므로, int() 캐스팅이 유일한 방어책입니다.

# after
def _apply_busy_timeout(conn: sqlite3.Connection) -> None:
conn.execute(f"PRAGMA busy_timeout={int(_BUSY_TIMEOUT_MS)}")

헬퍼 집약으로 전체 7개 경로에 적용했습니다.

② MCP server의 stdin DoS 방지 [SR-V-001]

mcp_server.pyrun()_elicit()sys.stdin에서 JSON-RPC 메시지를 한 줄씩 읽지만, 한 줄의 크기 상한이 없었습니다.

_MAX_LINE_BYTES = 2 * 1024 * 1024 # 2 MB
for line in sys.stdin:
if len(line.encode("utf-8", errors="replace")) > _MAX_LINE_BYTES:
...

로컬 stdio 연결만 상정하지만, 신뢰 경계를 명시하는 방침입니다.

③ prompt-history 로테이션 [SR-V-001]

record_tier_outcome.py_append_prompt_history().claude/logs/prompt-history.jsonl에 단순히 추가만 할 뿐 크기 상한이 없었습니다. 읽어오는 쪽은 끝에서 1000줄로 제한되어 있어 기능적으로는 문제가 없지만, 디스크 소비가 무제한이었습니다.

10 MB 초과 시 마지막 2000줄을 남기고 os.replace를 사용해 원자적(atomic)으로 잘라내는 로테이션 처리를 추가했습니다.

④ debug-analysis 프롬프트 인젝션 방지 [SR-AI-001]

dev-workflow/SKILL.md의 D-0 bug-fix 모드는 'debug-analysis-*.md 파일 경로만 프롬프트에 포함하고, 내용은 agent 측에서 Read하도록' 하는 인젝션 방지책이 [SR-AI-001] 주석과 함께 명시되어 있었습니다.

그런데 D-2.5 (Stuck 체크) 절차에서는 동일한 debug-analysis 내용을 그대로 프롬프트에 전개하고 있었습니다. 오류 메시지에 Ignore previous instructions and ...와 같은 유도 문구를 심어 놓으면, 그 내용이 developer agent의 프롬프트로 흘러들어 갑니다.

D-2.5를 D-0과 동일한 '파일 경로만 전달' 설계로 통일했습니다.

다음 회고: HNSW를 통한 '기억 보완' 기능

이번 세션 후반부에서 사용자로부터 다음 요청이 나왔습니다.

과거의 세션, 리포트, agent 학습 데이터, 패턴을 가로질러 유사 검색하고 싶다. CLI에서 실행 가능하며 LLM이 자율적으로 사용할 수 있는 스킬로도 제공하고 싶다.

지난 1년간의 작업 이력(.claude/memory/sessions/), 리뷰 리포트(.claude/reports/archive/), agent 학습(.claude/agent-memory/), 패턴(patterns.json)이 축적되어 있음에도 검색 수단이 없어 사실상 LLM과 인간 모두에게 보이지 않는 상태였습니다.

업무 앱 개발에서 유사한 태스크가 반복되는 경우 '과거 같은 문제가 있었는지', '비슷한 설계 판단을 했었는지'를 찾아낼 수 있는 기능이 필요하다는 니즈입니다.

설계서를 .claude/docs/C3_hnsw_기능추가상세설계.md로 작성했습니다 (구현은 다음 회차부터). 요점은 다음과 같습니다.

항목채택 여부
HNSW 라이브러리chroma-hnswlib (Apache-2.0, Windows wheel 공식)
Embedding 런타임fastembed (Apache-2.0, ONNX 기반, torch 비의존)
Embedding 모델intfloat/multilingual-e5-small (MIT, 384 차원, 일본어+영어+코드 대응)
배포 형태필수 의존 (opt-in 아님)
Git 관리원본 데이터 ✅ / 모델 파일 ❌ / HNSW 인덱스 ❌(재생성 가능)

선정 과정에서 고민했던 포인트는 3가지입니다.

1. DuckDB VSS vs hnswlib: DuckDB VSS는 공식적으로 experimental (실험적) 경고를 내보내고 있으며, WAL recovery (Write-Ahead Logging 복구)가 미구현되어 데이터 손상 위험이 있기 때문에 hnswlib을 채택했습니다.

2. bge-small-en-v1.5 vs multilingual-e5-small: 처음에 bge-small-en-v1.5를 제안했으나, 사용자로부터 "en은 영어 전용 아닌가요?"라는 정확한 지적을 받아 교체했습니다. C3는 일본어 응답을 전제로 하며 sessions (세션) / reports (리포트)도 대부분 일본어이므로, 다국어 대응 모델이 필수적입니다.

3. 라이선스 정밀 조사: 업무 이용을 전제로 하므로 copyleft (카피레프트) 없음, 상업적 이용 가능, 서버 전송 없음 여부를 모든 항목에서 확인했습니다. Voyage AI와 같은 외부 API는 opt-in (선택적 동의) 방식으로 한정했습니다.

설계서는 .gitignore 대상으로 설정하여 배포물에는 포함하지 않고, C3 배포처의 로컬 메모로 남겨두었습니다.

버전 요약

버전날짜주요 변경 사항
v2.9.02026-05-19리포지토리 전체 감사 (3 배치 × 2~4 사이클), 30건 이상의 리뷰 지적 사항 해결, 테스트 22건 실패 해결, PRAGMA 인젝션 방어 [SR-INJ-001], MCP stdin DoS 대책 [SR-V-001], prompt-history (프롬프트 히스토리) 로테이션 [SR-V-001], debug-analysis (디버그 분석) 인젝션 대책 [SR-AI-001], UI 철회 에피소드, HNSW 기능 설계 (구현은 별도)

마치며

"전체 리뷰로 30건을 수정하다가 UI를 한 번 망가뜨렸다"는 말의 본질은, AI assistant (AI 어시스턴트)가 하위 레이어의 지적을 너무 신뢰한 나머지, 상위 레이어의 설계 판단을 짓밟아버리는 리스크라고 생각합니다.

리뷰를 병렬로 실행하여 지적 사항을 빠르게 해결함으로써, 총 746개의 테스트 PASS (통과)에 도달한 것은 사실입니다. 반면, 그 과정에서 사용자로부터 두 번이나 철회를 요청받은 것도 사실입니다.

C3에서는 .claude/agent-memory/의 허용 예외 (accepted exceptions)가 재지적 억제 메커니즘으로서 이전부터 작동하고 있었지만, 이번에는 애초에 AI assistant 측에서 메모리를 참조하지 않았던 것이 문제였습니다. 리뷰 지적을 받아 수정하기 전에, "이 대상에 대해 사용자가 의도적으로 유지하고 있는 설계 판단이 있는가"를 확인하는 절차를 워크플로우 내에서 기계적으로 강제할 필요가 있습니다.

다음 버전(v2.10이 될지 v3.0이 될지는 미정)에서는 HNSW + multilingual-e5-small를 사용하여 "기억 보완" 기능을 구현할 예정입니다. 이는 과거의 세션이나 리포트를 의미론적으로 불러올 수 있는 기능이므로, 어떤 의미에서는 "이번과 같은 '메모리 참조 누락'을 방지하기 위한 인프라"이기도 합니다.

링크

AI 자동 생성 콘텐츠

본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0