거절한 설계를 지우지 않는 리포 — DeepSeek Harness 를 클론해서 세어봤다
DeepSeek 이 2026-08-13 에 deepseek-harness 를 공개했다. 보도는 대체로 두 가지에 몰렸다 — 스타 수가 폭발했다는 것, 그리고 “모든 것이 플러그인” 이라는 아키텍처. 둘 다 사실이다. 그런데 클론해서 파일을 세어보니 기사에서 한 번도 못 본 게 하나 나왔다.
이 리포에는 결정 739건의 원장이 들어 있다. 그중 11건은 거절된 제안인데, 지우지 않고 보관돼 있다.
사실관계
2026-08-27 기준 GitHub REST API 응답값과 리포 파일(b150a55, 얕은 클론) 실측이다.
| 항목 | 값 |
|---|---|
| 생성 / 최근 push | 2026-08-13 / 2026-08-21 |
| 스타 / 포크 | 199,396 / 22,740 |
| 라이선스 / 주 언어 | MIT / TypeScript |
| 버전 | 0.1.1-rc.2 (루트 package.json) |
| Node 요구사항 | ^22.19.0 \|\| >=24.0.0 |
워크스페이스 package.json |
256개 |
packages/ 하위 그룹 |
약 50개 (llm session sandbox shell fs mcp skill …) |
버전을 먼저 봐야 한다. 0.1.1-rc.2 이고, README 는 호환성 깨지는 변경이 있을 것이라고 대문자로 못박아 뒀다. 아래 내용은 오늘의 스냅샷이지 안정된 API 가 아니다.
성능·벤치마크 이야기는 이 글에서 다루지 않는다. 중립적인 제3자 헤드투헤드 측정이 아직 없고, 검색 상위에 뜨는 수치들은 대부분 출처를 확인할 수 없는 애그리게이터 글이다.
보도된 층: 플러그인 아키텍처
공식 페이지의 요지는 간명하다. 모델·툴·스킬·세션·샌드박스·스토리지·루프·스케줄링·UI 가 전부 플러그인이고, 그 조립을 Cordis 라는 별도 프레임워크가 맡는다. 256개 워크스페이스 패키지는 그 선언의 결과물이다. packages/llm 안에 llm-deepseek 이 다른 프로바이더와 나란히 한 칸을 차지하고 있는 게 이 설계의 상징이다.
여기에 단서를 하나 붙여야 공정하다. 프로바이더 중립성은 DeepSeek 이 직접 구현한 게 아니다. 멀티 프로바이더 지원은 서드파티 라이브러리(@earendil-works/pi-ai)에 위임돼 있고, 그 카탈로그에 있는 프로바이더를 붙이는 일은 코드 변경이 아니라 설정이라고 패키지 README 가 명시한다. 구조로 뒷받침된 중립성이라는 점에서 마케팅 문구보다 낫지만, 대신 프로바이더 지원 범위가 외부 의존성의 갱신 속도에 묶인다. 이건 트레이드오프지 결함이 아니다.
세어보니 나온 것: .agents/
리포 루트에 .agents/ 가 있다. 안에는 스킬 11개와, Agent Note 라고 부르는 결정 기록이 들어 있다.
파일을 세면 이렇다. 노트 하나는 영문 .md + 중문 .zh.md + .i18n.yaml 사이드카의 세 쪽이 한 벌이라, 파일 수를 그대로 결정 수로 읽으면 안 된다.
| 라이프사이클 | 결정 수 | 뜻 |
|---|---|---|
implemented/ |
559 | 이미 반영된 결정 |
archived/ |
143 | 반영됐으나 앞으로의 판단에 쓸모가 다한 것 |
proposed/ |
26 | 아직 제안 상태 |
rejected/ |
11 | 검토 후 기각된 제안 |
| 합계 | 739 | (파일로는 2,217개) |
경로 자체가 메타데이터다. {lifecycle}/{class}/yyyy-mm-dd-제목.md 꼴이고, 클래스는 architecture feature bug-fix simplification process testing 여섯 개의 닫힌 집합이다. 다른 폴더를 만들면 검사 스크립트가 거부한다. 클래스를 늘리려면 정본 집합과 문서를 같이 고쳐야 한다.
규율이 몇 겹 더 있다.
- 라이프사이클 이동은 형식 재작성을 강제한다.
proposed/→implemented/로 옮기면 “제안” 절을 현재형 “결정” 으로 다시 쓰고 수용 기준·위험을 “결과” 로 접어야 한다. 게이트가 이걸 검사해서, 계획을 그대로 둔 채 폴더만 옮기는 건 통과하지 못한다. - 노트끼리의 상호 참조는 상대 마크다운 링크여야 한다. 산문이나 번호로 가리키는 걸 금지한다 — 기계로 검증 가능하고 파일이 옮겨져도 살아남게 하려는 것이다.
- 중앙
INDEX.md는 금지돼 있다. 그리고 그 금지의 근거를 담은 노트가 따로 있어서, 규칙이 어디서 왔는지 문서 안에서 추적된다. - 아카이브는 영구 동결이다. 옮길 때만 정해진 메타데이터 한 줄이 들어가고, 그 뒤로는 편집·번역·재포맷·삭제가 전부 금지된다. 문서 검사도 아카이브를 건너뛰고, 현재 동작의 근거로 인용하지 못한다.
그리고 이게 장식이 아니라는 증거. README.md 135개가 이 노트들을 308번 인용한다. 패키지 문서가 “왜 이렇게 설계했나” 를 자기 안에서 설명하지 않고, 그 결정을 소유한 노트를 가리킨다. 근거의 소유권이 한 곳에 있다는 뜻이다.
rejected/ 11건이 진짜 흥미로운 부분
코드는 무엇을 했는가를 남긴다. 커밋 로그도 결국 한 일의 목록이다. 어느 쪽도 무엇을 하지 않기로 했는가는 남기지 않는다. 그런데 팀이 반복해서 잃는 지식은 대개 후자다. 6개월 뒤 새로 온 사람이 “이거 왜 이렇게 안 하고 저렇게 했지” 라고 묻고, 아무도 답을 모르고, 그래서 한 번 기각했던 설계를 다시 시도한다.
rejected/ 는 그 자리를 겨냥한다. 보관 조건도 명시돼 있는데 — 원문 표현으로는 “Keep it only while its rationale prevents a tempting, meaningful mistake” — 근거가 유혹적인 실수를 막아주는 동안만 남기고, 그 효용이 끝나면 세 쪽을 통째로 지우라고 돼 있다.
이 조건이 중요하다. 기각 기록을 무한정 쌓으면 그건 그냥 쓰레기장이 된다. “막아주는 실수가 있는 동안만” 이라는 단서가 원장을 살아 있게 만든다. 실제로 11건뿐인 것도 그래서일 것이다 — 739건 중 1.5%다.
나도 같은 규율을 다른 맥락에서 쓰고 있다. 이 블로그에서 여러 번 쓴 얘기지만, 보호를 제공하지 않는 안전 통제는 남겨두면 안 된다. 남아 있으면 보호받고 있다는 착각을 주기 때문이다. 그럴 땐 통제를 지우고 “미완화 노출” 로 등재하는 편이 정직하다. rejected/ 의 보관 조건은 정확히 같은 사고를 문서 쪽에서 한 것이다. 근거가 아무 실수도 막지 않는 순간, 그 문서는 지식이 아니라 부채가 된다.
fail-closed 가 두 군데 박혀 있다
원장 다음으로 눈에 띈 건 실패 처리 방식이다. 두 곳에서 같은 선택을 한다.
샌드박스. 리눅스는 bwrap 을 먼저 찾고 안 되면 Landlock, macOS 는 Seatbelt, 윈도우는 ACL 제한 토큰 러너를 쓴다. 그리고 지원되지 않는 플랫폼이나 쓸 수 없는 러너를 만나면 SANDBOX_UNAVAILABLE 로 실패한다. 격리 없이 조용히 실행되는 경로가 없다.
크리덴셜. 설정 파일에 API 키 값을 적지 않고 환경변수 이름(apiKeyEnv)만 적는다. 값이 아니라 참조다. 그리고 그 참조가 아무것도 가리키지 않으면 MISSING_CREDENTIAL 로 실패한다. 환경에 우연히 들어 있는 다른 키로 인증을 시도하지 않는다.
둘 다 흔한 선택이 아니다. 대부분의 도구는 이런 자리에서 폴백을 “친절” 로 구현한다 — 샌드박스가 없으면 그냥 실행하고, 키가 없으면 다른 키를 찾아본다. 그 친절이 만드는 게 정확히 신호 없는 실패다. 격리 없이 돌고 있는데 로그는 정상이고, 엉뚱한 계정으로 과금되는데 에러가 안 난다. 에이전트 하네스처럼 남의 코드를 실행하는 물건에서는 시끄럽게 죽는 쪽이 옳다.
조사하면서 밟은 함정 하나
검색 상위에 app-deepseek-harness/deepseek-harness 라는 리포가 뜬다. 이름만 보면 공식 같다. GitHub API 로 조회하면 404 다. 공식은 deepseek-ai/deepseek-harness 하나뿐이다.
이름이 그럴듯한 미러와, 출처를 밝히지 않은 채 커밋 수·팀 구성·내부 코드명 같은 걸 단정적으로 적어둔 애그리게이터 글이 상당수 상위에 노출된다. 이 글은 그런 출처에만 존재하는 사실은 하나도 쓰지 않았다. 확인 방법은 단순하다 — 리포를 클론해서 직접 세면 된다.
가져갈 것
하네스를 직접 만들 일이 없어도 이 리포에서 복제할 수 있는 건 명확하다. 결정 원장이다.
거창한 도구가 필요하지도 않다. 필요한 건 네 가지뿐이다.
- 경로에 라이프사이클과 종류를 인코딩한다 (
implemented/architecture/…). - 종류를 닫힌 집합으로 두고 검사 스크립트로 강제한다. 열어두면 반년 안에 30개가 된다.
- 참조는 기계로 검증 가능한 형태로만 쓴다.
- 기각한 것도 남기되, 막아주는 실수가 있는 동안만 남긴다.
4번이 핵심이고, 동시에 대부분의 팀이 안 하는 것이다. ADR 을 쓰는 조직은 많은데 기각된 ADR 을 관리하는 조직은 드물다. 그런데 실제로 반복되는 사고는 “이미 검토했고 안 하기로 했던 걸 아무도 몰라서 다시 하는 것” 쪽에서 나온다.
739건 중 11건. 비율로는 작지만, 그 11건이 이 리포에서 제일 비싼 문서일 가능성이 높다.
출처와 검증 범위. 위 수치는 2026-08-27 기준 main(b150a55)을 얕은 클론해 직접 센 값이고, 스타·포크 수는 같은 시점의 GitHub REST API 응답값이라 시간에 따라 변한다. 결정 수는 영문 .md 파일만 세고 중문 번역본과 사이드카를 제외한 값이다 — 파일 수(2,217)와 다르다는 점을 본문에 밝혔다. 이 글은 리포의 설계 문서와 디렉터리 구조를 읽은 결과이며, 하네스를 실제로 실행해 동작을 검증하지는 않았다. 샌드박스·크리덴셜의 fail-closed 동작은 각 패키지 README 의 서술이지 내가 재현한 관측이 아니다. 성능·품질 비교는 중립 측정이 없어 다루지 않았다.
References
- DeepSeek, DeepSeek Harness — https://deepseek.com/harness
deepseek-ai/deepseek-harness(main,b150a55) — https://github.com/deepseek-ai/deepseek-harness- 해당 리포
.agents/notes/README.md,packages/sandbox/sandbox-local/README.md,packages/llm/llm-pi-ai/README.md, 루트package.json - Cordis — https://github.com/cordiverse/cordis
- Michael Nygard, Documenting Architecture Decisions (2011) — https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions