HT001 · 터미널 IDE를 맨바닥에서 작성하기¶
2026-09-07, 한 agent가 flower 위에서 macOS 터미널에서 돌아가는 C/C++ 단일 파일 IDE를 맨바닥에서 만들어 냈다. 연속 10.4시간을 돌았고, $171.62를 썼고, 제품 코드 12,212줄과 테스트 13,291줄을 내놓았다. 도중에 네트워크가 한 번 끊겼지만 스스로 이어서 끝까지 돌았다. 이 페이지는 성과 전시가 아니다 —— 프레임워크의 주장 하나하나를 실제 규모에서 재본 기록이고, 그중 성립하지 않는 것으로 판명된 두 가지도 포함한다: 코디네이터가 "권한 제한을 자주 무시한다"는 건 사실이 아니었고, agent가 스스로에게 매긴 인수 판정 중 한 항목은 오판이었다.
전체 기록(19MB transcript 포함)은 ChenyuHeee/cppide에 있다. 아래의 모든 숫자는 tools/analyze_run.py가 sessions.db에서 계산한 값이고, 재현 가능하다:
이 페이지의 숫자는 어디서 나왔나
HT001의 runs/는 위 cppide 저장소 안에 있고, flower 저장소에는 없다. 그래서 이 페이지의 숫자는 당시의 런 기록에서 그대로 옮겨온 것이며, 이 페이지를 쓰면서 sessions.db를 놓고 다시 계산하지는 않았다 —— 다시 계산하려면 먼저 cppide를 clone하고 위 명령을 돌려야 한다. HT002는 반대다: 원본 기록이 이 저장소 안에 있어서, 그 페이지의 숫자는 항목별로 재계산했고 맞지 않은 두 곳은 페이지에 그대로 적어 두었다.
이 런의 모양¶
| 항목 | 값 |
|---|---|
| 과제 | "macOS 터미널에서 돌아가는 C/C++ 단일 파일 IDE를 만들어라, ICPC 훈련용" |
| 비용 | $171.62 —— 요구 확인 $0.3704 / 5턴, 작업 $171.2476 / 31턴 |
| 소요 시간 | 0.06h + 10.44h |
| 메인 스레드 컨텍스트 | 28.7K → 185.9K, 70번의 모델 턴 동안 단조 증가, 전 구간 컴팩트 없음 |
| 분업 | 코디네이터 툴 호출 32회 vs subagent 1,893회; 본문 문자 94.8%가 subagent에 떨어짐 |
| 캐시 | 총 입력 299.4M token, 적중 96.1% |
| 산출물 | src/ 12,212줄, tests/ 13,291줄(약 3,000개 단언), README 44K |
| 버전 | 목표 가드를 넣기 전의 flower, 그래서 이 런에는 턴마다의 판정이 없다 |
이 페이지에는 서로 다른 두 종류의 "턴"이 있다
표의 5턴 / 31턴은 runs/manifest.json이 기록한 num_turns로, 스텝 하나당 하나의 값이다. 아래 컨텍스트 곡선의 "제70턴"은 메인 스레드 위의 모델 턴, 즉 assistant 메시지 하나하나를 센 것이다. 둘은 세는 대상이 다르므로 서로 환산하지 말 것.
1. 컨텍스트 경제학: 현장의 94.8%는 메인 스레드에 들어오지 않았다¶
이것이 프레임워크의 핵심 주장이다 —— 코디네이터에는 결정만 담고, 현장은 subagent와 디스크로 가라앉힌다. 메커니즘 자체는 컨텍스트 경제학을 보라.
| 메인 스레드 | subagent | subagent 비중 | |
|---|---|---|---|
| 모델 턴 | 70 | 3.0K | 97.7% |
| 본문 문자 | 200.1K | 3.6M | 94.8% |
| 툴 호출 | 32 | 1,893 | —— |
이 런은 subagent 23개를 내보냈고, 출력 token은 최대 184,377, 중앙값 72,197, 최소 11,684였다.
직접 손을 댄 툴 호출 1,893회(Bash 1,034 / Read 471 / Edit 258 / Write 126) 중 코디네이터의 시야에 들어온 것은 32회뿐이다 —— 59:1. 한 번 사람을 내보낼 때마다 평균 82개의 툴 호출이 코디네이터에게는 보이지 않는다.
이전의 소규모 테스트에서는 83%가 측정됐지만, 실제 규모에서는 94.8%다. 규모가 커질수록 분업의 이득이 커진다 —— 태스크 브리프와 회신의 비용은 한 번 내보낼 때마다 고정인데, 바깥에 막아 둔 현장은 과제 복잡도에 따라 늘어나기 때문이다.
2. 컨텍스트 곡선: "장기 실행"의 상한이 어디인지 처음 알게 됐다¶
assistant 메시지 하나하나의 input + cache_read + cache_creation이 곧 그 턴에서 모델이 본 컨텍스트다:
- 70턴 동안 단조 증가, 기울기 약 2.2K/턴
- 전 구간 컴팩트가 한 번도 일어나지 않았다(
DISABLE_AUTO_COMPACT=1; 유일하게 값이 내려간 곳은 네트워크가 끊긴 턴의 빈 레코드) - 1M 윈도의 18.6% 사용
이 기울기로 외삽하면 약 440턴에서 벽에 부딪힌다. 이번에는 70턴으로 12K줄의 코드를 만들었으니, 현재 형태에서 장기 실행의 실제 상한은 이번의 약 6배다. 의사결정에 쓸 수 있는 숫자이고, 예전에는 추측밖에 할 수 없었다.
이 상한을 넘으려면 메인 스레드의 증가가 선형 이하여야 한다 —— 더 많이 디스크로 가라앉히고, 대화에는 덜 남겨야 한다.
3. 캐시 적중 96.1% —— $171.62가 성립한 건 전적으로 이것 덕분이다¶
299M 입력 token에 $171밖에 들지 않은 이유는 96%가 캐시 가격으로 나갔기 때문이다.
따름정리: 장기 실행의 경제성은 캐시 적중률 위에 서 있고, 캐시 적중률은 컨텍스트 접두부의 안정성에 달려 있다. "컨텍스트 재배치"류의 최적화 —— 오래된 내용을 옮기거나, 앞쪽에 새 시스템 프롬프트를 끼워 넣는 것 —— 은 모두 캐시를 깨뜨리며, 아낀 token이 잃은 캐시 할인을 전혀 상쇄하지 못할 수 있다. 예전에는 재본 적이 없었는데, 이제 숫자가 있다.
이것은 flower가 컴팩트 대신 핸드오프를 쓰는 부수적인 이유이기도 하다: 핸드오프는 새 세션을 열어 캐시를 처음부터 쌓는 것이라, 한 세대 안에서는 접두부가 계속 안정적이다.
4. 워크벤치 재사용: 61개 스크립트 중 쓰고 버린 것은 하나도 없다¶
워크벤치의 주장은 "스크립트는 한 번 쓰고, 이후에는 그냥 돌린다, 다시 쓰지 않는다"이다. 검증 방법은 agent에게 묻는 게 아니라, .flower/scripts/ 안의 스크립트가 transcript에서 Write/Edit된 횟수와 Bash로 실행된 횟수를 세는 것이다:
wave5-verify.sh는 5번 작성되고 45번 실행됐다 —— 반복해서 다듬고 반복해서 쓰는, 정확히 원하던 모양이다. 쓰고 버린 스크립트는 하나도 없다.
5. 코디네이터가 직접 손대다 거부당한 건 1회, 그것도 화이트리스트의 잘못¶
착수 전의 의심은 이랬다: "코디네이터는 권한 제한을 자주 무시하고, 거부되는 호출을 대량으로 유발한다." 실측은 이 전제를 무너뜨렸다.
툴 호출 32회 중 거부 1회(3.1%), 그리고 그 한 번은 이거였다:
which g++ clang++ make pkg-config 2>&1; echo ---; ls /usr/include/ncurses.h 2>&1; echo ---; ls /work/runs
구간별 판정: echo 통과, ls 통과, which 차단 —— 일회성 명령 목록에 없음.
이 명령 전체는 순수 읽기 전용이고, 표준적인 환경 탐지다. 모델은 완전히 옳게 했고, 화이트리스트가 불완전했다. which g++ 한 줄 때문에 subagent 시작 비용(약 4.3k token)을 한 번 치르게 만들었다.
수정 완료: which / command -v / type / whereis / uname / arch / locale / nproc / getconf / sw_vers를 추가했다. command와 type은 조회 형태만 통과시킨다 —— 맨몸의 command rm -rf /는 실행이고, 그 -v를 빼먹는 순간 뒷문이 열린다. tests/glance.py에 적대적 샘플 7개가 이를 지키고 있다.
여기서 가장 기억할 것은 그 패치가 아니라 방법이다. 오래 끌어온 아키텍처 논의의 출발 전제가 실제 데이터에서는 32분의 1이었고, 게다가 귀인이 반대였다 —— 문제는 모델이 아니라 화이트리스트였다. 먼저 재고 나서 말하라.
6. 네트워크 단절 후 이어 달리기: 실제 장애로 처음 검증됨¶
01:52:40 tool_result: "Agent terminated early due to an API error" ← subagent 하나가 중단됨
01:55:41 assistant: "API Error: ENOTFOUND" ← 메인 스레드가 수신
↓ 프로브가 걸린 채 네트워크 복구를 대기
resumed=True, 같은 세션을 이어서 실행, 8시간 넘게 더 돌아 완료
manifest에는 attempts=2 / resumed=True / ok=True가 기록돼 있다. 10시간짜리 작업을 처음부터 다시 하지 않았다. 그전까지 복원력은 계속 "미검증"에 걸려 있었다 —— 네트워크를 끊으려면 sudo로 hosts를 고쳐야 하는데, 실제 런 중에 그걸 하고 싶은 사람은 없다.
그 합성된 "API Error" 메시지는 PruningSessionStore가 load 시점에 떼어냈고, 모델은 그것을 보지 못했다 —— 프룬 설계도 함께 실제 장애로 검증됐다.
드러난 문제 두 가지(issue #2 참조):
- 복구가 전송 계층이 아니라 프레임워크 계층에서 일어난다. 대가는 컨텍스트 전체를 다시 재생하는 것이고, 그때 컨텍스트는 컸다.
- 중단된 그 subagent의 반쯤 만들어진 작업은 날아갔다. resume이 구하는 건 메인 스레드이고, subagent 수준의 중단 복구는 별개의 문제다.
7. 그 자기 검증은 진짜인가¶
여기가 가장 의심해야 할 지점이다: agent가 직접 테스트를 쓰고, 직접 돌리고, 직접 통과를 선언하는 건 형식적 절차로 전락하기 쉽다.
사후에 독립 감사 subagent를 하나 내보내 스크립트 58개와 산출물 22건을 정적으로 전부 읽게 했다. 결론은 진짜였고, 엄격함이 드물 정도였다:
- 항진 단언 0건. 저장소 전체에
[ 1 = 1 ]같은 패턴이 없다. || true는 총 8곳, 7곳은 정당하다(grep -c가 무매칭 시 1을 반환,ulimit폴백 같은 것).- 판정 대상이 파일 존재가 아니라 동작이다.
audit-idle-cost.sh:53은 IDE를 pty 안에서 60초 동안 유휴 상태로 돌리고, 로컬 가짜 서버가 실제로 받은 요청 건수를 세어-le 2일 때만 PASS다.fix-p1-idle-verify.sh:100-110은 한 걸음 더 나간다: 유휴 시 정확히 1회임을 단언하고, 또한 ghost가 여전히 표시되는지도 단언한다 —— "레이트 리밋 수정이 기능까지 같이 죽이는 것"을 막기 위해서다. - 진짜로 자식 프로세스를 띄운다.
wave5-pty-drive.cpp:316은forkpty로 TUI를 실제로 돌리고,audit-fake-ai-server.py는 진짜 소켓 서버로/err500 /err401 /badjson /drop /slow다섯 가지 장애 주입을 지원한다. 모든 AI 경로에서CPPIDE_API_KEY를 강제로 비운다.
예상을 넘어선 것이 두 가지 있다.
자기 검증기에 양성/음성 대조를 만들었다. 验收审计.md:29-34: 먼저 ZZZ_NEVER_APPEARS_ZZZ를 먹여 드라이버가 FAIL하는지 확인하고, 그다음 진짜 문자열을 먹여 PASS하는지 확인한다 —— "항상 PASS하는 가짜 기계로 감사하는" 상황을 피하려는 것이다. 검증기 자체를 검증하고 있다.
자기 자신의 약한 단언을 스스로 공개했다. wave5-b4-ai-states.sh:114의 for m in $must는 "AI 错误 请求超时"를 공백으로 쪼개 세 개의 부분 문자열로 각각 grep하는데, AI와 错误는 어떤 화면에도 반드시 나타난다 —— 명목상 "3건 통과"지만 실제로 판별력이 있는 건 1건뿐이다. 이 항목은 agent가 스스로 验收审计.md:576-582에 써넣은 것이고, 감사원이 파낸 것이 아니다.
같은 부류로: wave5-verify.sh:296-302는 주석 7줄로 B3b 항목이 왜 SKIP으로 강등됐는지 설명하고 (마지막 화면 타이밍 단언이며 제품 코드와 무관), "판정 로직과 허용 범위는 한 글자도 바꾸지 않았다"고 명기했다. :236-238은 자기가 예전에 쓴 가짜 단언을 스스로 발견하고 고친 것을 기록했다 —— if pgrep ...은 pgrep이 없는 경량 이미지에서 조용히 else로 빠져 항상 PASS가 된다.
재사용의 정성적 증거는 4절의 숫자와 일치한다: audit-fake-ai-server.py는 7개 스크립트에서 재사용됐고, wave5-pty-drive.cpp는 5개 이상의 컴파일에서 재사용되며 [ -x "$DRIVE" ] ||로 컴파일을 캐시한다. wave4 → wave5의 드라이버는 재작성이 아니라 확장이다(diff는 1줄만 수정, 343줄 추가, wave4의 281줄은 그대로 유지).
이 모든 행동은 목표 가드가 없는 버전에서 일어났다. 유일한 제약은 브리프 안의 판정 가능한 인수 기준 10개였다. 이는 곧: "완료"를 판정 가능한 형태로 쓰는 것만으로도 자기 감사와 자기 공개를 유발할 수 있다는 뜻이다. 목표 가드의 한계 가치는 0이 아니라 이것을 기준선으로 놓고 재야 한다.
하지만 다음 절이 보여준다: 자기 감사는 검증 기술에서는 매우 강했지만, 가장 중요한 항목의 최종 판정에서 틀렸다.
여기서 말하는 "감사원"은 판정자가 아니다
7절과 8절에서 말하는 독립 감사원은 코디네이터가 자발적으로 내보낸 subagent이지, 목표 가드의 판정자가 아니다 —— 이 런에는 목표 가드 자체가 없었다. 둘은 읽기에는 비슷하지만 하는 일이 다르다: 감사원은 사후 복기고, 판정자는 매 턴이 끝날 때의 판정 게이트다.
8. 스스로 매긴 점수는 맞았나 —— 인수 1은 오판이었다¶
두 번째 독립 감사원을 또 내보내, 그 10개 인수 기준을 코드와 대조해 항목별로 판정하게 했다.
인수 2–10은 전부 사실이었고, 코드 증거가 모두 있었다. 연습 모드에서 코드를 삽입하지 않는 것은 심지어 구조적 보장이다: 유일한 sink 결정 지점 ai.h:117 + 유일한 적용 switch app.cpp:590-615 + 네 개의 early-return editor.cpp:679-683, 흩어진 if가 아니다. 자기 감사로 발견한 P1–P4도 전부 사실이고 수정도 전부 반영됐다 (P4는 절반만 사실: 그 파란색 pair 줄은 죽은 코드이고, ghostAttr()은 원래부터 A_DIM을 탄다).
하지만 인수 1은 오판이었다.
기준 원문: "독립 실행 파일 하나를 컴파일해 macOS 터미널에서 바로 실행"
납품된 산출물: ELF 64-bit LSB pie executable, ARM aarch64, GNU/Linux
agent의 판정: 통과
이 항목은 사람이 직접 file을 돌려 확인했다.
숨기지는 않았다 —— 交付说明.md §4의 제목이 바로 "가장 큰 미검증 리스크: macOS에서 한 번도 돌려보지 않음"이고, 감사 §6도 macOS의 미검증 지점 6개를 나열했다. 리스크는 공개했지만, 그 사실을 인수 1의 판정으로 되돌려 연결하지는 않았다. 그런데 인수 1의 문언상 요구가 바로 그 일이었다.
하지만 이것이 "독립 판정자가 필요하다"는 증거는 아니다¶
이 항목을 손쉽게 "그러니까 목표 가드가 필요하다"로 읽기 쉽다. 틀렸고, 그렇게 읽으면 화살을 먼저 쏘고 과녁을 그리는 것이다 —— 바로 앞 절에서 비판한 그 동기화된 추론이다.
당시 버전의 목표 가드도 기본 설정에서는 이 항목을 놓쳤을 가능성이 크다. judge()의 기본값은 can_run=False이고, 툴은 Read / Glob / Grep뿐이다 —— file을 돌릴 수 없다. 소스를 읽으러 가서 Makefile:25-38에 실제로 Darwin 분기가 있는 걸 보고 "달성"으로 판정할 것이다. 자기 감사와 똑같은 실패다.
진짜 구조적 사실¶
인수 1은 이 런의 환경에서는 애초에 검증 불가능하다.
이 항목이 단언하는 것은 "산출물의 macOS 상 동작"인데, 런 전체가 Linux 컨테이너 안에서 돌았다. 컨테이너 안에서 macOS 바이너리를 검증할 수 있는 판정자는 없다, 자기 감사든 독립이든 상관없이. 문제는 "누가 판정하느냐"가 아니라, 이 항목이 평범한 판정 가능 기준으로 취급되어 판정되었다는 데 있다.
감사원이 추가로 파낸 세 가지(자기 감사는 전부 언급하지 않았다)¶
doTab()의 쿼터 의미:app.cpp:909가acceptGhost()이후 무조건noteActivity()→gen_++→ 쿼터 재개방을 하므로, Tab으로 수락할 때마다 500ms 뒤에 자동 요청이 한 번 더 발생한다. P1(유휴 상태에서 돈이 타는 문제)의 분석은setMode/cancelInFlight/askNow세 개의 취소 지점만 봤고, 가장 빈번한 이 과금 경로를 놓쳤다.- 인수 6의 "회색"은 256색 터미널에서만 문자 그대로 성립한다. 8색 단계에서는
A_DIM으로 고정된다(어두워지는 것이지 회색이 아니다). 验收审计.md§7의 소제목은 "인수 기준 9개"라고 쓰여 있지만 표에는 10줄이 있다 —— 오타지만, 종합 평가 표의 머리말에 나타났다.
9. 스필과 워크벤치의 최종 규모¶
spill 103회, 791.4K 문자가 경로 포인터로 치환됐고, 컨텍스트에 상주하지 않았다.
.flower/ 최종: 스크립트 58개(250K), 산출물 22건(330K), 노트 5건(43K).
남은 리스크¶
납품 대상은 macOS인데, 검증은 전부 Debian aarch64 컨테이너 안에서 이뤄졌고, macOS 실기 검증은 0이다. 이는 컨테이너 격리 방식의 실제 부작용이다: 안전을 위해 Linux에 가둬 놓았는데, 납품 대상은 macOS다. 프로젝트 자체 README §10.24는 이를 정직하게 기록하고 리스크 4개를 나열했지만, 닫히지는 않았다.
이 공백은 나중에 HT002가 메웠다 —— 그 런이 바로 이 바이너리를 실제 macOS에 설치하는 일이었다.
의외의 이득: 컨테이너 안에서는 /work만 보이기 때문에, 19MB transcript에서 호스트 사용자명 /Users/hechenyu 유출이 0건이다(/work는 11,495회 등장). 격리가 경로까지 덤으로 격리했다.
이 런이 검증하지 못한 것¶
- 목표 가드: 이 런은 그것을 넣기 전이다. 코디네이터가 자발적으로 독립 감사를 조직했고 검증 기술은 매우 단단했지만, 가장 중요한 항목에서 오판했다(8절). 두 결론을 함께 기억해야 한다 —— 브리프에 판정 가능한 기준을 명확히 쓰면 자기 감사가 유발되므로 목표 가드의 기준선은 0이 아니고, 당시 기본 설정의 목표 가드 역시 그 항목을 놓쳤을 것이므로 그것도 만병통치약은 아니다.
- 1M 윈도의 거동: 18.6%만 썼고, 경계에 닿지 않았다.
DISABLE_AUTO_COMPACT=1하에서 마이크로 컴팩트의 거동: 전 구간 컴팩트가 발동하지 않았으므로 여전히 추론이다.
이 런이 프레임워크를 어떻게 바꿨나¶
| 발견 | 반영된 변경 |
|---|---|
which가 차단됨(5절) | is_ephemeral()에 which / command -v / type / whereis / uname / arch / locale / nproc / getconf / sw_vers 추가; tests/glance.py에 적대적 샘플 7개를 넣어 command / type의 조회 형태를 지킴 |
| 인수 1 오판(8절) | JUDGE_RULES 1번 항목을 소스가 아니라 산출물을 판정하도록 변경 —— 돌릴 수 있으면 돌리고, 돌릴 수 없어도 산출물 자체(플랫폼, 크기, 로드 가능 여부)를 검사할 것 |
| "리스크는 공개했다"가 통과 하나와 맞바꿔졌다 | 판정은 "못 했다"와 "여기서는 검증할 수 없다"를 구분해야 한다. Verdict.parse는 이제 검증 불가 / 확인 불가 / 판정 불가를 제3의 상태 unreachable로 인식한다 → 사람에게 올려 묻고, 통과 판정은 절대 허용하지 않는다 |
| 인수 1은 이 환경에서 원리적으로 검증 불가 | 목표를 설정하는 단계에서 이 환경에서 검증할 수 없는 항목을 그 자리에서 표시하도록 요구, 작업을 끝낸 뒤에 발견하지 않도록 |
| 단절 복구가 프레임워크 계층에서 일어나고 subagent의 반제품이 유실됨(6절) | issue #2로 기록, 미해결 |
앞의 세 항목은 HT002에서 처음으로 실제 API에 올라갔고, 그중 "소스가 아니라 산출물을 판정"은 그 자리에서 한 번 구제했다 —— 하지만 같은 규칙이 새로운 실패도 하나 만들어 냈다.