50만 LOC의 코드 마이그레이션은 한 명의 영웅 엔지니어로 처리되지 않는다.#

oven-sh/bun의 한 SHA 시점(23427dbc12fdcff30c23a96a3d6a66d62fdc091d) .claude/workflows/ 디렉토리에는 52개의 *.workflow.js 파일이 있다. 약 380 KB. 모두 Claude Code 서브에이전트 swarm을 오케스트레이션하는 Node 스타일 JS 모듈이다. 공통 런타임은 다음 함수들로 이루어진 미니 DSL이다.

  • agent(prompt, { label, phase, schema })
  • parallel(fns[])
  • pipeline(items, ...stages)
  • phase(name)
  • log(msg)

Bun 팀이 자체 harness 위에 빌드한 워크플로우 언어다. 단일 거대 prompt도, 한 명의 영웅 엔지니어도 아니다. 시스템 전체를 관통하는 메커니즘은 “implement → 2-vote verify → fix → re-verify” 마이크로 루프의 무한 반복이다. 한 에이전트가 코드를 쓰고, 두 명의 적대적 검증자가 동시에 리뷰하고, 둘 다 동의한 bug만 적용한다.

이 분석은 52개 파일을 읽고 패턴을 추출한 보고서다. 핵심 관찰은 두 가지다. (1) 시스템의 약 90%는 zig-rust 특화 컴포넌트를 벗어나 일반 코드 마이그레이션에 재사용 가능하다. (2) 핵심 메커니즘은 거대 prompt가 아니라 작은 적대적 검증 루프 + structured output schema + ground-truth 룰북의 자기 개선이다.

Source: oven-sh/bun @ 23427dbc12fdcff30c23a96a3d6a66d62fdc091d / .claude/workflows/1 Scope: 52개 워크플로우 파일, 약 380 KB, 50만+ LOC 마이그레이션 오케스트레이션


I. Phase ladder: 10단계 게이트#

워크플로우는 알파벳 순으로 A, B0, B1, B2, C, D, E, F, G, H를 따른다. 각 단계는 명확한 입출력 invariant를 갖는 게이트다.

Phase 입력 출력 invariant
pre .zig 소스 docs/LIFETIMES.tsv (ownership 11분류)
A .zig + LIFETIMES.tsv draft .rs (compile 여부 무관)
B0 draft .rs 크레이트 DAG cycle 제거
B1 acyclic graph per-crate cargo check 통과 (gate-and-stub)
B2 gated crates gate 제거, logic 정확성 유지
C linked binary bun_bin 최소 commands 실행
D per-crate ok workspace 전체 컴파일
E compiling workspace 모든 todo!(), gate, stub 제거
F working binary refactor + accessor + smoke tests
G smoke-tested bun 자체 test suite 통과율 상승
H passing tests cross-platform + parity + hardening

ladder는 “draft → cycle break → 컴파일 → 실행 → 테스트 → 하드닝” 이라는 마이그레이션의 일반 순서다. B0(cycle break)와 pre-phase(ownership classification)만 Rust 특화고, 나머지는 어떤 source/target 쌍에도 적용 가능하다.

Phase H의 파일 구성(reliability-hardening, main-parity, windows-bughunt 등)으로부터 역추론하면 이 SHA 시점에 이미 cargo build가 linking되고 bun_bin이 실행은 되는 상태다2. 남은 작업은 테스트 통과율 상승과 main 브랜치와의 drift 관리.


II. 시스템의 정수: Implement → 2-vote verify → Fix → Re-verify#

거의 모든 phase의 마이크로 루프다. 이게 Bun 시스템에서 가장 일반화 가능한 패턴이다.

2-vote intersection#

두 verifier가 동시에 reviewing한다. 둘 다 flag한 bug만 “agreed"가 된다. 한쪽만 flag한 bug는 tiebreaker로 넘어가고, tiebreaker의 default는 reject다.

// phase-b2-cycle.workflow.js의 verify 단계 요약
const v1 = await agent(verifyPrompt(file), { label: 'verify-1', schema });
const v2 = await agent(verifyPrompt(file), { label: 'verify-2', schema });
const agreed = intersect(v1.bugs, v2.bugs);
const disputed = symmetricDiff(v1.bugs, v2.bugs);
const tiebreaker = await agent(tiebreakPrompt(disputed), {
  label: 'tiebreak',
  schema: { confirmed: 'bool=false' }, // default false
});
const confirmedBugs = [...agreed, ...tiebreaker.filter(b => b.confirmed)];

설계 의도는 명확하다. AI verifier 한 명은 환각을 만든다. 두 명이 동시에 같은 환각을 만들 확률은 훨씬 낮다. 의심스러우면 reject가 기본값이다.

3-vote refute#

false-positive 비용이 높은 경우(porting-md-zigleakage, lifetime-classify)에는 3-vote refute를 쓴다. 세 검증자가 모두 refute하지 못해야 finding이 통과한다.

Default to reject#

모든 verifier prompt가 명시적으로 Default to refuted=true / accept:false unless clearly correct를 박아둔다. 명시적으로 confirm하지 않으면 기각이다.


III. 결정론적 경계: Per-X parallelism + HARD RULES#

각 워크플로우가 명시적인 unit-of-parallelism을 정한다. per-file, per-crate, per-symbol, per-test-signature, per-edge 중 하나.

boundary가 침범되지 않도록 HARD RULES가 prompt에 박힌다.

  • Edit ONLY src/${c.name}/
  • Never edit .zig
  • Never run git reset/checkout/stash
  • Never delete .rs files
  • Never create new files (only Edit existing)

이 boundary 규율 덕에 50~170개의 concurrent 에이전트가 race condition 없이 동시 편집한다. 에이전트의 자유도를 의도적으로 깎는 게 시스템 설계의 핵심이다.

phase-a-port.workflow.js가 보여주는 또 다른 결정론적 설계: rsPathFor() 함수가 zig 경로를 받아 rs 경로를 결정한다. 에이전트가 출력 경로를 선택하지 못한다. mod.rs / lib.rs / <base>.rs 규칙이 코드에 박혀 있다.

1000 LOC 이상의 zig 파일은 강제로 800 라인 chunk로 나눠서 Write/Edit한다. harness가 180초 tool-call timeout으로 에이전트를 죽이기 때문이다. 제약을 알고 회피하는 설계.


IV. Structured output schema 강제#

모든 agent 호출에 schema가 지정된다. enum, required field, 타입이 강제된다.

흔한 필드들:

schema: {
  ok: 'bool',
  confidence: 'enum[high|medium|low]',
  severity: 'enum[block|warn|nit]',  // 또는 enum[logic-bug|incomplete|perf|nit]
  blocked_on: 'array<string>',
  fns_touched: 'array<string>',
  notes: 'string',
}

이게 있어야 메인 워크플로우 코드가 결과를 통계 집계하거나, 분기하거나, 다음 phase의 입력으로 변환할 수 있다. 에이전트가 free-form 텍스트를 반환하면 사람이 매번 끼어들어야 한다.

phase-a-port의 schema는 confidence: enum[high|medium|low], todos: integer, rs_loc: integer까지 강제한다. 메인 에이전트가 통계를 집계할 수 있게 설계된 형태.


V. Ground-truth doc + 적대적 자기 개선#

docs/PORTING.mdcontract다. 모든 에이전트가 작업 전에 이 파일을 읽는다. 에이전트 prompt가 그 룰을 cite한다.

추가 contract 문서들이 phase별로 존재한다: docs/CYCLEBREAK.md, docs/LIFETIMES.tsv, docs/RUST_IDIOMS_AUDIT.md, docs/CRASH_LEAK_ISSUES.md.

여기까지는 평범하다. 비범한 것은 다음이다.

porting-md-zigleakage.workflow.js가 PORTING.md 자체를 audit하고 개선한다. 8개 dimension의 auditor가 병렬로 동작한다: allocator-threading, collections, manual-lifetime, error-model, pointer-idiom, comptime-carryover, api-shape, trial-port-diff. 각 finding에 3-vote 적대적 refute가 붙고, 통과한 finding만 PORTING.md patch가 된다.

룰북이 자기 개선하는 메타 루프다. PORTING.md가 좋아지면 모든 phase 워크플로우의 prompt quality가 동시에 향상된다. 사람이 처음에 SEED finding(allocator threading, collections, perf markers)을 hard-code하고, 나머지는 AI 탐색이 채운다. 사람의 prior + AI의 탐색의 조합.


VI. Daemon-mediated build state#

cargo check는 workspace 단위로 분 단위가 걸린다. 100개 이상의 concurrent 에이전트가 각자 cargo를 돌리면 시스템이 죽는다.

해결: 워크플로우 바깥의 별도 daemon이 /tmp/cargo-check.log를 항상 최신 상태로 유지한다. 에이전트는 cargo를 직접 실행하지 않고 로그를 read한다. mtime 검증 후 stale이면 한 번만 실행한다.

결과적으로 100개 이상 concurrent 에이전트가 작업해도 cargo는 동시에 한 번도 돌지 않는다. 빌드 상태가 sharded read의 대상이 되도록 추상화한 것.

이 패턴은 target 빌드시스템 특화지만(cargo, go build, tsc --watch 등 각자), 원리는 일반화된다. 비싼 외부 상태는 daemon이 캐싱하고, 에이전트는 read-only.


VII. Worktree isolation + Banned-list injection#

Worktree isolation#

Phase F부터 H의 review-heavy 워크플로우들은 git worktree add로 격리한 worktree에서 작업한다. cgroup으로 리소스 격리, 결과는 patch 형태로만 반환. main repo는 검증 후 cherry-pick.

동시 100개 이상 작업이 main에 직접 쓰면 conflict가 폭발한다. Worktree isolation이 이걸 막는다.

Banned-list injection#

phase-e-proper-port.workflow.js의 BANS 섹션이 대표 예시다.

BANNED workarounds:
- todo!() / unimplemented!() / unreachable!("stub") — ZERO.
- #[cfg(any())] / mod _gated / mod phase_a_draft — ZERO.
- &self as *const _ as *mut _ 같은 &mut cast (UB).
- &'static을 만들기 위한 Box::leak.
- mem::forget.
- let _ = result; 같은 error swallowing.
- Transliterated Zig.

이런 anti-pattern을 negative example로 prompt에 inject한다. 에이전트가 자기 도구를 어떻게 잘못 쓸 수 있는지 미리 본 상태에서 작업한다. doctrine이 명시적이다: “LAYERING IS THE ROOT FIX. todo!("blocked_on: X")는 거의 항상 dep cycle 때문이다. 진짜 fix는 stub이 아니라 X의 타입을 더 낮은 crate로 MOVE하는 것이다.”

Verify가 accept하려면 slop_found == 0 AND no layering-workaround AND no logic-bug이 2/2 vote로 통과해야 한다. 명시적 합격선.


VIII. Steelman: “이건 Zig→Rust 특화일 뿐 일반화 불가”#

가장 강한 반론을 정직하게 펼쳐보자.

“이 시스템은 Rust 특유의 제약 — ownership, lifetime, cycle break, borrow checker — 에 맞춰 진화한 결과다. 동적 언어 target에는 cycle break도 ownership classification도 의미가 없다. 시스템 전체가 Rust target에 종속되어 있어서 다른 마이그레이션에 그대로 옮기면 부적합한 패턴이 따라온다.”

이 반론은 부분적으로 옳다. 다음 컴포넌트들은 Rust 특화다.

  • LIFETIMES.tsv pre-classification: ownership 11분류는 borrow checker가 있는 target 한정. Go는 거의 불필요, Python은 무의미.
  • B0 cycle break: 정적 타입 + 모듈 시스템 + cycle 금지 target 한정. TS, Python target이면 생략 가능.
  • #[cfg(any())] gate-and-stub: Rust 특유의 cfg 어트리뷰트.

그런데 분석해보면 이게 전체의 약 10%다. 나머지 90%는 source/target 쌍과 무관하다.

패턴 특화 정도
Phase ladder (A~H) 일반화 가능, B0만 정적 타입 한정
Implement → 2-vote verify → Fix 완전 일반화
Per-X parallelism + HARD RULES 일반화 (unit만 다름)
Structured output schema 완전 일반화
Ground-truth doc + 자기 개선 완전 일반화
Daemon build state 빌드시스템 특화, 원리는 일반화
Worktree isolation git 한정, 일반화
LIFETIMES.tsv pre-classification Rust 특화
Banned-list injection 완전 일반화

비대칭 리스크는 이렇다.

  • 패턴이 일반화 가능하다고 보고 다른 마이그레이션에 적용했는데 실패: 시간 손실. plan 단계가 cheap·결정론적이므로 잘못 짠 plan은 버리고 다시 짠다.
  • 패턴이 zig 특화라고 보고 자체 영웅적 마이그레이션을 시도: 100K LOC 이상에서 사실상 종료 불가능. 역사적으로 검증된 실패 모드.

비대칭이 명확하다. 패턴 적용 실패의 한계 비용은 plan 재작성이지만, 영웅 마이그레이션 실패의 한계 비용은 프로젝트 자체의 죽음이다.


IX. 적용 가능한 7가지 지표#

자체 마이그레이션 프로젝트에 적용할 추적 가능한 체크리스트.

  1. Plan-then-execute 분리. plan은 cheap·결정론·사람 검토 가능. execute는 expensive·parallel·plan 의존. 같은 plan으로 여러 번 resume/retry 가능해야 한다.

  2. 모든 에이전트 호출에 structured output schema 강제. enum, required field, 타입. 다음 단계가 분기 가능해야 한다.

  3. 2-vote intersection 적용. verifier 두 개 동시 실행, 둘 다 flag한 것만 fix. 한쪽만 flag는 tiebreaker, tiebreaker의 default는 reject. 의심스러우면 기각이 기본값.

  4. Ground-truth 룰북(PORTING.md 등가물)을 적대적 audit loop로 자기 개선. 사람이 SEED finding을 hard-code하고, 적대적 검증자가 새로운 finding을 찾고, 통과한 것만 룰북에 반영.

  5. 빌드 시스템이 분 단위면 daemon으로 추상화. 에이전트는 로그만 read. 한 번에 한 번만 실행. concurrent 에이전트가 폭발하지 않게.

  6. Per-X parallelism boundary + HARD RULES. “Edit ONLY ${this scope}”, “Never X”. 에이전트의 자유도를 의도적으로 깎는 게 race condition 방지의 핵심.

  7. Worktree isolation for risky refactors. 위험한 변경은 격리된 worktree에서, 결과는 patch로 반환, main은 검증 후 cherry-pick.


X. 워크플로우 파일 전체 목록 (부록)#

총 52개 파일, 약 380 KB3.

파일 Phase 크기
lifetime-classify.workflow.js pre-phase 7.2 KB
phase-a-port.workflow.js A 9.7 KB
phase-b0-cyclebreak.workflow.js B0 5.1 KB
phase-b0-movein.workflow.js B0 3.9 KB
phase-b0-moveout.workflow.js B0 3.9 KB
phase-b0-verify.workflow.js B0 2.9 KB
phase-b1-tier.workflow.js B1 2.6 KB
phase-b2-cycle.workflow.js B2 7.1 KB
phase-b2-fill-blocked.workflow.js B2 3.4 KB
phase-b2-fill.workflow.js B2 2.9 KB
phase-b2-fix-bugs.workflow.js B2 2.1 KB
phase-b2-keystone.workflow.js B2 5.5 KB
phase-b2-ungate-tier.workflow.js B2 2.5 KB
phase-b2-verify.workflow.js B2 4.8 KB
phase-c-panic-swarm.workflow.js C 8.1 KB
phase-d-blocked-on-resolve.workflow.js D 4.1 KB
phase-d-build-queue.workflow.js D 8.7 KB
phase-d-bundler-perfile.workflow.js D 3.8 KB
phase-d-bundler-shard.workflow.js D 5.0 KB
phase-d-crate-shard.workflow.js D 6.6 KB
phase-d-recursive-ungate.workflow.js D 9.7 KB
phase-d-subtree-batch.workflow.js D 9.1 KB
phase-d-todo-sweep.workflow.js D 8.1 KB
phase-d-unsafe-audit.workflow.js D 6.9 KB
phase-e-body-port.workflow.js E 5.8 KB
phase-e-mass-ungate.workflow.js E 4.8 KB
phase-e-proper-port.workflow.js E 11.1 KB ★ 가장 정교
phase-e-scopeguard-sweep.workflow.js E 5.2 KB
phase-e-test-bringup.workflow.js E 7.5 KB
phase-f-accessor-sweep.workflow.js F 7.5 KB
phase-f-probe-swarm.workflow.js F 6.1 KB
phase-f-reviewed-refactor.workflow.js F 8.3 KB
phase-f-test-swarm.workflow.js F 5.9 KB
phase-g-mega-swarm.workflow.js G 12.4 KB
phase-g-test-swarm-isolated.workflow.js G 9.3 KB
phase-g-test-swarm-v3.workflow.js G 11.9 KB
phase-g-test-swarm.workflow.js G 7.1 KB
phase-h-ci-tasks.workflow.js H 9.1 KB
phase-h-classify-issues.workflow.js H 6.4 KB
phase-h-dedup.workflow.js H 15.4 KB ★ 가장 큼
phase-h-deep-dive.workflow.js H 4.6 KB
phase-h-diff-review.workflow.js H 4.9 KB
phase-h-idioms-audit.workflow.js H 9.4 KB
phase-h-libuv-audit.workflow.js H 10.2 KB
phase-h-main-parity.workflow.js H 6.3 KB
phase-h-portnotes-survey.workflow.js H 8.3 KB
phase-h-unsafe-wrap.workflow.js H 16.5 KB
phase-h-windows-bughunt-wt.workflow.js H 9.2 KB
phase-h-windows-bughunt.workflow.js H 9.9 KB
phase-h-windows-errors.workflow.js H 4.7 KB
phase-h-windows-singlefix.workflow.js H 4.8 KB
phase-h-windows-testfix.workflow.js H 8.9 KB
porting-md-zigleakage.workflow.js meta 12.4 KB ★ 자기개선

TLDR#

  1. Bun 팀은 50만 LOC Zig→Rust 마이그레이션을 한 명의 영웅 엔지니어가 아니라 52개 워크플로우 + AI swarm으로 오케스트레이션
  2. 시스템의 정수: “implement → 2-vote verify → fix → re-verify” 마이크로 루프의 무한 반복. 두 검증자 intersection만 적용, tiebreaker default는 reject
  3. Phase ladder는 10단계 게이트(A~H), 각 단계가 명확한 입출력 invariant
  4. Ground-truth 룰북(PORTING.md)이 contract. 룰북 자체가 적대적 audit loop로 자기 개선 (porting-md-zigleakage 워크플로우)
  5. Daemon이 빌드 상태 추상화 → 100개 이상 concurrent agent가 cargo 동시 호출 없이 작업
  6. 7가지 핵심 패턴 추출. 90%는 source/target 쌍과 무관하게 재사용 가능. 비대칭 리스크 명확: 패턴 적용 실패 < 영웅 마이그레이션 실패

Sources#

  • oven-sh/bun @ 23427dbc12fdcff30c23a96a3d6a66d62fdc091d, .claude/workflows/ 52개 파일 (380 KB)1
  • Phase H 워크플로우 분포로부터 현재 시점 추정2
  • 파일 목록·크기는 SHA 시점 ls -la 결과3


  1. 약 50만 LOC는 본 SHA 시점의 oven-sh/bun 리포 전체(Zig + C++) 추정치. 정확한 LOC는 tokei 또는 cloc로 측정해야 한다. Bun 리포가 빠르게 성장하고 있어 SHA에 따라 다름. ↩︎ ↩︎

  2. 이 추정은 Phase H 워크플로우의 종류(reliability-hardening, parity tracking)와 그 존재가 전제하는 시스템 상태에서 역추론한 것. 실제 빌드 상태는 해당 SHA에서 cargo build를 직접 돌려야 확인 가능. ↩︎ ↩︎

  3. 파일 목록과 크기는 본 SHA 시점의 ls -la .claude/workflows/ 결과 기준. 이후 커밋에서 추가/삭제/리네임이 발생했을 수 있다. ↩︎ ↩︎