Shelley와 관리형 LLM 게이트웨이 exe.dev를 위한 배려가 녹아 있다. 즉 meat는 "장난감 데모"가 아니라 실제 프로덕션 에이전트 파이프라인에서 쓰려고 뽑아낸 부품이다.
boldsoftware/meat · 라이선스 Apache-2.0 (© 2026 Bold Software) · 언어 순수 Go (go 1.24, 서드파티 의존성 0개) · 규모 약 1.4만 줄(테스트가 소스보다 많음) · 설치 go install meat.dev/cmd/meat@latest · 기본 모델 OpenAI gpt-5.6-sol / Anthropic claude-opus-4-8 · TrendShift Daily #1)
meat는 "unified diff를 입력받아, LLM 에이전트에게 읽을 가치 없는 부분을 골라내게 하고, 그 편집 계획을 원본 diff에 기계적으로 적용해 '읽기용 diff'를 출력하는 명령줄(CLI) 도구"다. 사용법은 git show만큼 단순하다 — 저장소 안에서 그냥 meat라고 치면 최신 커밋을, meat HEAD~3이나 meat main...HEAD처럼 git 인자를 주면 그 범위를, git diff | meat처럼 파이프로 넘기면 그 diff를 압축한다.
...로 바꿔 주는 편집자"두꺼운 계약서를 검토한다고 하자. 보통의 요약 도구는 "제가 요약해 드릴게요"라며 자기 말로 새로 써 준다 — 그러면 원문에 없던 말이 섞일 위험(환각)이 생긴다. meat의 편집자는 다르다. 원본 계약서에는 한 글자도 새로 쓰지 않는다. 대신 "1~4쪽은 표준 약관이라 통째로 접기", "7쪽 둘째 줄은 회사명만 남기고 나머지는 …로 가리기" 같은 지시서(편집 계획)만 만든다.
그 지시서를 실제 원본에 적용하는 건 편집자가 아니라 기계다. 그래서 결과물에 보이는 모든 글자는 반드시 원본에 실제로 있던 글자다. 편집자(LLM)가 아무리 헛소리를 하고 싶어도, 할 수 있는 일은 "무엇을 숨길지 고르는 것"뿐이라 거짓말이 원천 봉쇄된다. 남는 건 계약의 핵심 조항, 즉 고기(meat)뿐이다.
+(추가)·-(삭제)로 보여 주는 형식. unified diff는 그중 가장 흔한 표준 포맷(git diff가 뱉는 그것). reading diff(읽기용 diff)는 meat가 만든 개념 — "적용 가능한 패치가 아니라, 사람이 읽고 이해하기 위한 압축된 diff". abridge는 "요약·축약하다"라는 뜻으로, meat의 핵심 동사다. 헝크(hunk)는 diff 안에서 @@ ... @@로 구분되는 "변경 덩어리" 하나를 가리킨다.기술적으로 정확히 말하면, meat는 순수 Go로 짠, 서드파티 의존성이 0개인 단일 바이너리다(go.mod에 require 블록이 아예 없다). 표준 라이브러리만으로 OpenAI Responses API와 Anthropic Messages API를 직접 호출하고, git CLI로 diff를 읽어 오며, 결과를 ~/.meat에 내용 주소 기반(content-addressed)으로 캐시한다. "기능 = 하나의 잘 정의된 편집 프로토콜 + 그 프로토콜을 강제하는 컴파일러"라는 등식이 이 저장소 전체를 관통한다.
첫째, 문제 정의가 반(反)직관적이고 시의적절하다. 대부분의 "AI 코드 리뷰" 도구는 "AI가 당신 대신 버그를 찾아 드립니다"라고 말한다. meat는 정반대 전제에서 출발한다 — "코드는 컴파일되고 테스트도 통과한다. 리뷰어는 nil 패닉을 사냥하는 사람이 아니다." 에이전트가 코드를 쓰는 시대의 병목은 "버그 찾기"가 아니라 "거대한 diff에서 개념적으로 중요한 변화를 사람이 빠르게 파악하기"라는 것이다. meat는 리뷰를 대신하지 않는다. 리뷰할 것을 줄여 준다.
보통의 LLM 요약기는 모델이 자유 텍스트로 결과를 생성한다 — 그래서 원본에 없던 코드·주석·식별자가 슬쩍 섞일 수 있다(환각). meat는 이 문을 아예 닫는다. 모델이 제출할 수 있는 건 오직 원본 줄 번호를 가리키는 세 가지 연산뿐이다: remove(줄 범위 삭제)·replace(한 줄의 일부만 …로 가리기)·fold(연속된 여러 줄을 ... 한 줄로 접기). 결과 diff는 meat가 변경 불가능한 원본에 이 연산을 적용해 기계적으로 렌더한다. "출력을 자유 생성이 아니라 원본에 대한 연산으로 제약한다"는, 안전한 LLM 편집의 정석 패턴을 극단까지 밀어붙인 표본이다.
둘째, 제작진의 무게가 다르다. meat는 Bold Software가 만들었다 — 에이전트 코딩 도구 Sketch를 만드는 회사다. 코드 주석에는 자사 에이전트 Shelley가 meat를 "임베딩(embed)"해 자기 LLM 클라이언트로 갈아끼울 수 있다고 명시돼 있고, exe.dev VM에 붙은 관리형 LLM 게이트웨이를 자동 감지해 API 키 없이도 돌아가는 경로까지 마련돼 있다. 즉 이건 데모가 아니라 실제 프로덕션 에이전트 파이프라인의 한 부품으로 설계됐다.
셋째, "실전 벤치마크를 저장소에 같이 넣었다". analysis/ 폴더에는 Django·Flask·pytest의 실제 유명 커밋에 meat를 돌린 결과가 JSON·diff·리포트로 통째로 들어 있다. 심지어 리포트는 자기 도구의 약점("Django에서 좋은 테스트 자극을 숨기고 기계적 호출부 변경을 남겼다")까지 솔직히 적는다. 마케팅이 아니라 엔지니어링 문서다.
| 대상 커밋 | 원본 변경 줄 | meat가 보여 준 줄 | 바이트 절감 |
|---|---|---|---|
| Django | 178줄 | 112줄 (62.9%) | 34.6% ↓ |
| Flask | 124줄 | 96줄 (77.4%) | 23.4% ↓ |
| pytest | 109줄 | 83줄 (76.1%) | 29.3% ↓ |
▲ 저장소 analysis/에 실린 자체 벤치마크(2026-08-02, 기본 모델 claude-opus-4-8, -no-cache). "얼마나 지웠나"가 아니라 "얼마나 안 읽어도 되게 만들었나"가 지표다.
넷째, 결과에 대한 정직함이 코드에 박혀 있다. meat는 "몇 줄을 남겼는지"를 모델의 자기 보고를 믿지 않고, 읽기용 diff를 원본에 다시 정렬해 직접 세어 화면 맨 아래에 kept 12/240 changed lines in 3/7 files처럼 찍는다(elision.go). "당신은 지금 240줄 중 12줄만 보고 있다"를 리뷰어가 항상 알 수 있게 — 압축의 대가로 무엇을 안 보고 있는지를 숨기지 않는다.
meat의 첫 번째 놀라움은 "의존성이 없다"는 점이다. go.mod은 딱 두 줄(module meat.dev, go 1.24.13)이고 require 블록이 없다. HTTP 클라이언트·JSON 파서·API SDK를 전부 Go 표준 라이브러리로 직접 짰다. LLM 회사가 만든 도구인데 공식 SDK조차 안 쓴다 — "단일 바이너리로 go install 한 방에 끝나고, 공급망 위험(supply chain)이 0"이라는 가치를 의존성 편의보다 위에 둔 선택이다.
| 영역 | 기술 / 근거 | 역할 |
|---|---|---|
| 언어·런타임 | Go 1.24 (표준 라이브러리 only) | 단일 바이너리, 크로스 플랫폼, 의존성 0 |
| LLM — OpenAI | Responses API (스트리밍, 직접 HTTP) | 기본 공급자. gpt-5.6-sol, reasoning effort medium |
| LLM — Anthropic | Messages API (직접 HTTP) | claude-로 시작하는 모델 ID면 자동 선택. 기본 claude-opus-4-8 |
| 관리형 게이트웨이 | exe.dev reflection endpoint | exe.dev VM이면 llm 통합을 자동 감지 → API 키 불필요 |
| 입력 수집 | git CLI (show/diff) | 커밋·범위·스테이징·워킹트리 diff를 읽어 옴 |
| 주변 소스 탐색 | git grep, 파일 읽기(루트 제한) | 에이전트가 "이 줄이 중요한가"를 판단할 단서 수집 |
| 출력 렌더 | git pager + color.diff 설정 재사용 | 터미널이면 git show처럼 색·페이저, 파이프면 평문 |
| 캐시 | ~/.meat, JSON 파일, SHA-256 키 | 같은 (diff+모델+rubric)이면 즉시 반환(네트워크·키 불필요) |
| 출력 스키마 검증 | 표준 encoding/json (strict decode) | 모델의 편집 계획을 엄격 파싱(미지 필드 거부) |
meat의 코어(meat 패키지)는 특정 LLM 회사를 모른다. 오직 Model이라는 메서드 하나짜리 인터페이스만 안다: Generate(ctx, system, messages, tools). OpenAI Responses(스트리밍·암호화된 reasoning 항목)와 Anthropic Messages(블록 기반)라는 서로 완전히 다른 API 두 개를, Block이라는 공통 자료형 하나로 정규화해 인터페이스 뒤로 숨겼다. 그래서 임베더(Sketch의 Shelley 등)는 자기 LLM 클라이언트를 Model로 감싸기만 하면 meat의 엔진을 그대로 재사용한다.
ProviderData에 그대로 담아 두었다가 재생한다. Messages API는 Anthropic의 대화 API. reasoning 토큰은 모델이 답 전에 "속으로 생각하는" 데 쓰는 토큰(비용에 포함되지만 화면엔 안 보임). 인터페이스(interface)는 Go에서 "이런 메서드를 가진 건 뭐든 받아들인다"는 계약 — meat는 Generate 하나만 요구한다.meat의 아키텍처는 세 개의 축 위에 서 있다: ① 캐시 우선(cache-first) 오케스트레이션, ② 편집 계획 간접화(edit-plan indirection)를 강제하는 에이전트 루프, ③ 계획을 원본에 적용하는 컴파일러. 전체 그림부터 보자.
모델은 완성된 diff를 절대 만들지 않는다. 대신 submit 툴로 세 배열을 제출한다. 좌표는 전부 원본 diff의 1-based 물리 줄 번호이고, 이 번호는 절대 밀리지 않는다(줄을 지워도 나머지 좌표가 바뀌지 않음 — 항상 원본 기준).
| 연산 | 하는 일 | 제약(컴파일러가 강제) |
|---|---|---|
remove | 줄 범위를 통째로 뺀다 | 순수 노이즈에 사용 |
replace | 한 줄의 일부만 …로 가림 | new는 old의 "투영"이어야 함 — 글자는 지우고 .../…만 넣을 수 있음. 식별자·주석·구두점 몰래 추가 금지 |
fold | 연속된 2줄 이상을 ... 한 줄로 접음 | 같은 헝크·같은 부호(+/-/공백). 들여쓰기는 meat가 계산. 모델은 좌표만, 텍스트는 절대 못 씀 |
모델의 "쓰기 권한"을 원천 차단하고 "지우기·가리기 권한"만 준 것. 결과 diff에 등장하는 모든 문자는 반드시 원본 diff에 실제로 있던 문자다(fold가 만드는 고정된 ... 한 종류만 예외인데, 이건 기계가 만들고 들여쓰기까지 원본에서 계산한다). 모델이 replace로 "이 부분만 …로 가려라"라고 할 때도, meat는 new가 old에서 글자를 빼고 …를 넣은 것 이상이 아닌지 정규식으로 검증한다. 통과 못 하면 계획을 거부한다. LLM이 거짓말을 하고 싶어도 프로토콜이 허락하지 않는다.
# 원본 diff (번호매김) — 기계적인 필드 복사 6줄
101|+ // Extra data used for cache management but not routing.
102|+ resp.SSHKeyID = rd.sshKeyID
103|+ resp.UserID = rd.userID
104|+ resp.BoxID = int64(rd.boxID)
105|+ resp.BoxName = rd.boxName
106|+ resp.ExpiresAt = timestamppb.New(rd.expiresAt)
# 모델이 제출하는 계획
remove : 103~106 삭제
replace: 102번 줄에서 ".sshKeyID" → "..."
# meat가 렌더한 읽기용 결과 (원본 글자만 사용)
+ resp.SSHKeyID = rd...
리뷰어는 "여기서 응답 구조체에 라우팅과 무관한 캐시용 데이터를 복사한다"는 의미만 한 줄로 읽는다. 나머지 다섯 줄의 기계적 반복은 사라졌지만, 지어낸 요약 문장이 아니라 실제 코드 조각으로 압축됐다.
abridgeOne은 전형적인 툴 사용 에이전트 루프다. 시스템 프롬프트(=rubric)와 번호매긴 diff를 넣고, 모델이 submit할 때까지 최대 24턴·4분 예산 안에서 돈다. 결정적인 건 모델에게 읽기 전용 도구를 쥐여 줬다는 점이다 — read_file(주변 파일 읽기)과 grep(git grep). "이 줄이 정말 중요한가? 이 파일은 자동 생성된 건가?"를 diff 텍스트만으로 판단이 안 설 때 실제 소스를 들춰 보라는 것. 단, 이 도구들은 resolveInRoot로 저장소 루트 밖으로 못 나가게 막혀 있다(../·절대경로 traversal 방어).
rubric은 "대부분의 줄은 diff만 보고 판단하라, 단서가 판단을 바꿀 때만 도구를 써라"라고 못 박는다. 또 모델이 처음 제출한 계획이 여전히 너무 장황하면(retention pressure가 높으면), meat는 그 계획을 폴백(fallback)으로 보관한 뒤 "한 번 더 다듬어 봐"라고 딱 한 번 넛지한다. 두 번째가 실패하거나 시간이 다 되면 보관해 둔 첫 계획을 반환한다 — 무한 루프도, 빈손 반환도 없다.
모델이 신경 쓰지 않아도 되도록, meat가 기계적으로 알아서 처리하는 것들이 있다.
@@ 헤더 재계산)이고, 독립적으로 압축된 뒤 하나로 병합된다.meat는 결과를 ~/.meat에 캐시한다. 키는 SHA-256(rubricHash + model + diff). 여기서 rubricHash가 정교하다 — 단순히 시스템 프롬프트만 해싱하는 게 아니라, 모델이 볼 수 있는 모든 문자열(시스템 프롬프트, 유저 프롬프트의 모든 분기, 툴 설명·스키마, 넛지 메시지, 계획 피드백)을 실제 빌더 함수로 렌더한 결과 전체를 해싱한다(promptSurface()).
구조는 Go 관례대로 딱 둘로 갈린다: cmd/meat/(사용자와 맞닿는 CLI 껍데기)와 meat/(재사용 가능한 엔진 라이브러리). "명령어는 얇게, 로직은 라이브러리에"라는 Go의 정석이다.
주목할 점: 테스트 파일이 소스 파일보다 크다. 예컨대 chunk.go(1,086줄)에 chunk_test.go(1,559줄)가 붙어 있고, editplan.go(770줄)에 editplan_test.go(923줄)가 붙는다. python_golden_test.go·rubric_e2e_test.go 같은 골든 테스트(golden test)가 "이 diff를 넣으면 이 읽기용 diff가 나와야 한다"를 통째로 고정해 둔다. 컴파일러형 도구답게, 동작을 예제로 못 박는 문화가 코드에 배어 있다.
meat의 가장 이식성 높은 교훈. "모델이 결과물을 자유 생성하게 두지 말고, 원본에 대한 연산으로 출력을 제약하라." 이 패턴은 코드 리뷰뿐 아니라 문서 편집·데이터 마스킹·법률 문서 redaction·리팩터링 도구 어디에나 적용된다. 핵심 실천: ① 모델의 출력 공간을 좁은 스키마(JSON)로 강제, ② 그 계획을 불변의 원본에 기계적으로 적용, ③ 적용 전에 계획을 컴파일러가 검증(투영·균형·대칭)해 거부 가능하게.
LLM 앱에서 캐시는 필수지만, "프롬프트를 고쳤는데 옛 캐시가 나오는" 버그가 흔하다. meat의 해법: 모델이 볼 수 있는 모든 문자열을 실제 빌더로 렌더해 통째로 해싱하고, 그 해시를 캐시 키에 섞는다. 프롬프트·프로토콜·모델 중 하나라도 바뀌면 자동으로 캐시 미스. "프롬프트 = 코드"라면 "프롬프트 해시 = 버전"이라는 발상. rubric.go의 promptSurface()와 abridgeProtocolVersion 상수를 정독할 가치가 있다.
OpenAI Responses(스트리밍·암호화 reasoning)와 Anthropic Messages(블록형)를 Model.Generate 하나로 정규화한 방식은 어댑터 패턴의 좋은 표본이다. 특히 Block을 인터페이스가 아니라 평평한 구조체로 두고 Type 필드로 분기시킨 선택(타입 스위치 최소화), 그리고 공급자별 불투명 상태를 ProviderData에 격리한 설계를 배울 것.
main.go의 run()은 flag·git·LLM 없이도 단위 테스트되도록, compute·render를 클로저로 주입받는다(의존성 역전). abridgeBudget·singleRunDiffBytes처럼 상수 대신 테스트에서 덮어쓸 수 있는 변수로 둔 패턴, exeDevMarkerPath를 var로 두어 게이트웨이 감지를 스텁하는 기법도 실전 Go 테스트 설계의 교본이다.
diff.go의 analyzeDiff는 모든 물리 줄을 헤더/헝크경계/헝크소스로 분류하고 파일·헝크·언어를 태깅한 불변 레이아웃을 만든다 — 하류 로직은 절대 diff를 재파싱하지 않는다. "한 번 파싱해 불변 자료구조로 굳히고 모두가 그것만 본다"는 원칙, 그리고 -- counter를 --- 메타데이터로 오인하지 않는 헝크 인식 분류 같은 디테일을 배울 것.
| 항목 | 요구/기본값 | 비고 |
|---|---|---|
| Go | 1.24 이상 | go install meat.dev/cmd/meat@latest 한 줄 |
| git | 필수 | diff 수집·git grep·페이저/색 재사용에 사용 |
| API 키 | OPENAI_API_KEY 또는 ANTHROPIC_API_KEY | exe.dev VM이면 게이트웨이 자동 감지로 키 불필요 |
| 네트워크 | 모델 호출 시에만 | 캐시 HIT이면 오프라인·키 없이 즉시 반환 |
| 메모리·디스크 | 매우 가벼움 | 단일 바이너리 + ~/.meat JSON 캐시 |
| 입력 상한 | 단일 실행 ~400KB / 전체 ~4MB | 초과 시 분할(최대 32조각) 또는 "범위를 좁혀라" 안내 |
meat는 커밋 하나를 처리하는 데 시간이 걸린다(reasoning 모델 + 최대 24턴의 툴 사용). README도 대놓고 권한다 — "에이전트를 시켜 meat를 devtools에 넣어 커밋을 미리 처리해 두라". 그래서 캐시가 1급 시민이다: 같은 diff·모델·rubric이면 두 번째부터는 즉시(meat: cached). 토큰 사용량은 실행 후 tokens in=… out=…로 항상 표시돼, 비용을 눈으로 확인하며 쓸 수 있다.
go install meat.dev/cmd/meat@latest 후, 최근 리팩터링 커밋에서 git show HEAD | meat와 원본 git show HEAD를 나란히 비교하라. 목표: meat가 지운 줄들이 정말 "안 읽어도 되는 것"이었는지 스스로 판정 → 맨 아래 kept N/M 수치와 체감이 맞는지 확인. import·에러 메시지·필드 복사가 어떻게 처리됐는지 눈으로 익히기.
-json 출력으로 편집 계획의 "정직함" 검증하기git show <sha> | meat -json으로 smart_diff를 뽑은 뒤, 그 안의 모든 코드 줄이 원본 diff에 실제로 존재하는지 스크립트로 대조하라(fold가 만든 ... 줄만 예외). 목표: "모델이 출력을 쓰지 않는다"는 주장이 참인지 직접 확인 → 제약 생성의 안전성을 체감. 여러 커밋에 반복해 반례가 나오는지 찾아보기.
저장소를 클론해 rubric.go의 systemPrompt 문구를 아주 살짝 고친 뒤 RubricHash()가 달라지는지, 같은 diff의 캐시가 미스로 바뀌는지 확인하라. 목표: "프롬프트 표면 전체를 해싱해 캐시를 무효화한다"는 메커니즘을 손으로 재현 → promptSurface()가 왜 툴 스키마·피드백까지 다 렌더해 넣는지 이해하기.
Model 구현으로 다른 공급자 붙이기Model 인터페이스(Generate 하나)를 구현해 로컬 LLM(Ollama)이나 다른 API를 meat 엔진에 꽂아라. OpenAI/Anthropic의 응답을 []Block으로 정규화하는 부분이 핵심. 목표: provider 추상화의 경계가 어디인지, tool_use/tool_result 블록을 어떻게 왕복시키는지 체득 → 어댑터 패턴 실전 연습.
preview_plan에 일부러 잘못된 계획을 넣어 보라 — replace의 new에 원본에 없는 식별자 추가, move의 한쪽만 접기, Python 삼중따옴표 균형 깨기. 목표: 컴파일러가 각 위반을 어떤 메시지로 거부하는지 수집 → "프롬프트에 규칙을 쓰지 않고 피드백으로만 가르친다"는 frozen-surface 철학이 실제로 어떻게 작동하는지 관찰하기.
| 주차 | 주제 | 무엇을 / 왜 |
|---|---|---|
| 1주차 | unified diff & git plumbing | git show/git diff/범위 문법(A..B, A...B), 헝크·@@ 헤더 읽기. diff.go의 analyzeDiff를 따라 파서 미니 구현. |
| 2주차 | LLM 툴 사용 에이전트 루프 | system/user/tool_use/tool_result의 왕복, 턴 상한·시간 예산·폴백. meat.go의 abridgeOne을 정독하고 최소 루프를 직접 작성. |
| 3주차 | 제약 생성 & 스키마 강제 | JSON Schema로 출력 공간 좁히기, strict decode(미지 필드 거부), 계획→적용 분리. 문서 redaction 도구를 같은 패턴으로 프로토타이핑. |
| 4주차 | 프롬프트 버저닝 & 캐시 | content-addressed 캐시, 프롬프트 표면 해싱, 프로토콜 버전 상수. "프롬프트를 바꾸면 캐시가 깨지는" 안전 장치를 자기 프로젝트에 이식. |
| 5주차 | Provider 추상화 & 어댑터 | 이질적 API를 공통 Block으로 정규화, 불투명 상태 격리(ProviderData), 스트리밍 처리. 로컬/원격 모델 2종을 하나의 인터페이스로. |
| 6주차 | 대용량 입력 분할·병합 | 구조 경계 기반 chunking, 각 조각을 유효 diff로 재구성, 전역 속성(move)의 조각 간 매핑. 스트리밍/맵-리듀스형 LLM 파이프라인 설계. |
remove/replace/fold 배열. 좌표는 전부 원본 1-based 줄 번호. 모델이 만드는 유일한 산출물이며, meat가 이걸 원본에 적용해 결과를 만든다.... 한 줄로 접기(텍스트는 기계가). replace=한 줄의 일부만 …로 가리기(원본 글자의 투영만 허용). remove=줄 범위 통째 삭제.llm 통합이 붙어 있으면 meat가 자동 감지해 API 키 없이 호출. Shelley=meat를 임베딩해 쓰는 에이전트(코드 주석에 등장) — meat가 라이브러리로 재사용되는 증거.analysis/ 벤치마크부터 볼 것go install meat.dev/cmd/meat@latest (Go 1.24+)meat/rubric.go(규칙+해싱) → meat/meat.go(에이전트 루프) → meat/editplan.go(계획 컴파일러) → cmd/meat/main.go(오케스트레이션)git diff 문서), JSON Schema