aster®

프로젝트별 경로와 실행 명령을 에이전트 설정으로 분리하기

마무리 · 2026-09

왼쪽의 문서 한 장에서 나온 선이 가운데 마름모 이음쇠를 거쳐 세 갈래로 나뉘고, 각 갈래 끝에 서로 다른 모양과 색의 도형이 놓여 있다.

코드를 대신 써 주는 AI 를 이 시리즈에서는 에이전트라고 부른다. 하나만 시킬 때는 잘 되다가도 화면·서버·테스트를 셋으로 쪼개 동시에 돌리면 사정이 달라진다. 셋은 서로 무엇을 하는지 모르고 어제 무엇을 정해 줬는지도 기억하지 못한다. 새로 실행한 에이전트에게는 이전 결정과 프로젝트 설정을 다시 전달해야 한다.

그래서 사정을 문서로 적어 두게 된다. 에이전트가 지킬 규칙만 모아 둔 문서 묶음을 하네스라고 하는데, curvez 가 그것이다. 규약이니까 어느 프로젝트에 붙여도 같은 문장이다.

그런데 그 문서를 끝까지 읽어도 알 수 없는 값이 있다. 테스트 명령이 pnpm test 인지 pnpm vitest run 인지, 웹 화면 코드가 src/ 에 있는지 apps/web 에 있는지, 작업 브랜치를 main 에서 따는지 develop 에서 따는지. 프로젝트마다 값이 다르니 규약은 이걸 정할 수 없다. 이 값들을 어디에 적어 둘지부터 정해야 한다.

curvez 는 그 값들을 .curvez/profile.json 파일 하나에 몰아 넣는다. 그 파일을 만드는 절차가 bootstrap 이다. 이 절차에는 확인할 수 없는 값을 처리하는 규칙도 포함돼 있다.

1. 잘못된 프로젝트 설정의 영향

profile.json 은 curvez 전체의 진입 전제다. stack 이 어떤 에이전트를 띄울지, paths 가 각 에이전트가 손댈 폴더를, commands 가 검사에 쓸 명령을 정한다.

그래서 값을 하나 빠뜨리면 그 값을 쓰는 에이전트가 죄다 어긋난다. 이 저장소에 그 상태가 그대로 남아 있다.

.curvez/profile.json 의 commands 는 이렇게 적혀 있다.

"commands": {
  "typecheck": "pnpm typecheck",
  "lint": "pnpm lint",
  "test": "pnpm test",
  "build": "pnpm build"
}

그런데 apps/handwork/package.json 의 scripts 에는 dev·build·typecheck·start·lint 다섯 개뿐이다. test 가 없다. 돌려 보면 이렇게 끝난다.

$ pnpm test
EXIT=1

코드를 다음 단계로 넘기기 전에 타입 검사·린트·테스트를 돌리는 관문이 있다. curvez 는 이걸 품질 게이트라고 부른다. 게이트가 이 값을 읽으면 매 라운드 같은 자리에서 exit 1 이 나오는데, 출력만 봐서는 코드가 깨진 실패와 구분되지 않는다. 테스트를 맡은 에이전트가 "검증 실패" 로 보고하면, 코드를 쓰는 에이전트는 멀쩡한 코드를 뜯어고치기 시작한다.

더 눈여겨볼 것은 이게 아직 안 터졌다는 사실이다. .curvez/handoff/ 에 남은 핸드오프 세 건을 열어 보면 verification 에 test 축이 0건이다.

핸드오프verificationtest 축
curvez-architect6항목없음
curvez-designer8항목없음
curvez-nextjs8항목없음

표 1. 실제 핸드오프 세 건의 검증 항목

세 라운드가 도는 동안 아무도 그 축을 안 돌렸으니 값이 틀렸다는 것도 드러나지 않았다. 설정값이 틀려도 해당 명령을 실행하기 전에는 오류가 드러나지 않는다.

앞의 실패는 그래도 에러 메시지가 뜬다. paths.domain 을 잘못 적으면 그마저 없다. 여러 에이전트가 같은 폴더를 한꺼번에 고치고 나중에 저장한 쪽이 앞선 쪽 변경을 덮어쓴다. 덮어썼다는 기록이 어디에도 안 남으니 무엇이 사라졌는지조차 모른다. 에이전트를 띄우고 순서를 정하는 쪽을 오케스트레이터라고 한다. 오케스트레이터는 이 값을 보고 하나씩 돌릴지 한꺼번에 띄울지를 정한다. 경로에 소유자가 없으면 한꺼번에 띄우는 쪽을 고른다.

그래서 이 스킬은 모르는 값을 채우지 못하게 한다.

2. profile.json의 구성

curvez 가 기대하는 profile.json 의 정본 형식은 이렇게 생겼다.

{
  "stack": "monorepo",
  "packageManager": "pnpm",
  "architecture": "ddd",
  "paths": {
    "web": "apps/web",
    "domain": "packages/domain",
    "tests": "tests"
  },
  "git": {
    "baseBranch": "release",
    "releaseBranch": "main",
    "mergeStrategy": "rebase",
    "protectedBranches": ["main", "release"],
    "humanMergeTargets": ["main"]
  },
  "commands": {
    "typecheck": "pnpm typecheck",
    "lint": "pnpm lint",
    "test": "pnpm test",
    "build": "pnpm build"
  }
}

stack 은 이 프로젝트가 무엇으로 만들어졌는지다. nextjs 와 monorepo 중 하나를 쓴다. nextjs 는 웹 앱 하나로 된 저장소, monorepo 는 웹 코드와 공용 코드를 저장소 하나에 폴더로 나눠 담은 형태다. 이 값에 따라 필수 키가 달라진다.

stack필수 키선택 키
nextjspaths.webpaths.tests
monorepopaths.web, paths.domainpaths.tests

표 1. stack 값에 따른 필수 키와 선택 키

architecture 초기값은 "ddd" 다. 확정은 다음 편의 architecture-setup 이 한다. bootstrap 은 .curvez/architecture.md 의 자리만 만들고 내용은 손대지 않는다.

값을 모르는 선택 키는 키째로 생략한다. 빈 문자열이나 null 을 넣지 않는다. 뒤에 오는 에이전트는 키의 존재 여부로 동작을 결정한다. "" 는 "없음" 이 아니라 "빈 경로" 로 읽혀 루트 전체를 대상으로 삼는 명령이 만들어진다.

3. 자동 탐지와 사용자 확인의 구분

첫 단계는 저장소를 훑어 알아낼 수 있는 것만 알아내는 일이다. 이걸 감지라고 부른다. 루트 package.json 한 장에서 workspaces 나 pnpm-workspace.yaml 의 유무, next 와 packageManager, scripts 의 키를 한 번에 본다. 워크스페이스 설정이 있으면 monorepo 를 의심한다.

감지한 결과에 따라 스택을 판정한다.

감지 결과판정
workspace: false, next 있음nextjs
workspace: true아직 확정하지 않는다. 워크스페이스를 순회한다

표 2. 감지 결과에 따른 스택 판정

commands 는 감지한 scripts 배열에서만 고른다. typecheck 키는 typecheck → type-check → tsc 순으로 보고 먼저 맞는 이름 하나를 쓴다. lint·test·build 는 같은 이름을 쓴다. 후보가 하나도 없으면 그 키를 통째로 생략한다. 명령을 지어내지 않는다.

감지로 못 채운 것만 인터뷰로 올라간다. 문항 상한은 5문이다. 후보는 여섯 개인데 던지는 것은 다섯까지다. 밀린 키는 status: blocked 로 남긴다.

사용자가 한 번에 답해야 하므로 5문까지만 묻는다. 6문을 넘기면 뒤쪽 답이 짧아지거나 통째로 빠져 추측으로 메우게 된다. 5문으로 안 끝날 때는 감지를 덜 돌렸는지부터 봐야 한다. 인터뷰를 늘리는 것보다 감지로 돌아가는 편이 빠르다.

폴백이 허용되는 키는 paths.tests 하나다. ls -d tests test __tests__ e2e 로 확인해 하나만 잡히면 그걸 쓰고 없으면 키를 생략한다. 필수 키에는 폴백이 없어서 못 채우면 blocked 다.

4. 브랜치 설정을 별도로 확인하는 이유

git 블록은 다섯 키를 전부 채운다. 감지는 git branch -r 로 한다.

키무엇채우는 법
baseBranch작업 브랜치를 따는 곳이자 PR 타겟원격의 release 또는 develop
releaseBranch배포된 것원격의 main 또는 master
mergeStrategy머지 방식초기값 "rebase"
protectedBranches직접 커밋 금지[releaseBranch, baseBranch]
humanMergeTargets이 타겟으로 가는 PR 은 사람이 누른다초기값 [releaseBranch]

표 3. git 블록의 다섯 키와 채우는 법

통합 브랜치가 원격에 없으면 release 를 새로 만들어 2단으로 간다. 이때 git push -u origin release 까지 마쳐야 한다. push 전까지 baseBranch 는 원격에 없어 검증을 통과할 수 없다.

원격의 release 나 develop 을 보고 2단으로 판정한 경우는 다르다. 프로파일에 쓰기 전에 사용자 확인을 받는다. 이름만 보고 정한 값이라서다. 이름만 남고 아무도 쓰지 않는 브랜치라면 작업 브랜치가 거기서 나고 PR 도 거기로 열린다. 오류가 나지 않으니 리뷰 화면을 열어 보기 전까지 드러나지 않는다.

새로 만든 release 에는 확인 문항이 없다. 빈자리에 기본 전략을 세웠으니 확인할 추정값 자체가 없기 때문이다. 확인 문항은 원격 브랜치 이름을 보고 추정한 값에만 붙는다. 새로 만든 브랜치처럼 추정하지 않은 값에는 붙지 않는다.

humanMergeTargets 초기값도 같은 이유다. 그 머지가 곧 배포인지는 프로젝트의 배포 설정에 달렸고 curvez 는 그걸 확인할 수 없다. 그래서 안전한 쪽인 [releaseBranch] 가 기본이다. 배포가 아닌 프로젝트라면 사용자가 배열을 비운다. 에이전트가 임의로 비우지 않는다.

5. bootstrap 실행 순서

트리거는 "curvez 붙여줘", "curvez 시작", "초기 설정 해줘", "부트스트랩", "bootstrap", "set up curvez", "init curvez" 다. .curvez/profile.json 없이 curvez 작업을 시작하려 할 때도 뜬다.

절차 전체가 실행기로 구현돼 있다.

node "$CLAUDE_PLUGIN_ROOT/scripts/bootstrap.mjs" [--dry-run] [--json] [--force]

스크립트는 사용자에게 묻지 않는다. 자동 판정만 한다. 나머지는 questions[] 로 돌려준다. 그 질문을 사용자에게 전달하는 것은 스킬의 몫이다.

그렇게 돌고 나면 아래 파일들이 만들어져 있다.

.curvez/profile.json       ← 확정된 전제
.curvez/architecture.md    ← 헤딩 7개 + "architecture-setup 이 채운다." 한 줄씩
.curvez/team.md            ← "아직 팀이 편성되지 않았다." 한 줄
.curvez/research/.gitkeep
.curvez/handoff/.gitkeep
.curvez/tmp/

여기에 더해 .github/workflows/ci.yml, ESLint·Prettier 설정, CLAUDE.md 를 없을 때만 만든다. --force 는 .curvez/profile.json 에만 걸린다. 워크플로·lint 설정·CLAUDE.md 는 --force 로도 덮지 않는다. 프로파일을 덮으면 저장소 안의 전제가 바뀌지만 워크플로를 덮으면 배포 파이프라인이 바뀐다. 되돌리는 데 드는 비용도 그만큼 다르다.

.gitignore 에는 .curvez/tmp/ 한 줄만 추가한다. .curvez/ 를 통째로 무시하면 안 된다. profile.json·architecture.md 는 사람과 에이전트가 공유하는 전제다. 커밋하지 않으면 다른 사람의 에이전트가 다른 전제로 시작한다.

절차의 마지막에는 지금까지 채운 값이 실제로 맞는지 검증한다. 출력은 이 한 줄이다.

stack=monorepo missing=none notOnDisk=none commands=typecheck,lint,test,build

missing=none, notOnDisk=none, exit 0 이어야 한다. notOnDisk 가 paths 의 경로를 디스크에서 찾아 보는 자리라, 그럴듯하게 지어낸 경로가 여기서 걸린다. missing 이 남으면 status: blocked 로 핸드오프를 낸다. 핸드오프는 다음 차례에게 남기는 인수인계 JSON 이고 거기에 blocked_on 으로 who: user 를 적어 둔다. 절대 메우지 않는다.

6. 프로필 설정의 관리 비용

시작이 대화 한 번 늘어난다. 감지가 다 되면 인터뷰가 0문이다. 애매한 저장소에서는 최대 5문을 답해야 첫 줄의 코드가 나온다. "일단 짜 보자" 로 시작하고 싶은 사람에게는 이 지연이 실제 비용이다.

멱등이지만 값을 고쳐 주지는 않는다. 여러 번 돌려도 결과가 같다. profile.json 이 이미 있으면 덮어쓰지 않는다. missing 에 나온 필수 키만 채운다. 그래서 처음에 잘못 답한 값은 bootstrap 을 다시 돌려도 안 고쳐진다. 고치는 것은 마이그레이션이고 사용자가 결정할 일이다.

패키지 매니저가 pnpm 으로 고정된다. 감지값이 pnpm 이 아니어도 프로파일에는 pnpm 이 들어간다. 그 사실은 인터뷰가 아니라 완료 보고에 남는다. 교체는 bootstrap 의 범위가 아니라서다. npm·yarn 프로젝트는 사람이 먼저 확인해야 한다. commands 의 pnpm ... 이 그 환경에서 도는지를 본다.

GitHub 이 아니면 CI 가 안 만들어진다. GitLab·Bitbucket 은 파일 위치와 문법이 다르다. 형식을 지어내면 안 도는 파일이 저장소에 남는데도 사용자는 CI 가 있다고 믿게 된다. 그래서 만들지 않고 이유만 보고한다. 이 경우 사용자가 해당 서비스에 맞는 CI를 직접 작성해야 한다.

아직 확인하지 못한 사항

presets/stack/*.md 는 세 파일이고 각 18~25KB 다. 스택별 오탐 케이스와 경로 후보를 담고 있다고 SKILL.md 가 말한다. 이 글에서는 목차 수준으로만 확인했다. 안에 적힌 함정을 실제로 대조하지는 못했다.

프로젝트 설정을 한 파일에 모은 결과

프로젝트당 사실상 한 번 도는 스킬이라 다시 볼 일은 드물다. 대신 뒤에 오는 열다섯 편이 전부 이 파일을 읽는다. 새로 온 에이전트도 이 파일부터 읽는다. 다음은 architecture-setup 이다. profile.json 이 "소스가 어디 있는가" 를 정했다면, 다음은 "무엇이 무엇을 import 할 수 있는가" 를 정한다.