Luthier: AI 코딩 에이전트가 관리하는 레포지토리의 검증기
요약
AI 코딩 에이전트가 관리하는 레포지토리의 검증 필요성을 강조하며, 기존 방식으로는 오류를 잡아내기 어렵다고 지적합니다. '감사관(auditor)' 역할을 하는 아키텍처는 모든 발견 사항을 로그에 기록하고 출처를 명확히 하여 신뢰도를 높이는 방법을 제시합니다.
핵심 포인트
- AI 에이전트의 행동은 예측 불가능하며, CI 통과만으로는 충분하지 않다.
- 모든 정보는 '감사관'가 읽고 추적할 수 있는 로그에 기록되어야 한다.
- 단순 텍스트 추출 대신 `lineno|text` 형태로 정보를 제공하여 정확도를 높여야 한다.
- 경로 유효성 검증(path_exists)을 통해 에이전트의 주장을 반증 가능하게 만들어야 한다.
당신 에이전트의 CLAUDE.md는 거짓말을 하고 있고, 아무것도 이를 확인하지 않는다
제가 만나는 모든 팀은 이제 같은 아티팩트를 가지고 있습니다: CLAUDE.md, AGENTS.md, .cursorrules, 스킬 폴더, 그리고 네 개의 서버가 포함된 .mcp.json입니다. 이 파일들은 낙관론의 분출로 작성되었고, 그러다가 코드가 움직이기 시작합니다.
3개월 후:
- 에이전트가 수정해야 한다고 알려준 파일 이름이 변경되었습니다;
- 에이전트가 실행하도록 지시한 명령은 더 이상 누군가 선언하는 스크립트가 아닙니다;
- MCP 서버 중 하나는 노트북을 재구축한 이후 아무도 설치하지 않은 바이너리를 가리킵니다;
- 어떤 명령어 파일에서도 언급된 적이 없는 디스크의 스킬 디렉토리가 존재하여, 에이전트는 이를 사용해도 된다고 알 수 없습니다;
- 그리고 모두가 신봉하는 규칙—커밋된 코드에
console.log를 넣지 않는다—은 진입점의 4번째 줄에서 위반됩니다.
이 중 어느 것도 오류를 발생시키지 않습니다. 당신의 테스트는 통과합니다. CI는 녹색입니다. 유일한 증상은 에이전트가 4분짜리 작업을 하는 데 40분이 걸리고, 존재하지 않는 파일을 수정하며, 자신 있게
정직함을 유지하는 아키텍처
paste repo path (또는 zip 압축 파일 드롭)
|
+-- discover harness ....... read_harness / inspect_repo -> audit.log
...
모든 것을 형성하는 설계 제약 조건: 감사관(auditor)은 자신이 읽은 것만을 단언할 수 있다. 레포지토리에 대해 알 수 있는 방법은 정확히 두 가지이며, 둘 다 기록된다.
@tool
def read_harness(paths: list[str]) -> dict:
"""하네스/명령어 파일 읽기 (CLAUDE.md, AGENTS.md, .cursorrules, skills,...)
...
bundle.call이 흥미로운 부분이다. 이 함수는 도구를 실행하고 지속 시간과 페이로드의 바이트 크기를 측정하며, audit.log에 JSONL 레코드를 추가하고 결과를 인메모리 저널에 보관한다. 모든 발견 사항에는 이를 정당화하는 로그의 행(inspect_repo#4, grep#15)을 가리키는 journalRef가 포함된다. 점수는 여기에서 의견이 아니라 쿼리가 가능한 흔적이다.
내용은 원시 텍스트(raw text)가 아닌 lineno|text 행으로 반환된다. 이것은 의도적인 프롬프트 엔지니어링 선택이다: 모델들은 줄 수를 잘못 세는 경향이 있고, 만약 그들에게 줄을 세도록 하면, 그들은 12줄짜리 파일의 27번째 줄을 자신 있게 인용할 것이다. 번호 매기기와 인용률을 제공하면 성공률은 약 100%에 도달한다.
다섯 가지 검사 및 각각이 실제로 증명하는 것들
1. path_exists
하네스 텍스트에서 경로 모양의 토큰을 추출하여 실제 트리와 glob(패턴 매칭)합니다. 흥미로운 작업은 _필터링_에 있는데, 산문에는 경로처럼 보이는 것들로 가득하기 때문이다:
def looks_like_path(token: str, root_entries: set[str]) -> bool:
raw = (token or "").strip().strip("`")
if not raw or raw.startswith(("~/", "$", "%", "(", "@", "#")):
...
토큰이 알려진 확장자를 가지거나, 첫 번째 세그먼트가 레포지토리 루트에 실제로 존재하는 디렉터리이거나, 관습적인 이름(src, docs, scripts, ...)인 경우 수락합니다. 플레이스홀더(<id>, {name}, path/to/...)는 거부됩니다. path/to/secret.json이라고 주장하는 하네스는 반증 가능한 주장을 하고 있지 않기 때문입니다.
Globs도 마찬가지입니다: src/**/*.tsx가 아무것도 일치시키지 못하면 드리프트(drift)이며, 파일이 누락된 경우와 정확히 같습니다.
2. command_runs — 선언되었으나 실행되지 않음
사람들이 반응하는 검사 항목입니다. 왜냐하면 단순한 버전은 보안 사고이기 때문입니다: 감사하고 있는 명령을 실행해서는 안 됩니다.
if base in {"npm", "pnpm", "yarn", "bun"}:
...
if sub in {"run", "run-script", "rs"}:
...
모든 것은 저장소가 이미 가지고 있는 선언(declarations)에 대한 조회입니다: package.json의 scripts(워크스페이스 전반), 라인 정규식으로 파싱된 Makefile 타겟, just 레시피, tomllib을 통한 pyproject.toml의 [project.scripts] 엔트리 포인트, 그리고 베어 바이너리를 위한 requirements*.txt/종속성 테이블입니다. 바이너리가 "해결(resolves)"되려면 PATH(node_modules/.bin, .venv/bin, ~/.local/bin, Homebrew 디렉터리로 확장됨)에 있거나 매니페스트에 선언되어 있어야 합니다. 왜냐하면 단순히 아직 설치하지 않은 문서화된 도구는 하네스 드리프트가 아니기 때문입니다.
3. file_referenced — 기술(skills)은 양방향으로 작용합니다
기술 폴더 내에 두 가지 독립적인 실패 사례가 숨어 있습니다:
- 선언되었으나 사용되지 않음: 하네스가 기술 이름을 명시하지만, 저장소의 어느 곳에서든 그 언급이 오직 그것을 선언하는 하네스 라인뿐입니다. 기술 자체의 디렉터리 외부에서는 아무것도 참조하지 않습니다. 이것은 자신감 있는 설명과 함께 존재하는 죽은 기능(dead capability)입니다.
- 디스크에는 있으나 목록에 없음:
.hermes/skills/orphan-skill/폴더 안에 실제SKILL.md파일이 있지만, 그것을 언급하는 지침 파일(instruction file)이 없습니다. 에이전트는 자신이 무엇인지 알려주지 않은 것은 사용할 수 없습니다.
첫 번째 사례가 제외 술어(exclusion predicate)가 중요한 이유입니다. 기술 자체의 파일과 선언 라인을 모두 제외해야 합니다. 그렇지 않으면 모든 기술에 대해 검사가 공허하게 통과합니다.
4. server_reachable — 스키마와 바이너리가 있지만, 생성(spawning)은 없음
.mcp.json, claude_desktop_config.json, .cursor/mcp.json, .vscode/mcp.json 및 opencode.json은 모두 약간 다른 키(mcpServers, servers, mcp) 아래에 서버 맵을 보유하고 있습니다. 모양(shape)을 검증합니다 — command는 비어있지 않은 문자열, args는 문자열 리스트, env는 객체여야 합니다 — 그런 다음 바이너리를 해결합니다. 절대 서버를 시작하지 마십시오.
사라진 서버에 대한 diff가 코드베이스에서 가장 만족스러운 부분입니다. JSON을 다시 구문 분석(reparsing)하고 다시 출력(re-dumping)하면 전체 파일이 재형식화되므로, 대신 작은 중괄호 매칭 스캐너가 멤버의 정확한 라인 범위(line span)를 찾아 해당 블록만 삭제합니다. 그런 다음 그 결과를 _다시 구문 분석_하여 JSON에 오류가 생길 경우 diff를 내보내는 것을 거부합니다:
patched = "\n".join(remaining) + "\n"
try:
json.loads(patched)
...
만약 사라진 바이너리가 PATH에 근접한 이웃(node21 → node)을 가지고 있다면, 패치는 서버를 삭제하는 대신 명령어를 다시 작성합니다.
5. semantic — 그리고 실제 답변으로서의 unknown
관습(Conventions)은 어려운 부분이므로 두 개의 엔진이 사용됩니다. 기계적인 부분집합은 모델 없이 검사할 수 있습니다: 절대 X를 사용하지 마십시오, B 대신 A를 사용하십시오, 모든 단위는 file.ext를 포함해야 합니다. 소스 파일에서 토큰을 검색하고 주석과 문서는 건너뜁니다 — 안티 패턴(anti-pattern)을 인용하여 작성하지 말라고 알려주는 README도 위반은 아닙니다.
나머지 모든 것은 엄격한 계약(strict contract)을 가진 모델로 전달됩니다:
{"verdicts":[{"id":"r7","verdict":"holds|violated|unknown","evidence":"<path:line + quote>"}]}
…그리고 답변 자체도 감사(audited)합니다. 만약 violated 판정이 리포에 존재하지 않는 경로를 인용한다면, 이는 unknown으로 하향 조정되고 검증되지 않은 인용은 발견 사항에 유지됩니다. unknown은 1점을 소모하며 항상 표시됩니다. 불확실성을 숨기는 도구는 결국 위반을 숨기는 도구가 될 것입니다.
설정된 키가 없습니까? 해당 섹션은 침묵하는 공백이 아닌, 발견 사항 테이블의 한 행으로 skipped: no_api_key를 보고합니다. 검사 1~4는 여전히 실행되며, 이곳에 대부분 실제 드리프트(drift)가 존재하기 때문입니다.
방어할 수 있는 점수 산정 (Scoring you can defend)
BASE_SCORE = 100
PENALTY_DETERMINISTIC_FAILURE = 8
PENALTY_SEMANTIC_VIOLATION = 5
...
카테고리별 상한선이 존재하는 이유는, 디렉터리 하나만 이름이 변경된 500개 경로의 레포지토리가 점수 0점을 받아서는 안 되기 때문입니다. 네 개의 결정적 실패(deterministic failures)가 한 카테고리를 포화시키면 UI에 capped라고 표시됩니다.
상수는 GET /api/scoring을 통해 제공되며, 확장 가능한 "이 점수가 어떻게 계산되었는지" 패널에 렌더링되므로 아무도 그 숫자를 믿을 필요가 없습니다.
실제로 저에게 무언가를 가르쳐준 두 가지 버그
lstrip("./")는 "선행 ./를 제거한다"는 의미가 아닙니다. 이 함수는 {'.', '/'} 집합에 있는 모든 선행 문자를 제거합니다. 따라서 .mcp.json 패턴은 조용히 mcp.json이 되었고, 점(dot)으로 시작하는 모든 하네스 파일(.mcp.json, .cursorrules, .hermes/skills/**, .claude/...)이 발견되지 않았습니다. 증상은 MCP 설정이 깨진 레포지토리를 깔끔하게 감사한 결과였습니다. 해결책은 루프입니다:
while norm.startswith("./"):
norm = norm[2:]
MCP 누락보다 더 심각했던 것은 다음과 같습니다: inspect_repo(".eslintrc.json")이 "존재하지 않음"을 반환하여, 감사자가 확신을 가지고 잘못된 발견 사항을 보고할 수 있게 만들었습니다. 바이트 제한이나 정규식 버그는 눈에 보이지만, 경로 정규화(path-normalisation) 버그는 증거를 만들어냅니다. 이것은 이 프로젝트가 감당할 수 없는 버그 유형입니다.
diff도 일종의 주장입니다. 제가 처음 사용했던 오래된 경로 패치(stale-path patch)는 하네스 라인 전체를 삭제했습니다. "CLI 출력이 변경될 때 src/old.ts 편집"에 대해서는 맞습니다. 하지만 "먼저 README.md 읽기, 그 다음 docs/architecture.md"와 같은 경우, 오래된 지시사항과 함께 유효한 지시사항까지 삭제합니다. 따라서: 만약 해당 라인이 다른 검증 가능한 주장들을 담고 있다면, 오래된 토큰만 제거해야 합니다. 이는 작은 체인의 정규식으로 잔여물(... first, then . → ... first.)을 정리하고, 검증할 수 있는 것이 아무것도 남아있지 않을 때 라인 전체를 삭제하는 것으로 대체되어야 합니다.
그리고 리네임(rename)은 파일 이름이 변경되지 않은 경우(move) 또는 두 경로가 ≥0.86 유사할 때만 제안됩니다. 그보다 덜 엄격한 것은 diff에 포함되는 것이 아니라 suggestion 필드에 들어갑니다. src/cli.ts → src/util.ts는 그럴듯해 보이지만 틀린 패치이며, 잘못된 패치는 아무것도 없는 것보다 더 나쁩니다.
정밀도(Precision): 감사에 대한 감사
Luthier가 실제 레포지토리(repo)를 대상으로 처음 실행했을 때 점수는 42점이었고, 해당 레포의 README에서 '드리프트(drift)'의 예시로 인용하는 경로인 src/old.ts를 플래그 지정했습니다. 기술적으로는 진정한 관찰이지만, 실질적으로는 노이즈이며, 점수에서의 노이즈가 사람들이 한 번의 수정에 신뢰를 잃게 만드는 방식입니다.
두 가지 규칙을 통해 대부분의 문제가 해결되었습니다:
- 문서에서 앱이 _작성(writes)_한다고 언급하는 파일은 깨진 참조(broken reference)가 아닙니다.
"run/settings.local.json에 저장됨 (mode 0600)"는 런타임 동작(run-time behaviour)을 설명합니다. 이는 경고(Advisory)이며, 점수로는 0점입니다. - 이 레포지토리가 가지고 있지 않은 디렉토리 내부의 경로를 명시하는 서술형 문서(Prose docs)는 아마도 예시일 것입니다.
README.md가src/자체가 없는 레포에서src/foo.ts를 인용하는 경우 → 경고입니다. 하지만CLAUDE.md가 이를 인용하는 경우 → 여전히 심각한 실패(hard failure)입니다. 왜냐하면 지침 파일(instruction file)은 에이전트가 로드하고 따르는 것이기 때문입니다.
— 지침 파일은 점수에 반영되고, 문서는 노출되는 — 이 구분이 사람들이 행동으로 옮기는 점수와 아무도 읽지 않는 린트 리포트 사이의 모든 차이를 만듭니다. 동일한 레포가 실제로 발견된 문제들이 여전히 나열되면서 42점에서 76점으로 향상되었습니다.
검증(Verification)
npm run verify # python -m agent.verify
두 개의 임시 git 레포지토리가 temp 디렉토리에 작성됩니다. 하나는 다섯 가지 종류의 시드된 드리프트(seeded drift)를 가지고 있으며, 각각은 정확한 file:line에서 플래그 지정되어야 합니다. 다른 하나는 0개의 결정론적 실패(deterministic failures)와 함께 ≥95점을 받아야 합니다. 이는 모든 것을 플래그 지정하는 검사기(checker)에 대한 방어책인데, 작성하기는 쉽지만 가치가 없습니다. 그런 다음 생성된 모든 diff는 git apply --check를 통해 실행되어야 하고, 모든 인용은 실제 파일의 실제 라인이어야 하며, audit.log는 매 행마다 tool, args, duration, bytes를 포함하는 유효한 JSONL 형식이어야 합니다.
drifted fixture score 47 findings 9 rules 9 tool calls 15
clean fixture score 100 findings 0 rules 4
...
The Strands 에이전트 루프는 로컬 모의(mock) OpenAI 호환 엔드포인트를 대상으로 자체 검사(scripts/agent_loop_check.py)를 수행합니다. 이 검사는 에이전트의 도구 호출이 실제로 저널링된 리더를 거쳤는지, 그리고 API 키가 응답 본문(response body)에 절대 나타나지 않는지를 확인합니다. 이는 모델의 판단력이 아니라 통합 자체를 증명하는 것이며, 이 도구가 존재하는 이유 자체가 '두 번째 종류'에 대한 주장을 잡아내기 위함이므로 그 구분을 솔직하게 밝힙니다.
목적
린트(Lint)가 아닙니다. 린트는 코드가 특정 스타일을 만족하는지 확인합니다. Luthier는 _명령어(instructions)가 현실을 만족하는지_를 확인하며, 이는 다른 축이며 아직 아무도 자동화하지 못한 영역입니다.
불편한 프레임 설정: 하네스(harness)는 프롬프트이고, 프롬프트는 의존성(dependency)인데, 우리는 프롬프트에 대한 의존성 검사기가 없습니다. 대신 저희는 이것을 가지고 있습니다. 즉, 당신이 에이전트에게 전달하는 내용 중 얼마나 많은 부분이 여전히 사실인지를 하나의 숫자와 인용문표가 담긴 표로 보여주는 점수판입니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기