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

X Facebook

Written by

in

이 시리즈에서는 코드를 작성하고 작업을 수행하는 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건이다.

핸드오프 verification test 축
curvez-architect 6항목 없음
curvez-designer 8항목 없음
curvez-nextjs 8항목 없음

표 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 필수 키 선택 키
nextjs paths.web paths.tests
monorepo paths.web, paths.domain paths.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. 프로파일을 만드는 절차

트리거는 “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 할 수 있는가”를 정한다.

Comments

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다