실무 프로덕트에 하네스 구축해보기 (세 앱을 모노레포로 합치고 에이전트에 울타리를 쳐보자)
세 개의 분리된 프론트엔드 레포를 Turborepo 모노레포로 합치고, 컴파운드 엔지니어링 기반 하네스를 구축한 회고입니다. 모노레포 통합부터 6-Phase 사이클, Agent-Agnostic 폴더 구조까지 정리했습니다.
프롤로그
한 사이클 만에 같은 실수를 두 번 하지 않는 시스템을 만들고 싶었습니다.
이 글에서는 그 욕망에서 출발해 프론트엔드 모노레포 통합과 에이전트 하네스 구축을 같이 진행한 이유, 그리고 그 결과 어디까지 갔고 무엇이 남았는지를 정리해 보았습니다.
기존 앱 문제
저는 현재 원격상담 솔루션인 RemoteVS (새 창에서 열림)(RVS, Remote Visual Sales & Support)라는 제품을 개발 및 유지보수하고 있습니다. 이 제품은 비대면 화상 상담 이 주 서비스라, 상담을 진행하는 상담원, 상담을 받는 고객, 모든 상담을 관리하는 어드민 까지 세 종류 사용자를 각각 하나의 앱으로 다룹니다. 외부 고객사에 솔루션 형태로 납품되는 제품이라 끊임없이 새로운 커스텀 요구사항이 밀려들어와요. 그 속에서 AI 에이전트로 빠르게 개발해보자 시도해 봤지만 그조차도 잘 풀리지 않았고, 세 앱이 각각 다른 위치에 흩어져 있어서 유지보수도 점점 힘들어지는 상황이었습니다.
각자 화면이 다르고, 인증 방식이 다르고, 통신 채널이 다릅니다. 그런데 도메인은 같아요. 같은 세션을 보고, 같은 이벤트로 움직입니다.
오랫동안 우리 팀은 이 셋을 세 개의 분리된 레포로 운영했습니다.
상담원/고객/어드민이 각기 다른 레포
세 앱은 모두 한 세션을 공유합니다. 같은 통신 채널로 메시지가 오가고, 같은 도메인 이벤트에 반응해요. 그런데 구현은 갈라져 있었습니다.
각 앱은 만들어진 시기가 달랐고, 그래서 사용하는 스타일 시스템·상태 관리·UI 라이브러리 까지 모두 갈라져 있었어요. 한쪽은 비교적 최신 스택, 다른 쪽은 레거시. 게다가 일부 앱은 번들 분리나 플랫폼 분기 같은 구조적 차이까지 더해지는 상태였습니다.
각자의 컨벤션, 각자의 빌드, 각자의 테스트. 같은 도메인 변경(예: 공통 프로토콜 특정 카테고리에 새로운 필드 추가)을 세 번 별도로 수정해야 했습니다. 변경 누락이 생겼고, 인터페이스 drift가 누적됐어요.
AI 활용이 어려움
작업자가 한 번에 한 레포만 보면 그래도 컨텍스트가 좁습니다. 에이전트는 다릅니다. "이건 host 작업이지" 하고 시작했어도 도중에 viewer 코드를 참조하다가 viewer 패턴으로 호출 규약을 바꿔버리는 식의 실수가 자주 났어요. 같은 기능을 앱마다 다른 패턴으로 풀어야 하는데, 한 번 컨텍스트가 부패하면 결과물이 섞여 나왔습니다.
세 레포가 분리되어 있는 한 컨벤션 강제 를 한 곳에 둘 곳이 없었습니다. lint 룰을 셋 다 동기화하면 그게 또 drift의 원인이 됐고요.
이 상황을 정리해보면 두 가지 근본 문제로 좁혀집니다.
- 컨텍스트 부패: 작업이 길어질수록 에이전트가 무엇을 알고 있는지 작성자도 모르게 됩니다.
- 규칙·울타리 부재: 알고 있어도 강제할 방법이 없어요. "any 쓰지 마" 는 부탁이지 강제가 아닙니다.
결론
레포 통합 자체가 목적이 아니었습니다. 하네스를 둘 자리 가 필요했어요. 하네스는 한 곳에 있어야 합니다.
그래서 팀원들에게 "이참에 앱들을 하나의 모노레포로 합치고, 그 위에 하네스 환경을 제대로 구성해보겠습니다" 라고 설득했고, 다행히 동의를 얻어 본격적인 작업이 시작됐어요.
Turborepo 적용 모노레포 구성
합칠 때 결정한 것들
작업을 시작하기 전에 두 가지 조건부터 분명히 했어요. 기존 앱의 코드는 건드리지 않을 것, 그리고 결함이 생기면 안 될 것. 운영 중인 제품이라 통합 작업 자체가 사용자 영향을 만들면 안 됐거든요.
그래서 일단 세 앱을 그대로 apps/{rvs-host, rvs-viewer, rvs-admin} 아래로 모았습니다. 코드는 손대지 않고 위치만 옮긴 거예요. 패키지 매니저는 pnpm 10, 빌드 오케스트레이터는 Turborepo 2. 공유 자산은 tools/ 아래로 분리했어요.
tools/tsconfig: base / app / library 세 종 tsconfigtools/eslint-config: 모듈 경계 룰(eslint-plugin-boundaries) 포함
결과
- 한 곳에서 같은 lint·format 규칙으로 모든 앱이 검증됩니다.
pnpm bootstrap한 번이면 의존성 설치부터 초기 셋업까지 끝나요.- 같은 컨벤션을 적용할 곳이 한 곳뿐이라 이제 거기에 하네스를 얹을 수 있게 됐습니다.
Compound Engineering + 하네스 환경
하네스의 3기둥
하네스는 한 마디로 모델이 아닌 것 전부 입니다. 모델 자체는 야생말이고, 그 말이 올바른 방향으로 달리도록 채우는 마구가 하네스예요. 마구는 말의 힘을 억누르는 게 아니라 원하는 경로로 빠르게 달리도록 모아주는 장치입니다.
마구가 필요한 본질적인 이유는 LLM이 비결정적 이라는 데 있어요. 같은 입력을 줘도 매번 조금씩 다른 결과가 나옵니다. 그게 창의성의 원천이긴 하지만, "같은 작업은 같게 처리되어야 한다" 는 실행의 안정성과는 정면으로 충돌해요. 창의성은 허용하되, 실행은 안정적으로. 모델 안에서 완전한 결정성을 보장받는 건 어려우니까, 재현 가능성을 바깥에서 끌어올리는 방향으로 가야 해요.
우리 환경에서 그걸 어떤 형태로 만들지 한참 고민한 끝에, 세 개의 기둥으로 정리됐습니다.
- 컨텍스트 파일: 매 세션을 깨끗한 상태로 시작하는 에이전트에게 이 프로젝트가 어떤 컨벤션을 따르는지·어떤 함정을 피해야 하는지 를 부팅 시점에 알려주는 텍스트입니다. 매일 기억을 잃은 채 출근하는 신입 개발자에게 매번 새로 읽혀주는 README와 같아요.
- 자동 강제 시스템: 프롬프트가 부탁이라면 하네스는 강제입니다. "any 쓰지 마" 라고 부탁하는 게 아니라, any가 들어간 코드가 통과하지 못하는 메커니즘을 둡니다. 같은 실수를 두 번 못 하게 만드는 핵심 층이에요.
- 가비지 컬렉션: 사이클이 끝나면 잔재(미사용 import, 임시 주석, 빈 파일)를 자동 청소합니다. 그리고 이번 사이클에서 발견된 새로운 실수 패턴 이 있다면 그 자리에서 룰로 승격되어, 다음 사이클에서는 같은 실수가 구조적으로 불가능해져요.
6-Phase 사이클
작업 단위는 레드마인 일감 한 건입니다. 사용자가 짧은 프롬프트(예: #266841 진행 해줘)로 트리거를 발생시키면, 하네스 안의 에이전트들이 6단계를 자동으로 진행하고 그 끝에 마무리 단계 SWEEP이 자동으로 따라붙어요.
EXECUTE에서 개발 에이전트가 첫 구현을 마치면, 그 결과물을 리뷰 에이전트가 받아 REVIEW를 돌립니다. 피드백이 있으면 다시 개발 에이전트에게 넘겨서 FIX 단계로 수정 작업이 이어져요. 이 리뷰 ↔ 수정 핑퐁은 합산 3회까지만 허용합니다. 3회를 넘기면 ESCALATE. 사이클을 멈추고 사용자에게 바로 보고합니다.
마지막 SWEEP은 VALIDATE PASS 직후·CLOSE 직전에 자동으로 끼어드는 단계로, 이번 사이클의 잔재를 청소하고 발견된 학습을 다음 사이클의 룰로 누적시킵니다(자세한 동작은 아래 사이클 마무리 섹션에서).
7명 에이전트 (기본 6 + 옵션 1)
| 에이전트 | 역할 | 단계 |
|---|---|---|
rvs-redmine-pilot | 흐름 조율, 사용자 보고 | 항상 |
rvs-architect | 영향 범위 분석(host/viewer/admin/공통) | Phase 2 |
rvs-executor | 구현 (카피라이트/타입/Playwright) | Phase 3 |
rvs-code-reviewer | 12개 룰 + 경계 + 프로토콜 정합성 | Phase 4 |
react-reviewer | Vercel React 베스트 프랙티스 | Phase 4 (병렬) |
rvs-qa-validator | 4종 게이트 + Playwright + Acceptance | Phase 5 |
rvs-docs-writer | 문서 변경 동반 시만 합류 | Phase 2~5 |
핵심은 두 리뷰어의 병렬 호출입니다. rvs-code-reviewer(컨벤션·경계·프로토콜)와 react-reviewer(React 성능·렌더링)가 공유 iteration 카운터 를 갖고 동시에 호출됩니다. 합산 3회 핑퐁을 넘기면 ESCALATE. 빌더(생성자)와 검증자를 분리하고, 검증자끼리는 병렬로 묶는 형태로 우리 식의 사이클을 짜본 거예요.
컨텍스트 파일: .agents/rules/
12개 룰을 두었습니다. 한 룰은 60줄 이내라는 원칙을 세웠어요. 긴 설명서가 아니라 짧은 지도 가 되어야 매 세션이 그걸 다 읽고 시작할 수 있거든요. 각 룰은 frontmatter에 paths: 필드로 적용 범위를 명시합니다.
---
description: any/unknown 무근거 사용 금지, 한국어 주석, 단일 책임
paths: ["**/*.ts", "**/*.tsx"]
---우리 환경에서 가장 강한 룰이 no-fallback-workaround.md입니다. 우리가 다루는 제품이 실시간 원격지원이라 이슈가 발생하는 지점이 비동기 타이밍 인 경우가 많은데, AI 에이전트는 그런 이슈를 만나면 setTimeout이나 임시 플래그 같은 우회 코드 로 증상만 가리려는 경향이 있거든요. 폴백을 한 번 짜기 시작하면 그게 누적되면서 진짜 결함이 우회 경로에 숨어버립니다. 그래서 폴백 자체를 차단하는 룰을 따로 두었어요.
일부 발췌를 해봤습니다.
---
description: fallback / workaround / 임시 우회 코드 금지 — 발견 시 reject하고 근본 원인 해결로 안내. 폴백 떡칠 방지.
paths: ["**/*.ts", "**/*.tsx", "**/*.mts", "**/*.cts", "**/*.mjs", "**/*.cjs", "**/*.js", "**/*.jsx"]
---
# fallback / workaround 코드 금지
## 원칙
**근본 원인을 해결한다.** 증상을 가리는 임시 우회 코드는 리뷰 단계에서 reject 한다. 폴백을 누적하면 디버깅이 불가능해지고 실제 결함이 본 함수가 아니라 우회 경로에 숨는다.
## 차단 대상 (REVIEW 시 FIX 또는 ESCALATE)
1. **빈 catch / 침묵 swallow** — `try { ... } catch {}` 또는 `catch (e) { /* ignore */ }`
2. **setTimeout / setInterval 기반 race 회피** — `setTimeout(() => doX(), 100)`으로 타이밍 race를 우회
3. **`fallback` / `tempFix` / `workaround` / `hack` 명명** — 함수·변수·파일명이 의도를 자백
4. **타입 캐스팅으로 우회** — `as any`, `as unknown as T`, non-null assertion(`!`)
5. **DOM 강제 조작 우회** — React state 흐름이 안 맞아서 `document.querySelector`로 직접 DOM 변경
## 잘못된 변명 패턴 (자기 검열)
다음 표현이 머릿속에 떠오르면 fallback을 짜고 있다는 신호 — 멈추고 근본 원인을 본다.
- "일단 ~로 해두고 나중에"
- "race 같으니 setTimeout으로"
- "어차피 거의 안 일어나는 케이스"핵심은 차단 대상과 변명 패턴을 같은 룰 안에 함께 두는 것입니다. "이런 코드 쓰지 마" 만으로는 부족하고, "이런 변명이 떠오르면 멈춰라" 까지 가야 자기검열 단계에서 폴백이 걸러져요. PreToolUse:Edit|Write 훅이 .ts/.tsx 파일을 수정하기 직전에 이 룰을 컨텍스트로 흘려주기 때문에, 코드를 쓰는 시점에 이 변명 목록이 바로 옆에 펼쳐져 있는 셈입니다.
자동 강제 시스템인 훅 3종
여기가 "프롬프트는 부탁, 하네스는 강제" 라는 원칙의 실제 구현입니다.
- SessionStart: 세션 시작 시 브랜치, 미커밋 변경, 활성 일감을 자동 표시합니다.
- UserPromptSubmit: 입력에서
#266841같은 일감 번호 패턴을 감지해rvs-redmine-flow진입을 권장합니다. - PreToolUse:Edit|Write: 수정 직전 파일 경로에 매칭되는 룰을 컨텍스트에 자동 주입합니다. 이 매칭을 담당하는 건 70줄짜리 작은 파이썬 스크립트(
rule-matcher.py)예요. 외부 라이브러리 없이 파이썬 기본 기능만으로 짰습니다.
세 번째 훅이 가장 강력합니다. 에이전트가 .ts/.tsx 파일을 수정하려고 하면 coding-conventions, copyright-header, pre-implementation-analysis, no-fallback-workaround 같은 매칭 룰이 코드를 쓰기 전 컨텍스트로 들어옵니다. 룰을 지키지 않은 코드는 그 다음 단계인 Phase 4 REVIEW에서 자동으로 reject돼요.
게이트는 lefthook과 일관
현재 프로젝트는 git hooks 기능을 husky 대신 lefthook (새 창에서 열림)으로 관리하고 있어요. pre-push 시점에 4종 게이트가 차례로 돌아갑니다.
pnpm type:check → pnpm lint → pnpm format:check → pnpm build
rvs-qa-validator가 사이클 안에서 돌리는 게이트도 명령·순서·옵션이 동일 합니다. 자체 게이트를 발명하지 않고 lefthook과 같은 방식으로 일관되게 맞췄어요. 우리 도구가 이미 잘 잡아내는 것을 또 만들면 그게 drift의 원인이 되거든요.
한 가지 더 짚자면, 이 게이트들은 통과한 결과를 굳이 컨텍스트로 흘리지 않아요. 성공은 조용히, 실패만 시끄럽게. 이게 원칙입니다. PASS 로그 4,000줄을 그대로 보여주면 에이전트가 그걸 읽다가 정작 다음 작업을 잃거든요. 통과는 한 줄 "PASS" 로만, 실패만 상세하게 흘려보냅니다. 이 비대칭이 컨텍스트 예산을 아껴줘요.
사이클 마무리로 청소와 학습 누적
VALIDATE가 PASS를 반환한 직후, CLOSE 직전. 자동으로 한 단계가 더 끼어듭니다. 이번 사이클의 잔재를 정리하고, 발견된 새로운 실수 패턴 을 다음 사이클의 룰로 누적시키는 단계예요. 다음을 자동 수행합니다.
- 변경 파일에 한해
eslint --fix로 미사용 import 제거 - 임시 주석 패턴(
// TODO 나중에,// 임시,console.log잔재)을 보고만 합니다(자동 삭제 X. 의도적 마킹일 수 있음) .workspace/issue-{N}/빈 placeholder 정리- 핑퐁이 2회 이상이었으면
.workspace/learnings.md에 한 줄 append - 사이클 통계(
cost.json)를 schema에 맞게 작성 +_index.jsonl에 한 줄 append
마지막 두 개가 Compound 학습 입력입니다. 여기서 말하는 Compound 는 반복 학습이 누적된다는 의미에 가까워요. 복리(複利) 처럼 시간이 지날수록 정교해지는 결이거든요. 같은 실수를 두 번 하지 않는 시스템은 사이클이 끝나도 학습이 휘발되지 않고 다음 사이클의 룰 로 자라납니다.
한 장으로 보는 사이클
여기까지의 흐름을 한 장으로 정리하면 이렇게 됩니다.
외부 도구 통합
우리 회사는 작업 관리에 Redmine (새 창에서 열림), 코드 관리에 GitLab (새 창에서 열림)을 씁니다. 두 도구 다 MCP로 다룰 수 있어서 하네스에서 직접 호출하는 방식으로 통합했어요.
Redmine을 SoT로 둔 이유
Redmine은 PM / 기획자 / QA / 개발자 한 일감을 거치는 모든 직군의 흔적이 시간순으로 쌓이는 히스토리 창고 입니다. 작업 명세도, 검증 기준 합의도, 진행 상태도 한 일감 안에 누적돼요. 그래서 작업 추적용 자체 매니페스트를 따로 두지 않기로 했습니다. 대신 외부 Redmine을 단일 진실 공급원(SoT) 으로 쓰기로 결정했어요.
- 일감 본문 = 작업 명세
- Acceptance 항목 = 검증 기준
- 일감 상태 전이 = 사이클의 시간 축
mcp__redmine MCP 서버를 통해 에이전트가 직접 일감을 읽고, 상태를 바꾸고, time_entry를 등록하고, Textile notes를 답니다. 우리가 만든 첫 슬래시 커맨드는 /redmine:show 266841. 일감 본문/Acceptance/상태를 그대로 보여줍니다. 상태 변경 없음, 디버깅용이에요.
Plan에 외부 시스템 흔적 남기기
사이클이 끝나면 /redmine:close-issue가 자동으로 다음을 합니다.
- 관련 git 커밋을
git log --grep="#266841"로 모읍니다 time_entry를 등록합니다 (Redmine 워크플로우는 status 전환 전time_entry를 게이트로 요구합니다)- Textile notes를 작성합니다. Acceptance + 작업 내역 + 커밋 해시 + 영향 범위 + 검증 결과
h3. 검증 기준 (Acceptance)
* {Acceptance 항목 1}
* {Acceptance 항목 2}
h3. 작업내역
* {커밋 1 subject 한 줄 요약}
커밋: @{첫 해시}@ ... @{마지막 해시}@
h3. 영향 범위
* 앱: {host | viewer | admin | 공통}
* 슬라이스: {features/chat 등}
h3. 검증
* type:check / lint / format:check / build : PASS
* Playwright ({앱}): PASS / 해당없음
원래 사람이 일감 끝낼 때 손으로 쓰던 정보입니다. 에이전트가 사이클 진행 중에 모든 데이터를 갖고 있으므로 마지막에 형식만 맞춰주면 돼요.
사실 AI 에이전트를 도입하기 전에 가장 빠르게 자동화하고 싶었던 게 이런 반복 작업 이었어요. Redmine 일감 갱신, time_entry 등록, GitLab MR 작성, 커밋 해시와 작업 내역 정리. 사람의 시간을 가져가지만 창의적 판단이 거의 없는 일들이거든요. 이 구조 덕분에 작업자는 반복에서 빠져나와 진짜 고민이 필요한 작업 에 리소스를 쓸 수 있게 됐습니다.
GitLab은 push와 MR 단계에서
/git:create-mr 커맨드가 GitLab MCP를 통해 push + MR 생성을 한 번에 합니다. 단 사용자가 명시적으로 호출해야만 작동합니다. 자의적 push 금지. 본 하네스가 자체적으로 커밋·push·close를 하지 않는다는 원칙은 일관됩니다.
안전 장치로 사용자 확인이 필수인 동작
다음은 반드시 사용자 확인을 거칩니다.
- 자의적 커밋 (사용자가 "커밋해" 또는
/git:commit호출 시에만) - 자의적 라이브러리 설치
- 운영 영향 작업 (배포·권한·외부 연동·DB)
- Redmine close (VALIDATE PASS 후 사용자 승인 게이트)
Playwright MCP 활용
복잡한 도메인을 AI가 어떻게 이해하게 할까
맨 처음 든 생각은 "실제 WebRTC가 붙는 테스트를 자동화로 할 수 있을까?" 였어요. 우리 제품이 영상·음성 시그널링을 핵심으로 하다 보니, 그게 안 되면 e2e의 절반이 무용이 되거든요. 다행히 Chromium 기반 자동화 도구에 다음 두 옵션이 있어서 더미 미디어 주입이 가능했습니다.
--use-fake-ui-for-media-stream
--use-fake-device-for-media-stream
이 설정만 켜주면 카메라·마이크 권한 프롬프트 없이 가짜 비디오·오디오 스트림이 흘러요. 실제 WebRTC 연결까지 자동으로 검증할 수 있는 걸 확인했습니다.
도구 가능성이 확인되니 다음 질문이 자연스럽게 나왔습니다. "그럼 복잡한 도메인이 얽힌 우리 제품을 AI가 어떻게 이해하게 만들까?" 우리 제품은 클라이언트가 API 서버 외에도 4~5개의 독립 서버에 동시에 커넥션을 유지하는 구조예요. 한 번의 상담 연결 만 봐도 여러 서버의 상태가 동기화되어야 하고, 그 흐름이 비동기로 얽혀 있어서 Playwright만으로는 핵심 시나리오를 검증할 수 없습니다. 버튼이 눌렸는데 왜 다음 화면이 안 뜨는지 도 메시지 시퀀스를 모르면 알 수 없거든요.
기존에는 연결 로직, 도메인 지식, 그리고 약속된 프로토콜과 메시지 까지 정리한 사내 문서가 있었습니다. 그런데 AI 에이전트가 그 문서를 읽어도 전체 그림 을 잡지 못했어요. 정적인 텍스트라 흐름이 어떤 순서로 일어나는지·어느 단계에서 어떤 서버가 관여하는지가 한눈에 안 들어오거든요.
직접 조작시키고 시퀀스 다이어그램으로 풀어내보자
아이디어는 단순했습니다. AI에게 Playwright MCP로 실제 상담 연결을 처음부터 끝까지 한 번 수행해보라고 시켰어요. 기존 사내 문서를 읽혀준 다음, 제품을 직접 만져보면서 어떤 화면 전환이 일어나는지·각 시점에 어떤 통신이 발생하는지 자기 눈으로 보게 한 거죠.
그 결과를 바탕으로 PlantUML 시퀀스 다이어그램을 그리게 했습니다. 사내에서 자주 쓰는 다이어그램 포맷이라 다른 직군과의 공유에도 자연스러웠고, .puml 원본을 코드와 함께 git에 커밋하면 도메인 SoT 로 누적된다는 장점이 있었어요.
다이어그램이 그려지는 순간 보이지 않던 흐름 이 한 장에 응축됩니다. 이게 다음 단계인 스킬화 의 기반이 됐어요.
remotevs-v2 도메인 스킬
정리된 시퀀스 다이어그램 + 학습 과정의 메모 + 기존 사내 문서를 한데 묶어 remotevs-v2 라는 도메인 스킬을 만들었습니다. 이 스킬이 다루는 범위는 다음과 같아요.
- 도메인 단어: 사내에서만 통용되는 용어 정의
- 연결 플로우: 사용자 액션이 어떻게 서버들을 거쳐 흐르는지
- 비즈니스 로직: 각 단계에서 무엇이 검증되고 어떤 분기가 나뉘는지
- 서버 상태 매트릭스: API 서버 외 4~5개 독립 서버 각각이 어떤 상태를 갖고 어떻게 동기화되는지
목표는 실제 개발자가 새로 합류했을 때 받는 인수인계 와 같은 수준의 정보였어요. 신입 개발자가 첫 주에 알아야 할 것을 그대로 AI 에이전트도 알 수 있도록.
SKILL.md 본문은 짧게 두고, 도메인 영역별 reference 파일을 references/ 아래로 분리했습니다. 앞서 학습하면서 그렸던 시퀀스 다이어그램들을 토대로 스킬을 만든 거예요.
사이클에서의 활용
스킬을 만든 다음엔 사이클 안에서 자연스럽게 호출됩니다. architect는 일감 본문에 도메인 핵심 키워드가 보이면 영향 범위 메타데이터를 채워서 executor에게 넘겨요. executor는 그 신호를 받아 해당 reference 파일을 먼저 읽고, 그 다음에 코드를 쓰고, Playwright 시나리오를 같이 씁니다. QA validator는 변경된 메시지 shape이 도메인 카탈로그와 양방향 일치하는지 비교하고요. 단, 이 비교가 현재는 LLM 추론에 의존 한다는 점을 스킬 자체에 자인해 두었어요. 정형 비교 자동화는 마지막 섹션에서 다시 다룹니다.
이 구조가 자리 잡으니 사용자 입장에서의 변화도 분명했어요. remotevs-v2 라는 인수인계 수준의 도메인 스킬 덕분에, 별다른 지시 없이 "#266841 진행해줘" 같은 짧은 프롬프트만 던져도 레드마인에 명시된 내용 만으로 웬만한 작업이 끝까지 진행됩니다. 도메인 컨텍스트는 스킬이, 작업 명세와 검증 기준은 일감이 받쳐주니까요.
새 시나리오를 만나면 스킬을 확장 합니다. /redmine:show 266841로 일감을 보고 → MCP로 제품을 직접 조작해보고 → Playwright 시나리오로 옮기고 → 도메인 흐름이 새로 발견되면 references/에 한 줄 추가. 학습이 반복되면 룰로, 더 반복되면 스킬로 승격되는 흐름이에요.
Agent-Agnostic 폴더 구조
도구가 늘어나면 SoT가 갈라진다
이 구조는 처음엔 Claude Code 하나에만 맞춰져 있었어요. 그런데 어느 시점에 같은 팀원이 Codex도 함께 쓰고 싶다 는 요청을 주셨고, 저도 Codex를 가끔 쓰는 입장이라 이 요구가 자연스러웠습니다. 그래서 Claude Code에 결합되어 있던 하네스 환경 을 좀 더 Agent-Agnostic 한 구조로 다시 짜야 했어요.
Claude Code에는 CLAUDE.md, Cursor에는 .cursor/rules/*.mdc, Codex에는 ~/.codex/config.toml. 같은 컨벤션을 도구마다 따로 쓰면 한 곳을 바꿀 때 다른 곳이 stale이 됩니다. 이걸 막는 가장 단순한 원칙은 모든 에이전트 설정을 세 카테고리 중 하나로 분류하는 거예요.
모든 설정은 세 카테고리
| 카테고리 | 의미 | 처리 방식 | 예시 |
|---|---|---|---|
| Portable | 모든 에이전트가 같은 포맷 으로 읽음 | .agents/에 정본 + 도구별 위치에 심볼릭 링크 | AGENTS.md, SKILL.md |
| Generated | 같은 데이터 인데 도구마다 포맷이 다름 | .agents/에 정본 + sync 스크립트로 렌더링 | MCP 설정 (JSON / TOML) |
| Agent-specific | 다른 에이전트에 대응되는 개념이 아예 없음 | 도구 전용 위치에 그대로 | .cursor/rules/*.mdc, .claude/settings.json |
한 줄 정리. Portable에는 심볼릭 링크, Generated에는 sync 스크립트, Agent-specific은 그 자리에 그대로 둔다.
우리 레이아웃
rvs-apps/
├── AGENTS.md # 루트 진입 (모든 도구 공통, Portable)
├── CLAUDE.md # @AGENTS.md 포인터
├── .agents/ # 도구 무관 SoT
│ ├── rules/ # 12개 룰
│ ├── skills/ # 11개 스킬
│ ├── memory/ # 인덱스 (포인터만, 본문 복사 X)
│ ├── tasks/ # 휘발성
│ ├── mcp/ # MCP 서버 정본 (Generated 입력)
│ └── scripts/ # 어댑터 동기화 스크립트
├── .claude/ # Claude Code 어댑터
│ ├── agents/ # 7명 에이전트 (Agent-specific)
│ ├── commands/ # 6개 슬래시 커맨드 (Agent-specific)
│ ├── hooks/ # 3종 훅 (Agent-specific)
│ ├── skills/ # → .agents/skills/ 심링크
│ └── settings.json # 훅·권한 (Agent-specific)
├── .codex/ # Codex 어댑터
│ ├── AGENTS.md # → 루트 AGENTS.md 심링크
│ ├── README.md
│ └── skills/ # → .agents/skills/ 심링크
└── .mcp.json # Generated (Claude용 MCP 출력)
세 카테고리가 어떻게 섞여 있는지 정리해봤어요.
- Portable:
AGENTS.md(루트),.agents/skills/*,.agents/rules/*..claude/skills·.codex/skills는 정본 디렉터리로의 디렉터리 심볼릭 링크 일 뿐이에요. - Generated: MCP 설정.
.agents/mcp/servers.json을 정본으로 두고 sync 스크립트가.mcp.json(Claude·JSON),~/.codex/config.toml(Codex·TOML)로 렌더링합니다. - Agent-specific:
.claude/settings.json(훅),.claude/agents/*(서브에이전트 정의),.claude/commands/*(슬래시 커맨드). 모두 그 자리에 그대로 둡니다.
Layer 1: 지침 - AGENTS.md
지침은 Portable 의 가장 단순한 사례입니다. AGENTS.md (새 창에서 열림)는 최근 여러 에이전트 도구들에서 채택되고 있는 사실상의 공통 컨벤션에 가까워요. Codex, Cursor, Copilot, Gemini CLI 같은 주요 도구들에서 프로젝트 지침 파일 로 활용하는 흐름이 확산되고 있습니다. 별도 sync 도구가 필요 없습니다.
CLAUDE.md는 단지 포인터로 둡니다.
@AGENTS.md
이 레포는 재사용 가능한 에이전트 자산을 `.agents/`에 두고,
도구별 어댑터는 `.agents/scripts/`로 동기화합니다.이렇게 두면 프로젝트 지침 이라는 같은 자료가 두 도구에 같은 형태로 도달하고, 변경할 때는 AGENTS.md 한 곳만 손대면 됩니다. 깊은 디렉터리에 별도 컨텍스트가 필요하면 하위 AGENTS.md(예: apps/rvs-host/AGENTS.md)를 두면 되고요. Claude Code와 Codex 모두 계층 구조로 읽어줍니다.
Layer 2: 스킬 - SKILL.md + 심볼릭 링크
스킬도 Portable 입니다. agentskills.io 스펙 (새 창에서 열림)이 정의한 SKILL.md 포맷을 최근 여러 에이전트 도구들이 채택하기 시작했어요. 정본은 .agents/skills/<skill-name>/SKILL.md에 두고, 도구별 디렉터리(.claude/skills/<skill-name>, .codex/skills/<skill-name>)에는 디렉터리 심볼릭 링크 만 둡니다.
---
name: my-skill
description: 이 스킬이 무엇을 하고 언제 써야 하는지.
---
에이전트가 따라할 단계별 지침...심볼릭 링크 정렬은 link-skills.sh라는 60줄짜리 bash 스크립트가 자동으로 합니다. .agents/skills/의 모든 스킬에 대해 .claude/skills·.codex/skills에 심링크를 만들고, 사라진 스킬의 stale 심링크는 정리해요. macOS bash 3.2 호환으로 짰습니다.
Layer 3: MCP - 같은 데이터, 다른 포맷
MCP 설정은 Generated 카테고리입니다. 같은 서버 정의 인데 도구마다 포맷이 달라요.
| 도구 | 파일 | 포맷 차이 |
|---|---|---|
| Claude Code | .mcp.json | JSON. URL 서버는 "type": "http" 필요 |
| Cursor | .cursor/mcp.json | JSON. type 필드 없이 그대로 복사 |
| Codex | ~/.codex/config.toml | TOML. [mcp_servers.<name>] 테이블 형태 |
정본은 .agents/mcp/servers.json 한 곳입니다.
{
"redmine": {
"type": "http",
"url": "https://redmine.internal/mcp"
},
"gitlab": {
"type": "http",
"url": "https://gitlab.internal/mcp"
}
}sync 스크립트가 이걸 도구별 포맷으로 렌더링합니다. 새 MCP 서버를 추가할 때는 .agents/mcp/servers.json만 편집하고 sync를 한 번 돌리면 세 도구가 동시에 인식해요.
⚠️ Codex는 레포-로컬
.codex/config.toml을 자동 로드하지 않아요. sync 스크립트가 머지 블록 으로~/.codex/config.toml에 managed MCP 영역을 끼워넣는 방식으로 우회했습니다.# BEGIN <repo> managed MCP/# END <repo> managed MCP같은 sentinel 주석으로 영역을 표시하면, 다음 sync에서 그 블록만 갱신할 수 있어요.
주의 Cursor MDC 옮기면 안 되는 이유
여기서 한 가지 짚어둘 게 있어요. Cursor MDC는 옮기면 안 된다 는 것. .cursor/rules/*.mdc는 Cursor 전용 frontmatter 형식 + glob scoping(globs: "**/*.py") + 조건부 로딩(alwaysApply) 같은 다른 에이전트에 대응되는 개념이 없는 기능을 사용해요. Portable로 만들려고 시도하면 그 기능들이 깨집니다. Agent-specific 그대로 두는 게 맞아요.
대신 같은 지식 을 여러 도구에서 공유하고 싶으면 MDC를 옮기지 말고 그 내용을 AGENTS.md에 적어주세요. AGENTS.md는 어차피 모든 에이전트가 보는 자료라 두 군데에 같은 내용을 따로 쓰는 구조 자체가 발생하지 않습니다. 새 추상화 레이어를 만들지 말고 기존의 Portable 자리를 활용하는 게 정답이에요.
우리는 Cursor를 사용하지 않기 때문에 이 부분에 대해 문제는 아직 없었습니다. 다만 도입할 때를 대비해서 .agents/AGENTS.md에 Cursor가 들어오면 link-skills.sh의 TARGETS에 .cursor/skills를 추가한다 고 명시해뒀어요.
Codex의 차이점
Claude Code는 SessionStart/UserPromptSubmit/PreToolUse 같은 이벤트 훅을 네이티브로 지원합니다. 우리가 사용한 Codex CLI 환경에는 Claude Code 수준의 네이티브 이벤트 훅이 없어서, 동등한 흐름은 수동 스크립트로 보강 해야 했어요.
.agents/scripts/
├── link-skills.sh # 심링크 자동 정렬
├── codex-session-snapshot.sh # SessionStart 동등
├── codex-rule-hint.sh # PreToolUse 동등 (수정 전 룰 확인)
└── sync-codex-mcp.sh # ~/.codex/config.toml에 MCP 서버 등록
같은 rule-matcher.py를 Claude 훅과 Codex 스크립트가 공용합니다. 룰은 .agents/rules/에서 한 번 정의하면 두 도구가 같은 결과를 내요.
결과
- 편집 지점이 1곳: 룰 한 줄을 바꾸면 두 도구가 즉시 그 룰을 봅니다.
- stale 심링크 0건: link-skills.sh가 매번 정리합니다.
- 도구 추가가 단순: 새 어댑터 디렉터리 + TARGETS 한 줄 추가면 끝이에요.
남은 고민
이 글을 다 쓰고 나서도 내내 머릿속에 맴도는 의구심이 있어요. 환경을 이만큼 구축했고 사이클도 돌려봤는데, 과연 이게 언제나 의도대로 잘 동작할까?
여전히 남는 의구심들
이게 정말 효율적인가?
룰 12개, 스킬 11개, 훅 3종, 에이전트 7명, 슬래시 커맨드 6개. 한 일감을 처리할 때마다 이 모든 설정이 컨텍스트로 흘러들어갑니다. 너무 많은 설정과 .md 파일 때문에 하나의 작업에 오히려 시간과 비용이 더 드는 건 아닐까? 사이클 한 번 돌리는데 토큰을 얼마나 쓰는지, 그게 사람이 직접 작업했을 때보다 정말 더 빠르고 더 정확한지. 정량적으로 비교한 데이터가 아직 없어요.
의도대로 동작할까?
룰을 적어두면 에이전트가 그걸 읽고 따른다 고 가정하지만, 실제로는 컨텍스트 양이 늘어날수록 주의가 분산 될 수 있습니다. PreToolUse 훅이 룰을 흘려준다고 해서 그게 우선순위 로 처리된다는 보장은 없고, 사이클 깊은 곳에서 어떻게 행동할지는 매번 봐야 알아요. 과연 맞게 설계된 하네스 구조일까? 라는 질문에 자신 있게 그렇다 고 답하기 어렵습니다.
모델이 좋아지면 이 설계가 곧 무용이 되는 건 아닐까?
사실 이 고민은 구성을 거의 다 끝내고 나서야 짙어졌어요. AI 기술은 매주 새로 나오고, 몇 달 전의 베스트 프랙티스가 이미 낡아 있거든요. 이렇게 거하게 룰·스킬·훅을 깔아두는 동안에도, 시간이 조금만 더 지나 모델이 더 똑똑해지면 아무 설정도 없는 퓨어한 상태 에서 돌리는 게 오히려 더 좋은 결과를 낼지도 모릅니다. 지금 자동 강제 시스템 으로 차단하는 패턴들 중 일부는 다음 세대 모델이 알아서 회피할 거고요. 하네스의 단순화 라는 큰 흐름을 머리로는 이해하지만, 동시에 내가 지금 만든 설정 중 어떤 게 몇 개월 후에도 살아남을지 모른다는 불안이 같이 와요.
Claude Code와 Codex가 같은 퀄리티를 보장할까?
Agent-Agnostic 구조로 두 도구를 모두 지원하지만, 같은 룰을 같은 방식으로 흘려준다고 해서 결과물의 퀄리티 까지 같다는 보장은 없습니다. 두 도구의 모델·컨텍스트 처리·도구 사용 패턴이 다 다르거든요. 한쪽에서 잘 동작하는 사이클이 다른 쪽에서는 망가질 수 있고, 그걸 미리 알 방법은 둘 다에서 직접 돌려보는 것 외에는 없어요.
미완성을 인정하고 계속 다듬는다
이렇게 글로 정리해놓고도 이 구성은 미완성 이라는 게 솔직한 평가입니다. 다만 초기 버전이지만 의도대로 잘 동작하고 있고, 작업 효율이 눈에 띄게 올라간 게 느껴진다 는 점은 분명해요. 아직 이 하네스 구조로 많은 일감을 돌려보지도 않았고, 여기 적힌 결정들 중 어떤 건 며칠 만에 뒤집힐 수도 있지만요.
그래도 분명한 게 있습니다. 실제로 업무하면서 이 환경이 점점 좋아지길 원한다 는 것. 사이클을 돌릴 때마다 발견되는 작은 어긋남, 어떤 룰이 너무 길어서 무시되는 패턴, 어떤 훅이 노이즈를 만드는 순간. 그런 신호가 누적되면 그게 다음 사이클의 룰이 되고, 결국 다음의 나에게 보낼 인수인계가 됩니다.
완벽한 하네스를 한 번에 만드는 건 불가능하다고 생각합니다. 실수를 두 번 하지 않게 만든다 는 원칙은 그 자체로 시간이 필요한 약속이거든요. 그래서 이 구성은 한동안 꾸준히 다듬는 대상 으로 두려고 합니다. 몇 개월 후 같은 제목의 글을 다시 쓴다면, 지금 적힌 의구심들 중 어떤 건 해소됐고 어떤 건 새로운 형태로 남아있을 거예요. 그 변화 자체가 이 시대를 살아가는 방식이라고 생각합니다.
마무리
길고 디테일한 회고가 됐어요. 우리 환경 고유의 결정도 많이 섞여 있고, 몇 개월 후에는 낡아 있을 디테일도 분명히 있을 거예요.
그래도 하네스를 둘 자리를 만들고, 한 줄씩 룰을 쌓고, 사이클을 돌리고, 다시 다듬는다 는 흐름은 한동안 안 바뀔 것 같아요. 도구가 바뀌고 모델이 바뀌어도 같은 실수를 두 번 하지 않게 만든다 는 원칙은 그 자리에 그대로일 테니까요.
이 글이 비슷한 환경에서 비슷한 고민을 하고 있는 누군가에게 작은 단서가 됐으면 좋겠습니다. 몇 개월 후에 같은 제목의 글을 다시 쓰게 되면, 그땐 지금 적힌 의구심들 중 어떤 게 해소됐는지 솔직하게 적어두려고요.
끝까지 읽어주셔서 감사합니다.
이런 글도 읽어보세요
요즘 에이전트 하네스에 대한 고민
프롬프트 엔지니어링에서 컨텍스트 엔지니어링, 그리고 하네스 엔지니어링으로 빠르게 옮겨가는 AI 코딩 패러다임의 변화. 몇 달 만에 베스트 프랙티스가 낡아가는 시대에 무엇을 쫓고 무엇을 끝까지 남길지에 대한 개발자의 깊은 고민.
Claude Code Agent Teams로 AI 뉴스봇 만들기
Claude Code의 새로운 Agent Teams 기능을 활용해 AI 뉴스 요약봇을 병렬로 개발하는 과정을 단계별로 안내합니다. Agent Teams의 개념부터 셋업, 실전 앱 개발, 트러블슈팅까지 실무 관점에서 정리했습니다.