아키텍처 심화
§1에서 L0→L1→L2가 "무엇이 어디서 어디로 흐르는가"를 다뤘다면, 이 절은 그 흐름이 실제로 어떻게 구현되는지를 파고든다 — 파일이 그대로 복사되지 않고 재생성되는 이유, 상속 프론트매터가 해석되는 순서, 보일러플레이트·티어 전략이 실전에서 어떻게 적용되는지의 구체적 예시, 그리고 플랫폼 디렉터리(.claude/.gemini/.agents/.codex)가 구조적으로 어긋나지 않게 유지되는 방법까지 다룬다.
- L2 파일이 L1을 그대로 복사하지 않고 "재생성"되는 이유(Layout Reconstruction)
extends프론트매터가 실제로 해석되는 순서- LOCKED/MERGE/PRESERVE 분류를 실제 파일에 적용한 예시
- 3-티어 전략 — High/Medium/Low 정의, frontmatter 예시, Tier Adjustment Rules, Tier Ceiling Rule
.claude/·.gemini/·.agents/·.codex/플랫폼 패리티가 강제되는 방법- 실행 계획 템플릿 — Design Gate(Row 0), 예외 카테고리(E1~E5), 실전 예시
Layout Reconstruction — L2가 L1을 그대로 복사하지 않는 이유
§1에서 짧게 언급한 agents/pm.md 사례를 좀 더 파고들어 보자. L0의 agents/pm.md는 워크스페이스 전체 거버넌스를 담아 300줄이 넘는다. 여기에는 Auto-Mode 동작, 라이프사이클 상태 필드처럼 워크스페이스 루트에서만 의미 있는 섹션이 섞여 있다. L1의 templates/common/agents/pm.md가 이걸 그대로 복사한다면, 베리언트(variant)를 새로 만들 때마다 프로젝트와 무관한 워크스페이스 전용 내용까지 함께 딸려온다.
그래서 L1은 복사 대신 필터링된 재구성을 한다 — L0 원본에서 variant 스캐폴딩에 실제로 필요한 섹션만 골라내고, 워크스페이스 전용 섹션은 제거한 뒤 extends 참조로 나머지를 연결한다. L2로 내려갈 때도 같은 원리가 반복된다. variant 고유의 오버라이드(예: co-retail이라면 마케팅 도메인 특화 PM 규칙)를 L1 위에 얹어 처음부터 다시 생성하지, L1 파일을 diff-patch 방식으로 수정하지 않는다. 그 결과 L0의 pm.md가 300줄이 넘어도 L2의 pm.md는 50~100줄 남짓으로 유지된다 — "복사하고 남는 걸 지우는" 방식이 아니라 "필요한 것만 골라 새로 짓는" 방식이기 때문이다.
이 설계가 중요한 이유는 실패 모드 때문이다. 만약 L1→L2가 단순 복사였다면, L0에서 워크스페이스 전용 섹션 하나를 추가할 때마다 모든 L3 프로젝트의 pm.md가 불필요하게 부풀어 오른다. Layout Reconstruction은 "L0가 아는 것"과 "L2가 알아야 하는 것" 사이에 항상 필터를 둠으로써 이 문제를 원천 차단한다.
extends 프론트매터 — 상속이 해석되는 순서
extends: ../../../agents/pm.md 같은 프론트매터 필드는 "이 파일은 저 파일을 참조한다"는 선언이지, 파일을 합치는 매크로가 아니다. 해석 순서는 다음과 같다.
- 도구가 파일을 읽을 때(예: Claude Code가
agents/pm.md를 로드할 때), frontmatter의extends경로를 먼저 확인한다. - 참조된 원본(L0 또는 L1)을 읽어 본문을 상속하되, 현재 파일에 명시적으로 다시 쓰인 섹션이 있으면 그 섹션만 오버라이드한다.
- 원본에 없는, 현재 파일에만 있는 섹션(예: variant 고유 규칙)은 그대로 추가된다.
즉 extends는 "상속 + 선택적 오버라이드 + 추가"의 3단계로 해석되는 것이지, 두 파일을 텍스트로 이어 붙이는 것이 아니다. 이 구조 때문에 L1의 templates/common/agents/pm.md는 실제 내용을 거의 담지 않고도(대부분 extends로 L0를 가리키기만 함) 유효한 파일로 동작하고, L0에서 공통 규칙이 바뀌면 extends를 쓰는 모든 하위 파일이 다음 조회 시점에 자동으로 최신 내용을 반영한다 — 단, 이건 "읽을 때 해석"이지 §1에서 다룬 "파일 동기화(SYNC_IF_NEWER)"와는 다른 메커니즘이다. extends는 도구가 파일을 여는 순간의 실시간 참조이고, SYNC_IF_NEWER는 upgrade-project.ts를 실행해야 갱신되는 정적 파일 복사다. 두 메커니즘이 같은 "상속처럼 보이는 동작"을 하지만 갱신 시점이 다르다는 걸 혼동하지 않아야 한다.
보일러플레이트 전략 실전 예시
§1에서 다룬 LOCKED/MERGE/PRESERVE 분류가 실제 파일에 어떻게 적용되는지 하나씩 예로 확인해보자.
- LOCKED 예 —
.githooks/pre-commit: 이 파일은 시크릿 스캔, 커밋 메시지 검증 같은 보안 게이트를 실행한다. 프로젝트마다 이 검증을 다르게 하면 조직 전체의 보안 기준이 프로젝트별로 달라지는 것이므로,upgrade-project.ts는 이 파일을 로컬 수정 여부와 무관하게 항상 L1 최신본으로 덮어쓴다. - MERGE 예 —
CLAUDE.md: 이 파일에는<!-- WORKSPACE-MANAGED -->...<!-- /WORKSPACE-MANAGED -->로 감싼 구간(예: PM 게이트웨이 규칙)과, 그 바깥의 프로젝트 고유 규칙(예: "이 프로젝트는 항상 한국어로 커밋 메시지를 쓴다")이 함께 들어 있다. 업그레이드 시 마커 안쪽만 L1 최신본으로 교체되고, 마커 바깥의 한국어 커밋 규칙은 그대로 남는다. - PRESERVE 예 —
docs/context.md: 이 파일은 프로젝트가 무엇인지 설명하는 문서라 L1이 존재 확인만 하고 내용에는 절대 관여하지 않는다.
세 예시를 나란히 보면 분류 기준이 명확해진다 — 바뀌면 보안·거버넌스 사고로 이어지는가(LOCKED), 조직 규칙과 프로젝트 맥락이 한 파일 안에 공존해야 하는가(MERGE), 프로젝트의 정체성 그 자체인가(PRESERVE). 새 파일을 이 저장소의 boilerplate에 추가할 때도 이 세 질문을 순서대로 던져보면 어디에 속하는지 대부분 바로 판단된다.
3-티어 전략 심화 — 비용 최적화를 실행 시점에 적용하기
5장에서 소개한 tier 배정은 agents/*.md frontmatter에 정적으로 정의된다. 각 에이전트 파일의 tier 필드는 플랫폼별 모델 별칭으로 번역되어 실행 시점에 적용된다.
frontmatter 예시
--- name: pm # pm, code-writer, automation-engineer ... tier: # 플랫폼별 모델 별칭(실행 시 번역됨) claude: high # high = claude-opus-5-0 antigravity: high # high = gemini-3.1-pro codex: high # high = gpt-5.6-sol ---
이 번역이 model: inherit 뒤에서 자동으로 일어나기 때문에, agents/*.md를 작성하는 사람은 구체적인 모델 이름을 전혀 몰라도 되고 "이 역할이 얼마나 무거운 판단을 하는가"만 신경 쓰면 된다.
티어 정의와 모델 매핑
| 티어 | 목적 | Claude 계열 예시 | Antigravity 계열 예시 | Codex 계열 예시 |
|---|---|---|---|---|
| High | 복잡한 추론, 아키텍처 설계, 계획 수립 | claude-opus-5-0 | gemini-3.1-pro | gpt-5.6-sol |
| Medium | 코드 리뷰, 테스트, PR 리뷰, 품질 게이트 | claude-sonnet-5-0 | gemini-3.7-flash | gpt-5.6-terra |
| Low | 빠르고 반복적인 코딩, 스크립트 유지보수 | claude-haiku-4-5 | gemini-3.7-flash | gpt-5.6-luna |
이 정적 배정이 기준선(baseline)이 되지만, 실제 실행 시점에서는 PM이 Tier Adjustment Rules을 적용해 동적으로 비용을 최적화한다. 기준선이 high인 에이전트(architect, pm)는 복잡한 설계 판단을 내려야 하므로 가장 강한 모델이 필요하다. 반면 automation-engineer는 ADR-0036에 따른 정형화된 스크립트를 작성하는 역할이므로 low 티어로 충분하다 — "무거운 판단"이 필요한가만 보면 되기 때문이다.
Tier Adjustment Rules — 실행 시 비용 최적화
PM이 실행 계획을 세울 때 기준선에서 벗어나는 티어 조정이 가능하다. 규칙은 간단하다.
- 다운그레이드 허용 — PM은 단순한 작업의 경우 에이전트의 티어를 기준선보다 낮출 수 있다. 예를 들어 기준선이 medium인 docs-writer가 "README.md 오타 하나 수정" 작업을 받을 때 low로 낮춰 비용을 절감할 수 있다.
- 업그레이드 금지 — 반대로 기준선보다 높은 티어로 올리는 것은 항상 금지다. low인 automation-engineer를 high로 올려 복잡한 아키텍처를 설계하게 하는 것은 거버넌스 위반이다.
- 실패 시 복원 — 다운그레이드한 작업이 실패하면, PM은 반드시 해당 에이전트의 기준선 티어를 복원하고 재시도해야 한다.
이 규칙의 핵심은 "에이전트 역할의 복잡도 상한은 고정되지만, 하한은 상황에 맞춰 조정할 수 있다"는 원칙이다. PM은 비용 절감을 위해 low로 내릴 수 있지만, 그 작업이 low의 역량을 넘어서면 즉시 기준선으로 되돌아간다. 비용 영향에 대해서는 4장 §2-A P-2 실습에서 체감으로 확인할 수 있다.
Tier Ceiling Rule
AGENTS.md의 Phase Determination 테이블(§3.5)은 각 산출물 유형에 허용된 티어 상한을 명시한다. 예를 들어 "스크립트 구현(승인된 계획 존재)"은 Phase 4이며 담당 에이전트는 automation-engineer(low)다 — 여기에 high를 배정하면 Tier Ceiling 위반이 된다. 반면 "새 파일 설계, 스키마 정의"는 Phase 1-2이며 architect(high)가 담당한다.
플랫폼 패리티 — .claude/, .gemini/, .agents/, .codex/를 구조적으로 동일하게 유지하는 법
이 저장소는 네 플랫폼을 동시에 지원한다 — Claude Code(.claude/), Gemini CLI(.gemini/), Antigravity(.agents/), Codex(.codex/). 플랫폼 디렉터리들이 시간이 지나면서 어긋나면(예: .claude/agents/reviewer.md는 있는데 .gemini/agents/reviewer.md는 빠져 있으면) 한쪽 도구 사용자만 특정 기능을 못 쓰게 되는 문제가 생긴다. 스킬 계층은 skills/가 단일 출처(SSOT)이며, sync-skills.ts가 이를 네 플랫폼 디렉터리 모두에 배포한다. 구조적 어긋남을 막는 장치가 validate-templates.ts의 플랫폼 패리티 검사다.
다만 검사 스코프에는 미묘한 차이가 있다. validate-templates.ts는 커맨드 등 템플릿 구조를 .claude/와 .gemini/ 두 디렉터리에 대해서만 대조하고(Antigravity용 .agents/와 Codex용 .codex/ 트리는 별도 동기화 경로로 운영된다), 스킬 내용 자체는 SSOT인 skills/를 validate-skills.ts가 검사한다. 즉 "네 플랫폼이 같은 기능을 제공하는가"는 배포 스크립트와 SSOT 검증의 조합으로 보장된다. 11장에서 다루는 승격(promote) 파이프라인도 패리티 검사를 승격 전 필수 게이트로 포함하고 있어서, 한 플랫폼용으로만 만들고 다른 플랫폼을 깜빡한 상태로는 애초에 정식 베리언트(variant)가 될 수 없다. 네 플랫폼이 서로 다른 실행 방식을 쓰더라도, 제공하는 기능 목록 자체는 항상 같아야 한다는 원칙을 구조 검사로 강제하는 것이다.
graft — 다중 플랫폼 플릿을 위한 저장소 맥락 그래프
graft(@nanonets/graft)는 저장소용으로 미리 만들어 두는 맥락 그래프다. 코드베이스를 정확한 file:line 범위를 담은 작고 서로 연결된 graft/ 마크다운 카드들로 색인하고, 누가 무엇을 호출하는지에 대한 콜 그래프도 함께 만든다. 핵심은 속도다. 큰 모노레포를 탐색하는 에이전트가 graft ask "<question>"이나 graft callers <symbol>을 물으면, 매 턴마다 같은 맥락을 재구성하려고 grep을 반복하거나 파일 전체를 여는 대신 소스 범위가 함께 담긴 순위화된 사전 계산 답변을 받을 수 있다. graft는 2026-09-06부터 워크스페이스 루트에서 돌아가고 있었지만, ADR-0076 이전까지는 수작업으로 연결되어 있었고 Claude Code에서만 사용할 수 있었다.
2026-09-12 플릿(fleet) 조사(설계 문서: 2026-09-12-graft-multiplatform-rollout-design.md)에서 실제 커버리지가 겉보기보다 훨씬 좁다는 사실이 드러났다. templates/common은 graft 스킬 파일과 Gemini MCP 항목만 포함하고 있어서, 새로 스캐폴딩된 프로젝트는 Gemini CLI/Antigravity용 graft만 갖추었고 같은 프로젝트의 Claude Code 쪽에는 graft 소스가 전혀 없었다. 11개 프로젝트 플릿 전체가 사실상 graft를 갖고 있지 않았는데, 이는 .claude/skills/**가 보통 sync-skills.ts의 플랫폼 미러링 규칙이 관리하는 영역인 반면 graft의 스킬은 의도적으로 SSOT인 skills/ 디렉터리 바깥에 위치하기 때문에, 어떤 업그레이드 패스도 이를 전달한 적이 없었기 때문이다. 여기에 더해 같은 MCP 서버가 어떤 설정 파일을 보느냐에 따라 npx, bunx, 네이티브 바이너리 등 세 가지 다른 방식으로 실행되고 있었고, Codex/Antigravity/Claude Desktop에 대한 호스트 커버리지는 문서화되지 않은 채 암묵적으로만 존재했다.
ADR-0076은 호스트별 전달 매트릭스로 이 공백을 메운다. 프로젝트 단위 설정을 지원하는 호스트에는 저장소 수준 MCP 등록을 배치하고(Claude Code의 .mcp.json, Gemini/Antigravity의 .gemini/settings.json, Codex의 .codex/config.toml, OpenCode의 opencode.json), AGENTS.md/CLAUDE.md/GEMINI.md에 안내 블록을 넣고, 프로젝트 스코프가 없는 호스트에는 머신 전역 등록을 사용한다(Antigravity는 graft init --agents antigravity를 통한 전역 레지스트리, Claude Desktop은 작업 디렉터리를 전달할 수 없어 절대 경로가 필요하므로 손으로 작성한 claude_desktop_config.json 항목). bunx @nanonets/graft mcp가 모든 플랫폼에서 공통으로 커밋되는 표준 실행기가 되며, 머신 전역 설정에서는 네이티브 바이너리도 계속 쓸 수 있다.
기존 플릿으로의 전달은 8장 §2에서 다룬 정책 기반 업그레이드 엔진을 그대로 타고 이루어지며, 일회성 마이그레이션 패스가 아니다. .mcp.json/opencode.json은 JSON_MERGE_FILES 분류에 합류해(그래야 프로젝트 고유의 MCP 서버가 병합 후에도 살아남는다) .claude/skills/graft/**는 일반 플랫폼 미러링 규칙보다 먼저 동기화되도록 특별 취급되고(그 규칙은 애초에 SSOT 밖에 있는 스킬에는 닿을 수 없으므로), .codex/**는 ADD_IF_MISSING으로 시딩되어 이미 자체 config.toml을 가진 프로젝트는 기존 내용을 유지한다. graft 스킬 자체는 SSOT로 옮기지 않고 Claude 전용으로 남기며 루트와 templates/common 두 곳에 바이트 단위로 동일한 사본을 손으로 유지한다. SSOT로 옮기면 sync-skills.ts가 이를 Gemini/Antigravity 자체 스킬 디렉터리로 미러링해 버려서, 이 도구에 대해 워크스페이스가 의도적으로 고정한 단일 플랫폼 설계가 깨지기 때문이다. 신선도(freshness)는 의도적으로 수동으로 남겨 둔다(graft 도구는 답하기 전에 스스로 새로고침하며, 별도의 CI나 푸시 전 신선도 게이트는 없다).
유니버설 디자인 게이트 — L0~L3 전반에서 스펙 게이트를 균일하게 만들기
8장 §4는 아래 실행 계획 템플릿에서 L0/L1 실행 계획의 디자인 게이트(0번 행)를, 그리고 이를 동기화 시 FATAL 차단 조건으로 강제하는 스펙 체크(ADR-0055 Stage 2)를 이미 소개한 바 있다. 이 절에서 다루는 두 ADR은 같은 거버넌스 개념을 서로 다른 방향으로 확장하는데, 둘을 구분해 둘 필요가 있다.
ADR-0068("유니버설 디자인")은 동기화 시점 게이트가 아니라 디자인 리뷰 방법론에 관한 것이다. 2026-09-06 리뷰에서, 워크스페이스가 WCAG 2.1 AA 준수(ADR-0065)를 기술적 최저 기준으로 강제하고는 있지만 어떤 계층에서도 노화, 인지 부하, 상황적 손상, 운동 능력 편차처럼 더 폭넓은 사용자 다양성에 대해서는 디자인을 평가하지 않는다는 사실이 드러났다. ADR-0068은 유니버설 디자인의 고전적인 7원칙(공평한 사용, 유연한 사용, 단순하고 직관적인 사용, 인지 가능한 정보, 오류에 대한 관용, 적은 신체적 노력, 적절한 크기와 공간)을 WCAG 위에 놓이는 리뷰 어휘로 채택하고, Design Foundation의 원칙 집합에 인지 부하와 오류 복구라는 두 가지 새로운 파생 기준을 추가하며, 여정 지도 작성(journey mapping) 중에 다양성 프로파일 리뷰 단계(노화·인지·상황·운동 제약 프로파일 각각으로 여정을 한 번씩 걸어보는 것)를 요구한다. 어떤 디자인이 WCAG는 통과하면서도 유니버설 디자인은 통과하지 못할 수 있다는 점, 그리고 UD는 토큰 값이나 색상, 레이아웃 세부 사항이 아니라 리뷰 방법론을 규율한다는 점을 명시하고 있다.
ADR-0074("유니버설 디자인 게이트")는 이름은 비슷하지만 위의 디자인 리뷰 렌즈가 아니라 동기화 시점 스펙 체크 게이트의 적용 범위에 관한 별개의, 더 나중 결정이다 — 그래서 단순히 "UD에 게이트를 씌운 것"은 아니다. 조사 결과 docs/specs/registry.json 스펙 게이트는 새로 스캐폴딩된 프로젝트에는 구조적으로 아예 없었고(ADR-0073이 docs/specs를 TEMPLATE_ONLY로 분류해 스캐폴드 시 삭제되고 업그레이드로도 전달되지 않았기 때문), 기존 11개 플릿 프로젝트 중 5개에는 아예 존재하지 않았다. AGENTS.md 또한 L2/L3 베리언트 프로젝트를 디자인 게이트에서 명시적으로 예외 처리하고 있었다. ADR-0074는 이 게이트를 모든 티어(L0~L3)에 걸쳐 보편적으로 만든다. 동일한 스펙 체크 메커니즘이 이제 어디서나 적용되지만, 절차는 다르다 — L0/L1은 기존의 완전한 0번 행 실행 계획 방식을 유지하고, L2/L3 프로젝트는 디자인 문서 하나와 spec-register.ts로 이루어진 더 가벼운 방식을 쓴다. docs/specs는 TEMPLATE_ONLY_DIRS에서 제외되어 add-if-missing 시드로 재분류되어 스캐폴드와 업그레이드 모두에서 실제로 레지스트리를 전달할 수 있게 되며, 기존 E1~E5 예외 범주는 모든 티어에서 안전 밸브로 변함없이 작동한다.
실행 계획 템플릿 — 다중 에이전트 작업의 구조화된 실행 설계
PM이 복잡한 작업을 여러 에이전트에 분배할 때 실행 계획(Execution Plan) 형식을 사용한다. 이 계획은 "누가 무엇을 어떤 티어로, 어떤 순서로"를 테이블로 명시하여 실행의 일관성을 보장한다.
표준 실행 계획 형식
| # | Task | Agent | Tier | Model | Spec |
|---|---|---|---|---|---|
| 0 | Create/update design doc → docs/designs/<spec-id>-design.md | architect | High | [model] | NEW |
| 1 | [task description] | [specialist] | High/Medium/Low | [model] | <spec-id> |
| N | /sync "type(scope): message" | pm | Medium | [model] |
Design Gate (Row 0)
Row 0은 L0(워크스페이스 루트)과 L1(공통 템플릿)에만 강제된다. architect가 설계 문서를 먼저 작성하고, PM이 이를 검증한 뒤에야 구현 단계(Row 1 이후)로 진행할 수 있다. L2 variant 프로젝트는 자체 워크플로우를 관리하므로 이 게이트가 적용되지 않는다.
Design Gate 예외 카테고리
Row 0이 부담스러운 단순 작업에는 다음 5가지 예외 카테고리(E1~E5)만 인정한다. 예외는 PM이 임의로 만들 수 없고, 아래 정의된 카테고리만 사용해야 한다.
| ID | 카테고리 | 설명 |
|---|---|---|
| E1 | memory-log | memory/YYYY-MM-DD.md 세션 로그 작성 |
| E2 | changelog | CHANGELOG.md 업데이트만 |
| E3 | hotfix-typo | 오타 수정, 한 줄 변경, 사소한 수정 |
| E4 | pure-readme | README.md 본문 텍스트만(구조/설계 변경 없음) |
| E5 | sync-only | /sync 실행만(라이프사이클 종료) |
예외 적용 시 Row 0의 Agent/Tier/Model 컬럼은 빈칸(—)으로 표시한다.
이 예외 체계는 문서 규칙에 그치지 않고 동기화 파이프라인이 기계적으로 강제한다. ADR-0055 Stage 2부터 dev-sync.ts의 3.9단계(spec registry check)는 코드 변경에 대응하는 spec 활동이 없으면 동기화를 FATAL 차단한다. 이를 피하는 유일한 방법은 audit.ts --spec-exempt=E1..E5(또는 SYNC_SPEC_EXEMPT 환경 변수)로 위 카테고리 중 하나를 명시하는 것이며, spec은 docs/specs/registry.json에 등록된다(2026-08-24 기준).
실전 예시: PM이 작성한 실행 계획
다음은 "agents/pm.md와 스크립트를 업데이트하는 플랫폼 패리티 작업"의 실제 실행 계획 예시다.
| # | Task | Agent | Tier | Model | Spec |
|---|---|---|---|---|---|
| 1 | Update agents/pm.md | docs-writer | Medium | sonnet | <spec-id> |
| 2 | Update scripts/audit.ts | automation-engineer | Low | haiku | <spec-id> |
| 3 | Update CLAUDE.md §5 | docs-writer | Medium | sonnet | <spec-id> |
| 4 | Update GEMINI.md §5 | docs-writer | Medium | sonnet | <spec-id> |
| 5 | /sync "docs(agents): update pm.md and platform dispatch rules" | pm | Medium | sonnet |
실행 순서: Sequential — 플랫폼 패리티를 위해 CLAUDE.md와 GEMINI.md를 함께 업데이트해야 하므로 순차 실행이다. automation-engineer에 low가 배정된 것은 Tier Adjustment Rule에 따른 것이다.