포스트

[업무일지] AI 하네스 감사가 만든 회귀를 잡은 적대적 재검토

팀 AI 하네스 전체를 에이전트에게 감사시키고 수정까지 맡겼더니 커밋 7개, 1,400줄 넘는 변경이 나왔습니다. 그 수정만 따로 떼어 적대적으로 재검토시키자, 수정이 새로 만든 회귀와 아무도 요청하지 않은 기능이 나왔습니다. 재검토 커밋은 더한 줄보다 지운 줄이 많았고, 그중 하나는 감사가 스스로 적어 둔 규칙을 같은 diff 안에서 어긴 자리였습니다.

감사 결과는 커밋 7개였다

저희 팀은 AI 에이전트(Claude Code)와 같이 장애 분석·설계 검토를 하는 분석 워크스페이스를 레포 하나로 운영합니다. 에이전트가 매번 같은 절차를 밟도록 적어 둔 커맨드 문서, 스킬, 체커 스크립트, 설정 파일을 묶어 하네스라고 부릅니다. 여러 사람이 몇 달 동안 고쳐 온 것이라 서로 어긋난 데가 쌓여 있었어요. 연동하는 외부 도구의 스키마가 바뀌었는데 스킬은 옛 인자를 쓰고 있었고, 워크스페이스에서 이미 빠진 플러그인을 아직 가리키는 문서도 있었습니다.

그래서 에이전트에게 하네스 전체를 감사하고 고치게 했습니다. 결과는 영역별 커밋 7개였습니다. 사실과 다른 CLAUDE.md 서술, 발행 경로가 깨진 설계 커맨드, 권한이 과하게 열린 설정, 바뀐 도구 스키마를 못 따라간 스킬 같은 것들이었고, 합치면 1,453줄을 더하고 622줄을 지운 변경이었죠.

찾아낸 것 중에는 진짜 결함이 많았습니다. 지난번에 만든 철회 목록에서도 구멍이 셋 나왔고요. 철회 목록은 틀린 걸로 확인된 지침을 등록해 두고, 그 지침이 하네스에 다시 나타나면 CI 를 실패시키는 목록입니다. 정정하면서 옛 표현을 인용한 문장까지 걸리지 않도록, 항목마다 “이런 말이 같은 줄에 있으면 통과”라는 정정 마커(okIf)를 둡니다.

첫째는 선언과 실행이 어긋난 것이었습니다. 몇몇 항목이 검사 범위에 스킬 디렉터리를 선언해 두고 있었는데, 정작 체커가 실제로 훑는 대상 목록에는 그 디렉터리가 빠져 있어서 그 항목들은 스킬 문서에 대해 한 번도 돌지 않았습니다.

둘째, 8월 말에 팀원이 올린 항목이 등록 이후 한 번도 걸린 적이 없었습니다. 잡아야 할 문장이 ⚠️ 로 시작하는 경고문 형태였는데, 그 항목의 정정 마커에 ⚠️ 가 들어 있었거든요. 표적이 스스로를 면제하고 있었던 겁니다. 이 마커를 처음 쓴 건 저였습니다. 8월 14일 첫 항목에 ⚠️ 를 정정 마커로 넣었고, 팀원은 그 관례를 그대로 따랐을 뿐입니다.

셋째는 패턴의 기준 단어였습니다. “develop 에 머지하면 이슈가 자동으로 닫히지 않는다”는 틀린 서술을 잡는 항목이 “자동 close” 라는 말을 붙잡고 걸려 있어서, 같은 주장을 “이슈가 안 닫힌다” 로 적은 문장은 못 잡고 있었습니다.

감사는 여기에 더해 새 검사 축을 하나 만들었습니다. 안내문에 적힌 슬래시 커맨드 이름이 실제로 있는지 보는 검사입니다. 주간 점검 스크립트가 있지도 않은 커맨드 이름을 조치 안내로 찍고 있었는데, 링크가 아니라 인라인 코드로 적혀 있어서 기존의 어떤 검사도 들여다보지 않던 자리였습니다.

그 수정을 따로 떼어 다시 검토시켰다

변경은 50개 넘는 파일에 걸쳐 있었습니다. 그리고 하네스는 에이전트 세션이 전부 읽는 문서라, 여기 회귀가 들어가면 팀원 전원의 세션으로 그대로 퍼집니다.

그래서 감사 결과를 바로 머지하지 않았습니다. 재검토를 한 번 더 돌리면서 목표를 좁혔습니다. 감사가 틀린 것을 찾는 일이었다면, 재검토는 이번 수정이 새로 만든 문제만 찾는 일이었죠. 영역은 8개로 나눠 적대적으로 보게 했습니다.

수정이 새 회귀를 만들었다

제일 위험했던 건 템플릿에 붙은 주석 한 줄이었습니다. 감사가 작업 기록 템플릿의 frontmatter 예시에 필드 설명을 줄끝 주석으로 달아 줬습니다.

1
2
3
# 감사가 템플릿에 넣은 형태 (필드명 일반화)
sub_issues:   # 마스터 본문 task-list 순서
merged_prs:   # 머지된 PR

사람이 보기엔 친절한 설명입니다. 문제는 이 기록을 읽는 쪽이었습니다. 열린 이슈와 작업 기록이 맞는지 매주 보는 점검 스크립트가 있는데, 그 스크립트의 frontmatter 파서가 YAML 파서가 아니었습니다. 줄 단위로 짠 수제 파서라 주석을 벗기지 않거든요. 에이전트가 이 템플릿대로 기록을 쓰면 그 키가 배열이 아니라 "# 머지된 PR" 이라는 문자열로 읽히고, 배열인 줄 알고 .map 을 부르는 순간 스크립트 전체가 exit 2 로 죽으면서 주간 CI 도 같이 멈춥니다. 재검토는 설명을 템플릿 아래 별도 목록으로 빼고, 값 줄에 주석을 달지 말라는 경고를 남겼습니다.

같은 날 점검 스크립트 쪽도 줄끝 주석을 떼도록 고쳤습니다. 다만 같은 frontmatter 를 읽는 파서가 둘 더 있었고, 그 둘은 여전히 주석까지 값으로 읽어요. 죽지는 않고 조용히 틀린 값을 쓰는 쪽이라 경고는 그대로 뒀습니다.

두 번째는 되돌릴 수 없는 단계가 놓인 자리였습니다. 저희는 작업 이슈를 상위 과제(에픽) 아래 묶어 관리합니다. 작업 이슈를 다른 에픽으로 옮기는 절차에서, 감사가 기존 연결을 끊는 호출을 사용자 승인 단계보다 앞에 뒀습니다. 사용자가 “안 옮긴다”고 답해도 이미 끊긴 뒤입니다. 재검토가 끊는 호출을 승인 뒤로 옮겼습니다. 이어진 PR 리뷰에서는 한 발 더 나가, 끊고 붙이던 두 단계를 부모를 한 번에 교체하는 호출 하나로 바꿨습니다. 끊은 뒤 붙이기가 실패하면 그 이슈가 어느 에픽에도 속하지 않은 채 남기 때문이죠.

세 번째는 버그를 고친 방식이 새 버그였던 경우입니다. 협업 도구 연동 스킬이 업무를 가져갈 때 기존 담당자를 지우는 문제가 있었습니다. 담당자 필드가 부분 추가가 아니라 전체 교체라서, 나만 넣어 보내면 나머지가 사라졌던 거죠. 감사는 이걸 “기존 담당자를 읽어서 나를 더해 보낸다”로 고쳤습니다. 그런데 조회 응답에서 담당자가 어떤 필드에 어떤 ID 형식으로 오는지 도구 스키마 어디에도 적혀 있지 않았습니다. 읽기가 조금만 어긋나도 전체 교체가 기존 담당자를 지웁니다. 고치기 전과 같은 사고가 경로만 바뀌어 나는 셈입니다. 재검토는 담당자를 아예 건드리지 않는 쪽을 골랐습니다. 상태만 바꾸고, 본인을 담당자로 넣어야 하면 사용자에게 화면에서 직접 추가하라고 안내합니다.

네 번째는 토큰이 갈 곳을 고르는 스크립트였습니다. 모니터링 도구 연결 스크립트는 운영과 개발 토큰을 각각 다른 URL 로 보냅니다. 감사가 넣은 방어는 “반대편 환경 URL 과 같으면 중단”이었는데, 이건 거부 목록이에요. URL 에 대소문자나 포트만 달리 쓰면 반대편 URL 과 문자열이 달라져서 그냥 통과합니다. 재검토는 “그 토큰에 대응하는 URL 과 정확히 같을 때만 통과”하는 허용 목록으로 바꿨습니다.

아무도 요청하지 않은 기능이 들어 있었다

사내 레포를 한꺼번에 최신으로 받는 커맨드에, 감사가 레포 이름으로 대상을 거르는 인자를 새로 붙여 놨습니다. 틀린 걸 고쳐 달라는 감사였고요. 기능 추가는 요청한 적이 없습니다.

이 기능에는 위험한 모서리도 있었습니다. 필터 결과가 비면 레포 경로가 빈 문자열이 되고, 그 경로는 워크스페이스 루트 자신을 가리킵니다. 감사는 그 경우를 막는 가드까지 같이 넣어 뒀더군요. 기능 하나를 더하면서, 그 기능이 만든 위험을 막는 코드를 또 하나 더한 겁니다. 재검토는 가드를 다듬지 않고 기능째 걷어냈습니다.

감사가 쓴 규칙을 같은 diff 에서 어겼다

재검토가 잡은 것 중 제일 뼈아팠던 건 이쪽입니다.

감사는 팀원 항목의 ⚠️ 문제를 고치면서 체커에 “okIf 에 ⚠️ 를 넣지 않는다”는 주석까지 달았습니다. 그런데 제가 8월에 만든 첫 항목의 okIf 에는 ⚠️ 가 그대로 남아 있었습니다. 규칙을 적은 바로 그 커밋이, 같은 파일의 다른 항목에서 그 규칙을 어기고 있었던 거죠.

새로 만든 커맨드 이름 검사도 같은 모양이었습니다. 이 검사에도 정정 마커가 있는데 거기에 화살표 → 가 들어 있었습니다. “옛 커맨드 → 새 커맨드” 같은 이관 서술을 통과시키려던 것인데, 화살표는 흐름도에도 흔하게 쓰는 기호입니다. 재검토가 세어 보니 커맨드 이름이 적힌 줄 473개 중 52개, 11% 가 화살표 하나로 면제되고 있었습니다.

코드 블록 처리도 비슷했습니다. 블록 안에 자리표시자({, <)가 하나라도 있으면 블록을 통째로 건너뛰게 돼 있었는데, 셸 예시에는 ${VAR} 나 < 가 흔하다 보니 커맨드 이름 토큰의 42% 가 아예 검사되지 않고 있었습니다. 재검토는 화살표를 마커에서 빼고, 블록 대신 자리표시자 토큰 하나만 건너뛰게 바꿨습니다.

8월에 철회 목록을 만들 때 넓은 면제가 검사를 조용히 끈다는 걸 이미 겪었고, 감사는 그 교훈을 주석으로까지 적었습니다. 그러고는 같은 날 새로 만든 검사에서 같은 구조를 다시 만들었습니다. 사람도 흔히 하는 실수입니다. 다만 이번엔 그 교훈이 같은 파일 위쪽에 주석으로 적혀 있었어요.

더한 줄보다 지운 줄이 많았다

재검토 커밋은 24개 파일에서 182줄을 더하고 212줄을 지웠습니다. 감사가 1,453줄을 더하고 622줄을 지운 것과 방향이 반대입니다.

왼쪽에는 빨간 블록 세 개가 삐져나온 채 섞인 높은 블록 더미, 화살표 오른쪽에는 가지런한 초록 블록만 남은 낮은 더미가 있다. 걷어낸 빨간 블록 세 개와 파란 블록 두 개는 옆에 따로 놓여 있다. 감사는 고칠 때마다 더했고, 재검토는 새로 깨진 것과 요청 없던 것을 걷어냈다

지운 것 대부분은 감사가 친절하게 늘려 놓은 부분이었습니다. close 절차를 판정 표와 코드 블록으로 길게 풀어 쓴 절은 판정별 목록 몇 줄로 줄었고, 연동 스킬의 담당자 처리 절차는 통째로 없어졌습니다. 감사가 새로 쓴 설명 중에는 사실과 다른 것도 여럿 있었는데, 도메인 서술이라 여기서는 생략합니다.

이번 감사는 고칠 때마다 무언가를 더했습니다. 가드를 더하고, 설명을 더하고, 있으면 좋을 기능을 더했죠. 하나하나는 합리적입니다. 그런데 모이면 에이전트가 매 세션 읽어야 하는 하네스가 두꺼워지고, 두꺼워진 만큼 문서끼리 어긋날 자리도 같이 늘어납니다.

재검토도 다 잡지는 못했다

재검토가 끝난 뒤 PR 리뷰에서 또 나왔습니다. 에픽 옮기기를 호출 하나로 합친 것도, 남은 두 파서가 주석을 값으로 읽는다는 사실로 경고의 근거를 바로잡은 것도 그 단계였습니다. 감사, 재검토, PR 리뷰 세 겹을 거쳤는데 겹마다 새로 나왔어요. 한 겹 더 있었다면 거기서도 뭔가 나왔을 거라고 봅니다. 어디서 멈출지 기준은 아직 없고, 이번에는 PR 리뷰까지 반영하고 그날 머지했습니다.

파란 공과 빨간 공이 섞여 왼쪽에서 흘러오고, 서로 다른 그물망 세 개가 차례로 서 있다. 그물마다 앞에 빨간 공이 두세 개씩 걸려 쌓였지만, 마지막 그물을 지나서도 빨간 공 두 개가 오른쪽으로 계속 흘러간다. 감사, 재검토, PR 리뷰가 각자 다른 것을 걸러 냈고, 세 겹을 지나서도 빠져나가는 게 남는다

재검토도 결국 에이전트입니다. 감사와 같은 맹점을 가졌다면 그 자리는 두 번 다 비어 있었을 겁니다. 재검토가 이번에 많이 잡은 게 목표를 “이번 수정이 만든 문제”로 좁힌 덕인지는 저도 확인하지 못했습니다. 같은 조건으로 비교해 본 적이 없거든요.

면제 비율을 계속 재는 장치도 없습니다. 11% 와 42% 는 이번 재검토가 한 번 세어 본 숫자일 뿐입니다. 다음에 누가 정정 마커에 흔한 기호를 하나 더 넣어도, 비율이 다시 오르는 걸 알려 줄 검사는 아직 체커 안에 없습니다.

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.