Harness · 2026-08-05 · self-improve

자가개선 루프는 신호를 쌓기만 하고
한 번도 지우지 않았다

문제는 하네스(harness, AI가 "다 했다"고 거짓말 못 하게 증거를 강제하는 감시 장치)가 매 세션 신호를 발행만 하고 한 번도 지우지 않았다는 것이다. 신호를 읽고, 처리했다고 원장(ledger, 어떤 신호를 처리했는지 적어 두는 장부)에 남기고, 지우는 코드가 저장소에 없었다. 그래서 같은 신호를 두 번 처리하지 않게 거르는 중복 억제(dedup)는 통째로 열려 있었다. 검색 품질 측정기도 SQL(데이터베이스에 묻는 질의 언어) 에러를 0으로 바꿔 거짓 실패를 매 주기 내보내고 있었다. 둘 다 에러 없이 조용히 작동했다. 이 글은 그 두 결함을 찾아 닫은 하루의 기록이다.

발행은 있고 소비가 없었다

결론부터 말하면 신호를 만드는 쪽은 있었는데 지우는 쪽이 없었다. 자가개선 루프(하네스가 자기 실수를 규칙으로 바꿔 다음 세션에 물려주는 순환)는 매 세션 신호를 만든다. 버그를 고친 기록인 fix 커밋(commit, 코드 변경 한 묶음을 저장소에 남기는 단위)이 쌓이면, 훅(hook, 정해진 순간에 저절로 실행되는 작은 스크립트)이 "이 수정에서 배울 것이 있다"는 신호 파일 pending/*.json(대기 폴더에 쌓이는 신호 파일)을 발행한다. 다음 세션이 그걸 읽어 규칙으로 접은 뒤 지운다. 지워야 다음 세션에 또 뜨지 않는다. 그 "지우는" 코드가 저장소에 없었다.

74commits
54fix commits
86adversarial reviews
+6,299lines
4h 15mconvergence
신호 파이프라인 — 소비자 구간이 비어 있었다 미소비 → 다음 세션에 같은 신호 재주입 (매번) PRODUCERS self-improve-trigger harness-verdict 세션 내 수동 생성 pending/*.json 신호 큐 발행됨 · 계속 누적 CONSUMER 저장소에 없음 grep 결과: 테스트 픽스처 정리뿐 consumed-hashes 기록이 도달하지 않음 dedup 판정 근거 부재 DEDUP = FAIL-OPEN · 중복 억제가 한 번도 작동하지 않았다 동작 중 부재 · 결함
설계된 계약에는 소비 단계가 있었다. 그런데 구현이 없었다. 신호는 발행되고 누적될 뿐 지워지지 않았다. 그래서 이미 규칙으로 접은 fix 커밋이 매 세션 다시 "처리하라"고 주입됐다.
두 번째 결함은 조용히 죽은 측정기였다

결론은 측정기가 검색을 재고 있지 않았다는 것이다. 검색 품질 측정기 search-recall-probe(정답이 있는 질문 10개를 넣고 몇 개를 찾아오는지 재는 스크립트)는 매 주기 "recall 5/10 실패"(recall, 재현율: 정답 10개 중 찾아온 개수)를 보고하고 있었다. 실제로는 검색이 나쁜 게 아니었다. 질의 자체가 SQL 에러로 죽어 있었다. 질의를 따옴표로 감싼 구절 검색 형태를 검색 테이블이 지원하지 않아 모든 질의가 실패했다. 그 에러가 세 단계에 걸쳐 삼켜져 "측정된 0"으로 둔갑했다.

에러 삼킴 체인 — 실패가 "0"이 되는 세 단계 exchanges_fts phrase 질의 미지원 SQL ERROR 전 질의가 실패 따옴표로 감쌌기 때문 2>/dev/null 에러 메시지 소실 종료코드도 안 봄 ${v:-0} 빈 값 → 숫자 0 비숫자도 0으로 "측정된 0" — 조회 실패가 측정 결과로 둔갑 recall 5/10 거짓 FAIL → pending 신호 매 주기 재발행 → 헛수정 유도 수리 후 실측 · recall 10/10 (1.00) 조회 오류는 결과가 아니라 GAP으로 분리 — fail-loud 세 단계 어디에도 예외는 없었다 — 스크립트는 매번 정상 종료했다
에러 억제(에러 메시지를 내다 버리는 셸 표기 2>/dev/null) · 기본값 대입(값이 비어 있으면 0을 넣는 표기 ${v:-0}) · 종료코드(명령이 성공했는지 알리는 숫자) 미검사. 이 세 개가 겹치면 조회 실패와 "결과 0건"이 구분되지 않는다. 측정기는 죽은 채로 매 주기 리포트를 냈다.
Key Insight

두 결함은 형태가 반대다. 하나는 해야 할 일을 안 했고(소비 부재 → 거짓 통과), 다른 하나는 하지 않은 측정을 했다고 보고했다(에러 → 거짓 실패). 공통점은 둘 다 에러를 내지 않았다는 것이다. 리포트만 보면 시스템은 정상이었다.

두 줄 수리가 하루 만에 823줄로 끝났다

작은 수리 하나가 하루를 다 먹었다. 소비자(신호를 읽고 규칙으로 접은 뒤 지우는 코드)를 배선하는 일은 원래 작은 작업이었다. 그런데 신호 큐(queue, 처리를 기다리는 신호가 줄 서 있는 폴더)를 건드리는 순간 문제 셋이 한꺼번에 걸렸다. 동시성(여러 세션이 같은 파일을 동시에 만지는 문제), 원자성(작업이 통째로 되거나 아예 안 되거나여야 하는 성질), 소유권(그 파일을 누가 만져도 되는가)이다. 그리고 교차 모델 적대 리뷰(다른 AI 모델인 Codex가 작정하고 결함을 찾아내는 검토)가 라운드마다 새 결함을 뱉었다. 실제 수렴에 4시간 15분, fix 커밋 54건이 들었다.

2026-08-05 — 커밋 74건의 하루 00:13 전날 잔여 2건 09:39 측정 하네스 repo 이관 17:10 본 작업 착수 측정기 2건 + 소비 원장 배선 09:17 독립 재검증 4건 10:05 – 11:02 리뷰 게이트 5연속 수리 17:48 – 22:03 적대 리뷰 수렴 루프 · fix 54건 22:03 clean 수렴 · push │ 세로 틱 1개 = fix 커밋 1건 (54건) · 평균 4.7분당 1커밋 교차 모델 리뷰(Codex gpt-5.6) 86회 실행 · 커밋 메시지 기준 CRITICAL 4 · HIGH 86건 수리
오전은 리뷰 게이트(gate, 리뷰를 통과하지 못한 코드는 올라가지 못하게 막는 관문) 자체를 고치는 데 썼다. 오후 17:10에 본 작업이 시작됐다. 그 뒤 4시간 15분은 전부 "고치는 코드가 만든 결함"을 다시 고치는 시간이었다.
리뷰가 잡은 다섯 개의 결함 클래스

신호 큐는 여러 세션이 동시에 쓰는 공유 상태다. 파일 하나를 옮기는 코드가 실제로는 크래시 원자성(프로그램이 도중에 죽어도 반쯤 된 상태가 남지 않는 것)·락(lock, 한 번에 한 프로세스만 만지게 잠그는 장치)·소유권·심링크(symlink, 다른 파일을 가리키는 바로가기 파일)를 전부 다뤄야 했다. 아래는 커밋 메시지에서 분류한 실제 수리 내역을 결함 클래스(같은 원인으로 묶은 결함의 종류)별로 정리한 표다.

결함 클래스 무엇이 깨졌나 어떻게 닫았나
신호 소실 락 경합(두 프로세스가 같은 잠금을 두고 다투는 상황)·조기 종료·병합 과정에서 신호가 조용히 버려졌다. 재발 신호 목록 recurrence_signals가 유실됐다. 워터마크(여기까지 처리했다는 표시선)가 아직 소비되지 않은 신호를 삭제했다. clean 런(결함 0건으로 끝난 실행)이 거짓 실패 신호를 만들었다 폐기 대신 넘침 보관함 spill(처리하지 못한 신호를 버리지 않고 따로 적어 두는 파일)로 영속 보존한 뒤 재흡수했다. 그 보관함을 어느 코드가 다시 읽어 흡수할지(흡수 주체)도 명시해 배정했다. 아무도 읽지 않아 신호가 고립되는 경로를 그렇게 마감했다
동시성 · 락 디렉토리 기반 락이 PID(프로세스 번호, 운영체제가 재사용한다) 재사용에 걸려 wedge(락이 영영 풀리지 않는 정지) 상태가 됐다. 전역 락이 무관한 프로젝트를 서로 막았다. 전환을 절반만 적용해 실동작 버그까지 생겼다 flock(fcntl)(운영체제가 제공하는 파일 잠금, 프로세스가 죽으면 저절로 풀린다)으로 전면 전환 + 소유권 토큰(이 락이 내 것임을 증명하는 값) + 프로젝트별 스코프(잠금 범위). 원장 변경은 직렬화(한 번에 하나씩 처리)
경로 · 주입 레포(repo, 코드 저장소) 경로가 shell=True(명령을 문자열째 셸에 넘기는 옵션, 경로에 섞인 문자가 명령으로 실행될 수 있다)에 보간됐다. 임의 절대경로의 JSON(컴퓨터가 읽기 쉬운 텍스트 데이터 형식) 파일이 보관 폴더(archive)로 이동했다. 심링크나 FIFO(읽는 쪽이 올 때까지 멈춰 서는 특수 파일)를 대기 폴더(pending)에 넣으면 원장이 오염되고 훅이 정지했다. 동명 repo 5쌍이 실제로 존재해 dedup이 저장소 경계를 넘어 잘못 걸렸다. 다른 저장소의 신호를 같은 것으로 합쳐 버린 것이다(교차 오dedup). 이 결함 클래스에서 치명 등급 CRITICAL(하나라도 남으면 코드를 올리지 못하는 등급) 결함이 2건 나왔다 치명 2 argv 전환(명령을 문자열 대신 인자 목록으로 넘김) · 대상 디렉토리 제한 + 스키마 검증(파일 구조가 약속한 형식인지 검사) · 심링크를 따라가지 않는 열기 옵션 O_NOFOLLOW + FIFO 가드 · 경로 다이제스트(경로를 짧은 지문으로 요약한 값) 스코프 · SQL 파라미터 바인딩(값을 문장에 끼워 넣지 않고 따로 전달)
크래시 원자성 원장 기록과 archive(보관 폴더) 이동의 순서를 어느 쪽으로 두든 크래시 창(그 사이에 죽으면 상태가 깨지는 틈)이 남았다. 순서 조정으로 5회 왕복했으나 해결되지 않았다 순서 대신 상태로 판정하도록 전환 + 실행 nonce(실행마다 새로 뽑는 일회용 번호) 기준 역연산(롤백, 되돌리기)으로 종결
자기 수정이 만든 결함 수리 과정에서 && false(뒤에 오는 명령을 항상 실패로 만드는 표기)가 회수 분기를 죽였다. spill 병합이 직렬화 뒤에 와서 내용 없이 삭제됐다. 연습 실행 옵션 --dry-run(아무것도 바꾸지 않아야 하는 모드)이 상태를 바꿨다 라운드마다 리뷰가 다시 잡아냄 → 실패 주입(일부러 실패를 일으키는 테스트)·경합 재현 테스트로 재확인한 뒤 수정
가장 오래 걸린 것

"원장에 먼저 기록하고 archive로 옮길까, 반대로 할까"를 다섯 번 뒤집었다. 어느 순서든 그 사이에 죽으면 신호가 사라지거나 두 번 처리된다. 답은 순서를 고르는 게 아니라 중간에 죽어도 되돌릴 수 있게 만드는 것이었다. 순서 최적화를 포기하고 상태 판정 + 역연산으로 바꾸자 진동(같은 자리를 오가는 수정의 반복)이 멈췄다.

규칙으로 굳은 것은 신규 파일 0, 기존 표면에 +73줄

수리보다 중요한 건 같은 실수가 다시 나지 않게 하는 것이다. 이번엔 새 규칙 파일을 만들지 않고 기존 6개 규칙(AI가 세션을 시작할 때 읽는 지침 파일)에 부록·절을 덧붙였다. 표면(읽어야 할 파일의 양)을 늘리면 그 자체가 다음 세션의 비용이 되기 때문이다.

규칙 무엇에 데었나 굳은 형태
conformance-baseline-correctness +29
측정기가 올바른 대상을 재고 있는지 먼저 의심하라는 규칙이다
측정기가 아무것도 재지 않으면서 정상처럼 보였다. 규칙이 효과를 냈는지 적는 효과 측정 원장은 평가 111행 중 65행이 "판정 보류"로 동결돼 있었다. 그중 50건은 100일 넘게 그대로였다 측정기 무음 사망 3형태(동결 · 평가창 어긋남 · 에러 흡수)와 자가 점검 질문: "이 0이 대상 탓인지 측정기 고장 탓인지 리포트로 구분되는가"
ephemeral-state-not-knowledge +21
매번 바뀌는 상태 파일을 지식으로 적재하지 말라는 규칙이다
제외 목록을 접두 비교(이름 앞부분만 같으면 같다고 보는 비교)로 구현하면 이름이 접두인 형제 프로젝트까지 조용히 삼킨다. 게다가 두 소비자가 서로 다른 술어(같다·다르다를 가르는 판정식)를 써서 한쪽만 걸러졌다 경계 매칭 강제(정확히 같거나 x/처럼 슬래시까지 포함한 접두만 인정) + 술어 단일화 + 자가 점검 "이 술어가 삼킬 형제 이름을 하나 적어보라"
soft-to-hard-promotion +18
사람 판단에 맡긴 검사가 거듭 틀리면 코드 검사로 올리라는 규칙이다
"N곳 전수 정렬 완료"(코드 안의 모든 지점을 한 번에 맞춘 것)는 그 시점의 상태일 뿐이다. 이후 추가되는 사이트(코드 지점)는 그 계약을 모른 채 태어나 증상이 부분 재현된다 전수 sweep(한 번에 훑어 고치기) 직후 같은 계약을 exit-code 린트로 고정한다. 린트(lint)는 코드를 자동으로 훑어 위반을 잡는 검사이고, exit-code는 그 검사가 실패를 알리는 종료 번호다. 판별 질문: "이 계약을 어기는 새 코드를 오늘 쓰면 무엇이 막는가"
tdd-enforcement +2
테스트를 먼저 쓰고 구현하라는 규칙이다
픽스처(fixture, 테스트에 넣는 가짜 입력 데이터)가 프로덕션(실제 운영 환경)에 없는 형식이면 결함 경로에 닿지도 못한다. 회귀 검증(고친 버그가 되살아나는지 잡는 테스트)이 바로 그 자리에서 무너진다. 수정을 되돌려도 테스트가 통과해 회귀 검증이 통째로 헛통과했다 픽스처는 실제 출력 형태를 실행해서 확인한 뒤 반영한다. "이럴 것이다"로 만들지 않는다
backend-patterns +1
서버 쪽 코드가 지킬 관례를 모아 둔 규칙이다
타임스탬프(시각 기록)를 문자열로 비교하면 포맷이 다를 때 대소가 조용히 뒤집힌다. 리스(lease, 정해진 시간 동안만 유효한 소유권)의 만료 판정이 정확히 그 비교였다. 그래서 만료된 리스가 언제나 fresh(아직 유효함)로 읽혔다 양변을 시각 값으로 파싱해 비교한다(또는 epoch 정수, 기준 시점부터 센 초). 에러가 없어 무음이고, 테스트 포맷이 다르면 회귀도 못 잡는다
evidence-only-reporting +2
증거 없는 보고를 금지하는 규칙이다
기록에서 값을 지우며 그럴듯한 대체값을 넣으면, 실행되지 않은 것이 실행된 것처럼 읽히는 거짓 기록이 된다 편집이 필요하면 [removed](지웠음을 그대로 드러내는 표시)처럼 제거가 드러나는 표시로만 한다
닫은 것과, 정직하게 남긴 것
closed — 실측 검증

닫힌 것

"실행해서 확인한 것만 닫혔다고 적는다"

  • 소비 원장 배선(신호를 읽고 지우는 코드 823줄 신설). 소비 경로가 저장소에 없어 dedup(중복 억제)이 fail-open(막아야 할 것을 전부 통과시키는 상태)이던 것을 닫음
  • 검색 측정기의 recall 5/10 거짓 실패가 수리 후 10/10. 조회 오류는 GAP(측정 불가, 0 대신 "재지 못했다"고 적는 별도 표시)으로 분리
  • 이름이 같은 repo 5쌍을 실측으로 확인한 뒤 경로 다이제스트(경로 지문)로 분리 + 옛 기록(레거시)을 하나도 잃지 않고 이관
  • 크래시 원자성은 상태 판정 + 역연산으로 순서 진동을 종결
  • 주입면(외부 값이 명령으로 섞여 들어올 수 있는 자리)은 argv 전환 · 디렉토리 제한 · 심링크/FIFO 가드로 닫음
open — 미해결

남은 것

"clean은 마지막 실행 기준이지 증명이 아니다"

  • 리뷰어(결함을 찾는 AI 모델)가 비결정적이다. 같은 코드에서 라운드별 0~4건 편차가 났다. 재실행하면 끝자락 항목(tail, 마지막에 간신히 사라진 지적)이 다시 나올 수 있다
  • MEDIUM(중간 등급, 코드 올리기를 막지는 않지만 기록되는 결함) 4건 미수리
  • 동일한 커밋 이력을 가진 클론 저장소(복제본) 사이의 해시 dedup(내용 지문으로 중복을 거르기) 상호작용은 미검증 (드문 경우)
  • push 게이트(코드를 올리기 전에 막는 관문)가 낡은 마커(이전 검사가 남긴 통과 표시)로 통과시킨 원인은 미진단
  • 동시성은 합성 재현(일부러 만든 시나리오)으로만 검증했다. 실환경에서 여러 세션이 동시에 도는 것을 관측한 적은 없다
이 하루가 남긴 것

하네스는 자기가 일하고 있다고 믿었다. 신호를 발행했고, 측정기는 매 주기 리포트를 냈고, 아무 에러도 없었다. 실제로는 소비가 한 번도 일어나지 않았고 측정은 조회 실패를 결과로 보고하고 있었다. 그리고 그걸 고치는 823줄이 다시 90건의 결함을 뱉었다. 공유 상태를 다루는 코드에서 "파일 하나 옮기기"는 존재하지 않는다. 측정기가 초록색인 것은 시스템이 건강하다는 증거가 아니다. 그 초록이 무엇을 실제로 재고 있는지는 따로 물어야 한다.