받아 온 스킬은 고치지 않고 훅으로 늘렸다
QA 수정 요청 55건을 워커 여섯 개로 나눠 돌린다. 화면 34건을 워크트리 다섯 개에 흩어 분석시킨다. 테스트를 다섯 갈래로 갈라 각자 쓰게 한다. Orca의 오케스트레이션으로 요즘 이렇게 일한다.
굴러가는 모양은 이렇다.
- 사람이 대화하는 창이 코디네이터 하나. 일을 쪼개 나눠 주고 결과를 받아 합친다
- 태스크 하나에 워커 하나. 자식 워크트리에서 각자 뜬 세션이다
- 워커 브랜치는 코디네이터 브랜치에서 갈라져 나오고, 다시 그리로만 합쳐진다. develop으로 직접 가지 않는다
- 워커는 계획만 세우는 모드로 뜬다. 코드를 고치는 모드로 바꾸는 건 사람이 그 터미널에서 직접 한다
- 테스트를 돌리려면 워커가 코디네이터에게 순번을 받아야 한다. 전체를 통틀어 동시에 두 개까지다
- 합치는 건 코디네이터만, 한 번에 하나씩 fast-forward로
- 실제로 합쳐진 걸 확인한 다음에만 워크트리를 지운다
정본에는 이 중 아무것도 없다
명령 문법의 정본은 orca skills get orchestration이 낸다.
400여 줄짜리 가이드다. 어떤 명령이 있고 어떤 메시지를 주고받고
셀렉터를 어떻게 쓰는지 다 여기 있다.
그런데 위에 적은 일곱 줄 중 여기서 나온 건 하나도 없다. 정본이 답할 수 없는 것들이기 때문이다.
- 테스트를 동시에 몇 개까지 돌려도 되나
- 워커를 어떤 계정으로 띄우나
- 워커가 바로 코드를 고쳐도 되나, 계획부터 보여줘야 하나
- 다 끝난 작업을 누가, 어떤 방식으로 합치나
프로젝트마다 답이 다르다. 모노레포 테스트가 무거운 건 우리 사정이고, 계정을 나눠 쓰는 것도 우리 사정이다. 범용 가이드가 정해 줄 수 있는 게 아니다.
안 적어 두면 어디로 가느냐 하면, run 목표 줄로 간다.
TICKET-794 QA 수정필요 55건: 6개 워커 병렬 구현, 이슈당 1커밋+1PR, 코디네이터가 rebase+FF 머지
TICKET-575: as-portal 테스트 전면 재작성, 5개 클로드 에이전트 분할 작업, 완료 시 test/TICKET-575로 rebase+ff 병합
일이 다 다른데 뒤쪽 절반은 늘 같다. 워커 몇 개, 어디로, 누가 합치나. 일할 때마다 손으로 다시 적고 있었다.
받아 온 스킬에는 적을 자리가 없다
그러면 그 절반을 스킬에 적어 두면 된다. 문제는 그 스킬이 우리 것이 아니라는 데 있었다.
~/.claude/skills/orchestration/SKILL.md를 열면 82줄이다.
그런데 그 82줄이 이렇게 적혀 있다.
This file is a discovery stub, not the usage guide. The full, version-matched Orca orchestration reference is served by the
orcabinary itself — kept out of this file on purpose so it can never drift from the binary that will actually run your commands.
설치된 파일은 입구일 뿐이고, 본문은 바이너리가 그때그때 낸다. 여기에 규칙을 끼워 넣을 자리가 없다. 스텁에 뭘 적어 둬도 실제로 읽히는 본문은 바이너리가 낸 쪽이다.
받아서 쓰는 다른 스킬은 사정이 조금 다르지만 결론은 같다.
| 스킬 | 출처 | 프로젝트 lock |
|---|---|---|
| shadcn | shadcn/ui | computedHash 기록 |
| migrate-radix-to-base | shadcn/ui | computedHash 기록 |
| orca-cli | stablyai/orca | computedHash 기록 |
| orchestration | orca 바이너리 | 없음(스텁) |
skills-lock.json에 해시가 박혀 있으니 본문을 고치면 그
순간부터 어긋난다. 업데이트를 받으면 덮인다. 위쪽 셋은 고치면
어긋나고, 맨 아래는 고칠 게 없다. 어느 쪽이든 받아 온 것은
손댈 자리가 아니다.
훅으로 이어 붙였다
규칙은 별도 스킬로 만들고, 훅이 그걸 정본 뒤에 붙이게 했다. 스크립트는 16줄이다.
set -eu
input="$(cat)"
skill="$(printf '%s' "$input" | jq -r '.tool_input.skill // empty')"
[ "$skill" = "orchestration" ] || exit 0
rules="${CLAUDE_PROJECT_DIR:-.}/.agents/skills/orca-orchestration-rules/SKILL.md"
[ -f "$rules" ] || exit 0
header="[orca-orchestration-rules — hook 자동 주입] ..."
jq -Rs --arg header "$header" \
'{hookSpecificOutput: {hookEventName: "PostToolUse", additionalContext: ($header + "\n\n" + .)}}' \
< "$rules"
.claude/settings.json에 PostToolUse / matcher Skill로
걸어 뒀다. 어떤 스킬을 썼는지는 matcher가 못 걸러 준다.
도구 이름까지만 걸리기 때문이다. 그래서 스킬 이름 판정은
스크립트가 한다. 직접 넣어 보면 이렇게 갈린다.
| 입력 | 출력 |
|---|---|
{"tool_input":{"skill":"orchestration"}} |
15,294 bytes |
{"tool_input":{"skill":"git-ops"}} |
없음 (exit 0) |
왜 훅이냐가 이 구조의 전부다. 스킬은 description에 적어 둔 말이 걸리면 열린다. 걸리면 열린다는 건 안 걸리면 안 열린다는 뜻이다. 규칙은 빠지면 안 되는 쪽이라, 말이 걸리는 순간이 아니라 기능이 실제로 켜지는 순간을 붙잡았다. 규칙 스킬 쪽 description에는 자동 트리거 키워드를 아예 안 넣고, “자동 트리거 키워드는 없다”고 본문에 적어 뒀다. 열리는 경로가 훅 하나여야 빠지는 경우가 없다.
규칙에는 문법을 적지 않았다
붙이는 쪽에 뭘 적을지가 남는다. 여기서 한 줄도 베끼지 않기로 했다.
- 명령 문법·셀렉터·메시지 타입의 정본은
orca skills get orchestration이다 - 우리 문서는 정책과 실행 순서만 적는다
- 명령이 우리 문서와 다르게 실패하면 정본을 확인한다
- 정본의 범용 권장과 우리 정책이 부딪히면 우리 문서가 우선한다
같이 받아 쓰는 orca-cli 스킬도 79줄짜리 안내문으로 뒀다. 본문을 안 쓴 게 아니라 쓰면 안 되는 것이다. 바이너리가 자기 버전에 맞는 가이드를 들고 있으니, 우리가 베껴 두면 그 순간부터 어긋난다.
못 박은 것은 다섯 개다
사람이 승인하지 않으면 에이전트가 바꿀 수 없는 항목이다. 맨 앞에서 적은 일곱 줄이 여기서 나온다.
| 정한 것 | 이유 |
|---|---|
| 자식 워크트리는 코디네이터 브랜치에서 갈라져 그리로 합친다 | 여러 워커 결과가 develop에 제각각 흘러들지 않게 |
| 워커 계정(alias)을 띄우기 직전에 사람이 고른다 | 같은 계정을 물면 사용량과 대화 기록이 엉킨다 |
| 워커는 plan 모드로 시작한다 | 자동으로 뜬 세션이 승인 없이 파일을 고치지 못하게 |
| 테스트는 전체를 통틀어 동시에 2개 | 모노레포 테스트는 무겁다 |
| 머지는 코디네이터 전담·직렬 | 동시에 합치면 ff 기준점이 흔들린다 |
세 번째를 지키는 데 함정이 하나 있었다. 워커 터미널에 그냥
claude라고 치면 셸 별칭이 그걸 다른 명령으로 바꿔치기한다.
그 바꿔치기된 명령에 바이패스 옵션이 들어 있어서
--permission-mode plan을 덮어 버린다. 앞에 command를 붙여
별칭을 건너뛰고, 띄운 뒤에 상태바에 ⏸ plan mode on이
보이는지 확인하게 적어 뒀다.
워커에게는 훅이 안 닿는다
훅은 코디네이터 세션에만 붙는다. 워커는 자식 워크트리에서
따로 뜬 세션이라 그 세션이 이 규칙 파일을 볼 수 있을지 알 수
없다. 그래서 워커용 규칙은 일감을 보낼 때 전문을 실어
보낸다. 101줄짜리 worker-task-block.md가 그것이다.
요약하거나 발췌해서 보내지 말라고 문서에 적어 뒀다. 워커에게 규칙을 전달하는 통로가 그거 하나뿐이라서다.
받아 온 스킬을 안 더럽히려고 훅으로 깔끔하게 뺐는데, 워커 쪽은 붙여 넣기다. 코디네이터는 훅이, 워커는 복붙이 담당한다.
16줄이 401줄을 끌고 온다
| 줄 | 바이트 | |
|---|---|---|
| 훅 스크립트 | 16 | 800 |
| 우리 규칙 전체 | 401 | 26,243 |
| └ 훅이 주입하는 SKILL.md | 209 | 14,747 |
| └ 링크로만 걸어 둔 references | 192 | 11,496 |
주입은 209줄만 한다. 머지 절차와 워커 지시서 192줄은 링크로만 걸어 두고 필요할 때 읽게 했다. 훅이 미는 건 목차지 전집이 아니다.
그런데 그 401줄이 올라앉은 정본 가이드가 400줄 남짓이다. 문법을 한 줄도 안 베꼈는데 정책만으로 그만큼이 나왔다. 정본 쪽 숫자는 바이너리 버전에 따라 움직이니 정확히 몇 대 몇인지는 의미가 없는데, 자릿수가 같다는 건 좀 봐야 할 것 같다.
아쉬운 것
실측은 태스크 한 개였다. E2E 드라이런으로 워커 생명주기와 머지 플로우를 확인했는데 태스크가 하나였다. 워커도 하나다. 정작 규칙의 핵심인 2슬롯은 두 개가 붙어 본 적이 없다. 동시 GRANTED가 2에서 멈추는지, FIFO 순서가 맞는지, 대기하는 워커의 resume 루프가 도는지 전부 미실측이다. 스킬 본문에도 그렇게 적어 뒀다.
테스트 2슬롯은 재 본 숫자가 아니다. 근거가 “모노레포 테스트는 무겁다”뿐이다. 다섯 개를 한꺼번에 돌려 보고 정한 게 아니라, 그렇게 될까 봐 미리 2로 박았다.
훅은 Claude Code에만 붙는다. 스킬 본문을 .agents/skills
한 곳에 두고 도구마다 심링크로 보게 한 이유가 도구 사이
공유였다. 그런데 .cursor/skills에 걸린 심링크 열두 개에
orca-orchestration-rules는 없다. 훅은 심링크를 따라가지
못한다. 규칙을 스킬 밖으로 뺀 대가다.
스킬을 쓸 때마다 훅이 한 번씩 돈다. matcher가 도구
이름까지만 걸리니 어쩔 수 없다. jq로 이름 하나 보고 나가는
게 전부지만 프로세스는 매번 뜬다.
사람이 읽을 문서가 없다. 209줄 SKILL.md가 유일한 설명서인데 그건 에이전트에게 시키는 말로 쓰여 있다. 팀에 “우리는 이렇게 합니다”를 보여줄 게 아직 없다.
외부 스킬을 확장하는 법
Orca 얘기를 걷어내고 남는 건 이거다. 받아서 쓰는 스킬에 우리 규칙을 얹어야 할 때, 본문에 손대는 대신 이렇게 하면 된다.
- 받아 온 본문은 안 건드린다. 해시가 박혀 있으면 고치는 순간 어긋나고, 스텁이면 고쳐도 안 읽힌다. 어느 쪽이든 손댈 자리가 아니다.
- 정책만 따로 스킬로 만든다. 문법·명령어는 한 줄도 베끼지 않는다. 베낀 줄은 원본이 바뀌는 날 거짓말이 된다. 대신 “정본은 여기”를 적고, “정본과 부딪히면 우리가 우선”을 적는다.
- 자동 트리거 키워드를 안 넣는다. 규칙이 말에 걸려 열리면 안 걸리는 날이 온다. 입구를 훅 하나로 만든다.
- 훅은 원본이 켜지는 순간에 붙인다. 도구 이름까지만 걸리니, 어떤 스킬이었는지 판정하는 건 스크립트가 한다. 아니면 조용히 나간다.
- 주입은 목차만. 상세는 references로 빼고 링크만 건다. 매번 전집을 밀어 넣을 이유가 없다.
- 훅이 닿지 않는 세션을 확인한다. 서브에이전트나 다른 워크트리에서 뜬 세션에는 안 붙는다. 그쪽은 지시에 전문을 실어 보내야 한다.
붙는 자리도 우리 것이고 붙는 내용도 우리 것이다.
.claude/settings.json에 등록한 16줄과 .agents/skills 아래
401줄. 받아 온 쪽에는 지문 하나 안 묻었다. 그래서 업데이트가
와도 부딪히는 게 없고, 규칙을 고칠 때 남의 파일을 열지 않는다.