L3 프로젝트 업그레이드
L3 프로젝트를 만든 뒤 상위 표준(L0/L1)이 바뀌면 무슨 일이 일어날까 — 정답은 "아무 일도 안 일어난다"이다. 8장 §1에서 다룬 포크 모델 때문에, 이미 태어난 L3 프로젝트에 새 변경을 실제로 반영하려면 upgrade-project.ts를 사람이 명시적으로 실행해야 한다. 이 장은 그 절차를 다룬다.
소스: ai-workspace-standards/scripts/upgrade-project.ts(v1.10.1 기준) · 관련: 8장 §1 배포와 SSOT
- 포크 모델에서 "업그레이드"가 왜 자동이 아니라 명령을 실행해야 하는 작업인지
upgrade-project.ts의 기본 사용법과--dry-run우선 원칙- LOCKED·MERGE·DOCS_MERGE·VARIANT_DOCS_SYNC·COMMANDS_SYNC·SYNC_IF_NEWER·PRESERVE·PRUNE와 TEMPLATE TREE SYNC·ENV_SAMPLE SYNC 등 주요 파일 처리 방식
- 자동 git stash 스냅샷과
--rollback으로 되돌리는 안전장치 - 11장에서 다루는 "승격"과 방향이 정반대라는 점
왜 필요한가 — 포크 모델의 이면
8장 §1에서 확인했듯, L1은 new-project.ts로 프로젝트를 스캐폴딩하는 시점에 딱 한 번만 내용을 전달한다. 그 이후 L3는 완전히 독립적으로 진화하며, L1이 아무리 바뀌어도 이미 만들어진 L3에는 아무것도 자동으로 반영되지 않는다.
문제는 시간이 지날수록 L1 쪽에서 훅 스크립트가 고쳐지고, 새 스킬이 추가되고, 보안 설정이 강화된다는 점이다. 이런 변경을 개별 L3 프로젝트에 반영하고 싶을 때 파일을 하나하나 손으로 대조하는 건 현실적이지 않다. upgrade-project.ts는 이 반영 작업을 자동화하되, 포크 모델의 핵심 원칙 — "L3가 의도적으로 다르게 고친 부분은 함부로 덮어쓰지 않는다" — 은 그대로 지킨다.
upgrade-project.ts 사용법
가장 먼저 --dry-run으로 무엇이 바뀔지 미리 확인한다. 실제로 파일을 건드리지 않고 콘솔에 계획만 출력한다.
bun scripts/upgrade-project.ts <project-path> --dry-run
출력 내용에 문제가 없으면 --dry-run을 빼고 다시 실행한다.
bun scripts/upgrade-project.ts <project-path> # 선택 플래그: --variant <variant> --platform claude|antigravity|codex|all # --prune-removed --rollback
--variant는 프로젝트의 .claude/template-version.txt에 기록된 값을 자동으로 읽어오므로 대부분 생략해도 된다. 이 파일이 없는 오래된 프로젝트라면 실행 시 확인 프롬프트가 뜨고, --variant를 직접 지정해야 한다.
주요 파일 처리 분류
스크립트는 파일을 성격에 따라 다르게 다룬다 — 전부 똑같이 "새 걸로 덮어쓰기"가 아니다.
| 분류 | 동작 | 대상 예시 |
|---|---|---|
| LOCKED | 항상 템플릿 내용으로 덮어씀 | .githooks/*(pre-commit 라우팅 등), .gitattributes |
| LOCKED (merge-aware) | LOCKED처럼 항상 템플릿을 가져오되 병합 인식 — 프로젝트에만 있는 정규식·허용 목록(regexes/paths) 항목은 보존하고 관리 규칙만 복원한다(v1.10.0부터; 이전의 무조건 덮어쓰기는 프로젝트 전용 허용 목록을 유실시켰음) | .gitleaks.toml(mergeGitleaksToml()) |
| MERGE | WORKSPACE-MANAGED/COMMON-CLAUDE/COMMON-GEMINI/COMMON-AGENTS/VARIANT-INJECT/DYNAMIC_SKILLS 마커로 감싼 구간만 병합, 나머지 로컬 내용은 그대로 보존 | CLAUDE.md/GEMINI.md/CODEX.md(--platform에 따라), .gitignore, agents/pm.md |
| DOCS_MERGE | 문서에도 MERGE와 같은 마커 구간 병합을 적용 — 마커 없는 문서만 예외적으로 통째로 덮어씀(v1.7.0 추가) | AGENTS.md, docs/<variant>.context.md, docs/phase-definitions.md |
| VARIANT_DOCS_SYNC | 파일 안의 *<파일명> version: X.Y 각주 버전(없으면 해시)이 템플릿보다 낮을 때만 갱신(v1.7.0 추가) | docs/context.md(v1.9.0부터), docs/engagement-orchestration.md, docs/team-configuration-guide.md |
| COMMANDS_SYNC | 해시가 다르면 플랫폼 명령 파일을 템플릿 내용으로 동기화(v1.7.0 추가) | .claude/commands/*.md, .gemini/commands/*.md |
| SYNC_IF_NEWER | 템플릿 버전이 프로젝트 버전보다 높을 때만 복사 | scripts/*.ts(@version 주석, JSDoc 헤더 포함), agents/*.md(frontmatter version:), skills/*/SKILL.md |
| PRESERVE | 목록에만 표시, 전혀 손대지 않음 | README.md, README_ko.md, src/ |
| PRUNE(선택) | --prune-removed 지정 시, 템플릿에서 사라진 scripts/agents/skills 파일을 프로젝트에서도 제거 | 위 SYNC_IF_NEWER 대상과 동일한 디렉터리 |
upgrade-project.ts는 위의 전통적 분류에 더해 TEMPLATE TREE SYNC(전담 분류가 없는 템플릿 파일을 기본 전달), ENV_SAMPLE SYNC(국가별 .env.sample 병합), CONTEXT_COMMONIZATION(중복 context 섹션 정리), GOVERNANCE FILES SYNC(LICENSE/SECURITY 등 add-if-missing)을 사용한다. Codex 표면도 CODEX.md MERGE와 .codex/config.toml add-if-missing 대상으로 포함되며, graft 관련 표면도 같은 업그레이드 엔진으로 전달된다.v1.10.x부터 업그레이드는 국가 프로필(국가 스코프 자산)도 인식한다. 실행 전 프로젝트의 .claude/template-version.txt에서 country= 줄을 읽어(없으면 docs/countries/ACTIVE.md의 관할 표기로 대체) 대상 국가를 파악하고, 마무리 시 이 줄을 그대로 보존한다 — 구버전은 이 파일을 재작성하면서 country= 줄을 지워버려 국가 소속 정보를 잃게 했다. 또한 스킬 동기화가 모두 끝난 뒤 레지스트리 기반 prune가 한 번 더 실행되어, 국가 스코프 스킬(k-dart/k-law/k-kosis)이 해당 국가가 아닌 프로젝트에 다시 주입됐다면 삭제한다 — 스캐폴딩 시점의 정리 결과를 매번 업그레이드가 되돌리는 사태를 막기 위함이다. 단, 로컬에서 의도적으로 고친 포크는 ⚠️ CONFLICT 경고와 함께 그대로 보존된다.
SYNC_IF_NEWER 대상 파일을 로컬에서 직접 고쳐 쓴 상태인데 템플릿 쪽 버전이 더 높으면, 스크립트는 ⚠️ CONFLICT로 표시하면서도 결국 템플릿 내용으로 덮어쓴다. "덮어쓰기 전에 물어봐 주는" 동작이 아니라 "경고만 하고 진행하는" 동작이라는 점을 기억해야 한다 — 그래서 아래 안전장치가 중요하다.
안전장치 — 스냅샷과 롤백
실행 전(--dry-run이 아닐 때) 스크립트는 자동으로 git stash push -m pre-upgrade-snapshot-YYYYMMDD를 실행해 현재 작업 트리 상태를 스냅샷으로 남긴다. 업그레이드 결과가 마음에 들지 않으면 다음 명령으로 즉시 되돌릴 수 있다.
bun scripts/upgrade-project.ts <project-path> --rollback
이 명령은 방금 만든 pre-upgrade-snapshot-* 스태시를 찾아 git stash pop으로 복원한다. 업그레이드 자체는 커밋을 만들지 않으므로, 결과를 확인한 뒤에는 평소처럼 git diff로 변경 내용을 검토하고 직접 커밋해야 한다.
마무리 단계로 .claude/template-version.txt가 새 버전으로 갱신되며 이때 country= 줄은 그대로 보존되고, .gitleaks.toml·pre-commit 훅·.gitattributes(eol=lf)·.gitignore(.env 패턴)·git core.hooksPath 같은 보안 부트스트랩 항목이 자동으로 점검된다(3장 가드레일 원칙과 동일한 항목들이다). core.hooksPath가 잘못돼 있으면 스크립트가 스스로 고쳐준다.
승격과의 방향 차이
11장에서 다루는 "승격"과 이번 장의 "업그레이드"는 이름이 비슷해 보이지만 방향이 정반대다.
- 승격(
l3-to-variant-pipeline.ts, 11장) — L3 → L2. 내가 만든 프로젝트의 변경을 공식 variant 템플릿으로 끌어올려, 다른 사람도 쓸 수 있는 베리언트(variant)로 만드는 작업. - 업그레이드(
upgrade-project.ts, 이 장) — L1 → L3. 공식 템플릿의 최신 변경을 이미 만들어진 내 프로젝트에 끌어오는 작업.
두 스크립트는 서로 반대 방향으로 파일을 흘려보낼 뿐, 어느 한쪽이 다른 쪽을 대체하지 않는다. 템플릿에 기여하고 싶다면 승격을, 이미 만든 프로젝트를 최신 상태로 유지하고 싶다면 업그레이드를 쓴다.
ai-workspace-standards 저장소의 scripts/upgrade-project.ts(v1.10.1) 소스를 직접 확인해 작성했다. 이 핸드북 저장소에는 해당 스크립트가 포함돼 있지 않으므로, 실제 실행 결과는 반드시 최신 소스와 대조해 확인한다.