
미래의 변경 마찰(change friction)을 측정하는 CI 게이트를 구축했습니다. 그런데 이 게이트가 자체 검증에서 실패했습니다.
요약
미래의 코드 변경 비용(change friction)을 측정하기 위한 CI 게이트인 MORROW 프로젝트를 소개합니다. 코딩 에이전트의 작업 궤적을 분석하여 코드 변경이 향후 작업 난이도에 미치는 영향을 정량화하려는 시도입니다.
핵심 포인트
- MORROW는 미래의 변경 작업 비용을 측정하는 CI 게이트 구축을 목표로 함
- 에이전트의 파일 읽기 수, 테스트 사이클, 변경 라인 수를 통해 지표 집계
- 실험 프로토콜을 통해 main 브랜치와 후보 브랜치의 에이전트 수행 차이 측정
- 현재 프로토타입은 자체 유효성 검증 실패 사례를 투명하게 공개함
Agents of SigNoz의 Track 01 제출물로, 코딩 에이전트가 수행해야 했던 작업을 측정하고, 대조군이 실패했을 때 그 결과를 신뢰하기를 거부하는 과정에 관한 내용입니다.
여러분이 지금까지 추가한 모든 CI 게이트는 현재 시제의 질문에 답합니다. 단위 테스트 (unit tests)가 통과하는가? 컴포넌트들이 서로 잘 맞는가? 알려진 취약점이 존재하는가? 모두 유용한 질문들이며, 모두 '지금'에 관한 것입니다.
하지만 그 어떤 것도 여러분 앞에 놓인 풀 리퀘스트 (pull request)가 다음 달의 작업 비용을 더 비싸게 만들었다는 사실을 알려줄 수 없습니다. 그 질문에는 테스트가 없기에 아무것도 이를 차단하지 못하며, 결과적으로 문제는 축적됩니다.
MORROW는 이를 측정 가능하게 만들려는 시도입니다. 미래의 변경 작업 (future change task)이 사전에 등록됩니다. 실험 프로토콜은 main 브랜치와 후보 브랜치 모두에 대해 동일한 작업을 실행합니다. 동일한 모델, 동일한 프롬프트 (prompt), 동일한 리소스 제한을 사용하며, 각 실행은 별도의 컨테이너에서 이루어집니다. 이때 에이전트가 수행해야 했던 작업의 차이가 바로 측정값이 됩니다. 현재 프로토타입은 결과 증거를 카세트 (cassettes)로 기록하고 결정론적 검증 (deterministic verification) 및 CI 게이팅 (CI gating)을 구현하며, 자동화된 풀 리퀘스트 오케스트레이션 (pull-request orchestration)이 다음 단계입니다.
에이전트가 측정 도구입니다. 실행당 세 가지 항목이 집계됩니다: 에이전트가 읽어야 했던 서로 다른 파일의 수, 소모한 테스트 사이클 (test cycles)의 수, 그리고 실제로 변경한 라인 수입니다.
그리고 이 포스트의 대부분을 차지하는 솔직한 부분입니다: 제 실험은 자체 유효성 검증 (validity check)에서 실패했으며, 저는 그 결과를 그대로 배포했습니다.
$ morrow verify cassettes/treatment-replace-cache
ERROR · INVALID_EXPERIMENT · exit 2
report reproduced byte for byte, and the re-derived verdict is INVALID_EXPERIMENT:
...

SigNoz가 결합되는 방식
에이전트의 궤적 (trajectory)은 원재료이며, 궤적은 트레이스 (trace)입니다. 이 대응 관계는 매우 정확하여 별도의 복잡한 매핑 기술이 필요하지 않았습니다:
morrow.experiment ← 등록된 작업 하나, 후보 하나
└── morrow.pair ← 하나의 베이스라인/후보 비교
├── morrow.run ← 하나의 에이전트 실행
...

한 쌍(pair)의 두 팔(arms)은 동일한 부모 아래 나란히 놓여 있으므로, 그들 사이의 차이는 트레이스 (trace)의 형태이며, 별도로 찾아봐야 하는 숫자가 아닙니다. 기록된 처리 트레이스 (treatment trace)에서 베이스라인 실행은 18개의 파일을 읽고, 1개의 테스트 사이클을 소모하며, 129줄을 작성합니다. 반면 후보(candidate)는 16개를 읽고, 4개를 소모하며, 487줄을 작성합니다. 숫자를 단 하나도 읽지 않고도 한 서브트리 (subtree)가 눈에 띄게 더 조밀하다는 것을 알 수 있습니다.
저는 Foundry를 사용하여 SigNoz를 셀프 호스팅했습니다. 전체 배포 계약 (deployment contract)은 casting.yaml 및 casting.yaml.lock으로 커밋되어 있으며, 의미 있는 부분은 작습니다:
apiVersion: v1alpha1
kind: Installation
metadata:
...
foundryctl cast -f casting.yaml 명령어가 스택을 실행합니다. MORROW는 Python OpenTelemetry SDK를 사용하며, OTLP gRPC를 통해 localhost:4317로 트레이스 (traces)를 내보냅니다.
이를 연결하면서 배운 세 가지 실질적인 사항입니다.
에러 없이 내보내기(Exporting)를 했다는 것이 데이터가 도착했다는 증거는 아닙니다. 저의 첫 번째 내보내기는 "성공"했지만, 제가 찾을 수 있는 데이터는 아무것도 저장되지 않았습니다. 저는 익스포터 (exporter)의 반환 값을 신뢰하는 것을 중단하고, ClickHouse에 직접 쿼리하는 리드백 (readback)을 작성했습니다:
uv run python scripts/signoz_query.py --minutes 15
file.read 220
patch.apply 88
command.run 78
...

이는 의도적으로 SigNoz HTTP API를 거치지 않고 ClickHouse로 직접 전송됩니다. 세션도, 토큰도, 파일에 저장할 것도 없으며, 결과값은 렌더링된 형태가 아닌 저장된 행(rows) 그 자체입니다. 정확한 총합은 465개의 스팬 (span)입니다: 실험(experiment) 3개, 쌍(pair) 7개, 실행(run) 14개, 그리고 내보내기 된 이벤트 스팬 (exported event spans) 441개입니다. 카세트 (cassettes)에는 1,553개의 소스 이벤트가 유지됩니다; 1,112개의 불투명한 프로바이더 이벤트 (opaque provider events)는 의도적으로 스팬 (span)으로 변환되지 않습니다.
컬렉터 (collector)는 첫 번째 조직 (organisation)이 존재하기 전까지는 등록될 수 없습니다. 조직을 생성하기 전까지, 컬렉터는 다른 부분은 건강해 보임에도 불구하고 cannot create agent without orgId 메시지를 내며 루프를 돕니다. 저는 이 문제 때문에 내보내기 도구 (exporter)를 디버깅하는 데 꽤 많은 시간을 보냈습니다.
모든 이벤트가 스팬 (span)이 될 가치가 있는 것은 아닙니다. 하나의 실행 (run)에서 22개의 실제 액션 (actions)에 대해 측정 신호 (measurement signal)를 포함하지 않는 108개의 이벤트가 생성되었습니다. 이들은 게시된 증거 (published evidence)에 그대로 남아 있어 독자가 직접 셀 수는 있지만, 스팬 (span)이 되지는 않습니다. 5/6가 노이즈인 트레이스 (trace)는 관측 가능성 (observability)이라고 할 수 없습니다.
또한 의도적으로 한 가지 필드를 제외했습니다. 재생된 실행 (replayed runs)에는 벽시계 시간 (wall-clock duration)이 포함되지 않습니다. 벽시계 시간 (wall time)은 작업을 수행한 머신의 속성이지, 작업 자체가 요구한 작업량의 속성이 아니므로 게시된 증거 (published evidence)에 포함하지 않았습니다. 0을 내보냈다면 아무도 측정하지 않은 숫자가 대시보드에 표시되었을 것입니다.
작동하지 않은 부분을 포함한 결과
열 개의 컨테이너 실행 (container runs). 하나의 작업 (task), 하나의 모델 (claude-sonnet-5), 동일한 제한 사항 (limits).
변동 (Churn)은 어느 쪽 팔 (arm) 순서로 정렬하든 기술적으로 구분되었습니다. 모든 처리 쌍 (treatment pair)은 3.75에서 5.11 사이에 위치했습니다. 반면 대조군 (null control) — 동일한 트리의 두 복제본 — 은 2.22 미만에 머물렀습니다.
집계된 판결 (aggregate verdict)은 신뢰할 수 없었습니다. 대조군 (null)은 대칭적이지만, MORROW의 집계 (aggregation)는 대칭적이지 않습니다. 개선 사항은 1에서 하한선이 정해지므로, 한 축에서의 퇴보 (regression)가 다른 축에서의 개선으로 상쇄될 수 없습니다. 팔 (arm) 레이블을 바꿨더니 대조군 (null) 값이 1.0000에서 1.7403으로 이동했으며, 이는 게시된 허용 오차 범위인 1.20을 넘어섰습니다.
설계 단계에서 데이터가 존재하기도 전에, 이 정확한 실패 모드 (failure mode)를 문서로 예상해 두었습니다:
단방향 집계 (one-sided aggregation)의 비용은 대칭적인 노이즈 (symmetric noise)조차 상향 편향 (bias upward)된다는 점입니다. 그 편향을 상쇄하는 것이 바로 귀무 대조군 (null control)입니다.
대역 (band)을 벗어난 귀무 값 (null)에 대해 사전 등록된 규칙은 대역을 넓히는 것이 아니라, 해당 실험을 INVALID_EXPERIMENT로 보고하는 것입니다. 따라서 이것이 공개된 카세트 (cassette)에 기록된 내용이며, CI가 단언하는 내용이자, 데모 영상이 시작되는 지점입니다.
저는 양쪽 팔 (arm)의 순서를 모두 커밋했으므로, 어느 쪽도 데이터를 확인한 후에 선택한 것이 될 수 없습니다. files_read_distinct는 신호가 없음을 보여주었으며 (0.89, 0.81, 1.42), 이 또한 보고되었습니다.
단언하는 대신 검증 가능한 결론 만들기
믿어야만 하는 대시보드는 믿어야만 하는 주장과 별반 다르지 않습니다. 따라서 각 실험에 대한 증거는 **카세트 (cassette)**로서 저장소에 커밋됩니다. 여기에는 정규화된 이벤트 (normalized events), 이탈 횟수 (churn counts), 런처 종료 코드 (launcher exit codes), 당시 적용되었던 평가자 정책 (evaluator policy), 그리고 두 가지 보고 인터페이스가 포함됩니다.
morrow verify는 고정된 순서에 따라 해당 파일들로부터 결론을 재도출합니다. 즉, 다이제스트 (digests), 닫힌 스키마 (closed schemas), 이벤트로부터 재계산된 지표 (metrics), 재계산된 결론을 거쳐, 보고서를 다시 생성한 뒤 바이트 단위로 비교합니다. 기록된 보고서 내용 중 그 결정의 입력값으로 사용되는 것은 아무것도 없습니다.
검토 중인 풀 리퀘스트 (pull request)에 의해 제어되는 카세트가 스스로의 결론을 선택할 수 있는 방법을 찾기 위해 6차례의 적대적 검토 (adversarial review, OpenAI Codex 및 보안 및 품질 검토 포함)를 진행했습니다. 카세트는 자체 판단 임계값 (judging thresholds)을 제공하거나, 스스로의 성공을 단언하거나, 한 번의 실행을 두 번의 반복으로 재사용할 수 있었습니다. 수정 사항은 하나의 규칙으로 수렴되었습니다:
매니페스트 (manifest)는 레이블과 포인터를 담는 것이지, 결과 (findings)를 담는 것이 아니다.
매니페스트는 특정 실행이 어떤 팔 (arm)과 쌍 (pair)에 속하는지, 그리고 그 증거가 어디에 있는지를 명시할 수 있습니다. 실행 성공 여부와 테스트 사이클을 포함하여, 무언가를 결정하는 모든 것은 재도출됩니다.
위에서 언급한 모든 허점(hole)은 이제 공격을 재현하고 이를 차단하는 상태를 단언(assert)하는 테스트가 되었습니다. 총 212개의 테스트가 있으며, CI는 모든 푸시(push)마다 게시된 세 개의 카세트(cassette)를 각각의 종료 코드(exit code)와 함께 검증합니다. 두 개는 실패하도록 설계되었으므로 결과는 0 / 2 / 2로 단언됩니다.

제가 주장하지 않는 것들
- 통계적 유의성(statistical significance) 없음. 비교된 세 쌍의 경우, 단측 부호 검정(one-sided sign test)의 하한선은 1/8 = 0.125입니다. 보고서는 표본 크기에 따라 적절한 형식으로 이를 명시합니다.
- 신뢰할 수 없는 저장소에 대한 안전한 실행 보장 안 함. 컨테이너에 모델 API가 필요하므로
--network none옵션을 사용할 수 없습니다. - 변조 방지(tamper resistance) 기능 없음. 다이제스트(digest)는 우발적인 손상을 감지하며, 재계산(recomputation)은 증거로부터 더 이상 도출되지 않는 보고서를 감지합니다. 둘 다 서명(signature)은 아닙니다.
직접 시도해보기
git clone https://github.com/ichiburn/morrow && cd morrow
uv sync --all-groups --locked
...
AI 공개 사항 (AI disclosure)
Anthropic Claude Code가 구현, SigNoz 및 OpenTelemetry 통합, 테스트 및 문서 작성을 수행했습니다. OpenAI Codex는 적대적 설계 검토(adversarial design review, 구현 시작 전 4회)와 코드 리뷰(구현 후 6회)를 수행했습니다. ChatGPT는 제품 아키텍처 및 계획 수립에 사용되었습니다. 모든 생성된 변경 사항은 제가 직접 검토했습니다.
검토자가 이 글을 통해 얻어갔으면 하는 점은 단순한 변동성 분리(churn separation)가 아닙니다. 바로 이 측정 도구가 제가 원치 않는 결과를 보고할 수 있도록 구축되었으며, 실제로 그렇게 했다는 사실입니다.
프로젝트 저장소: github.com/ichiburn/morrow
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기