에이전트의 작업 완료 기준과 인수인계 형식 정하기

X Facebook

Written by

in

설계·구현·테스트를 하나씩 맡겨 서브에이전트 셋을 순서대로 띄웠다. 첫 번째가 done을 돌려주자 두 번째가 그 결론을 바탕으로 구현했고 세 번째가 테스트했다. 테스트에서 문제가 나와 원인을 찾다 보니 첫 번째로 돌아가야 했다. 두 번째와 세 번째 작업도 함께 되돌렸다.

이 작업 방식에서는 다음 담당자가 앞선 작업의 근거를 인수인계서로 확인해야 한다. 에이전트끼리 직접 대화하지 않고 작업이 끝나면 세션도 종료되기 때문이다. 인수인계서의 done을 작성자와 수신자가 같은 의미로 이해할 수 있어야 했다.

curvez는 그 인수인계서를 핸드오프라고 부른다. 형식을 정해 둔 것이 agent-contract 스킬이다. done에 검증을 붙였다는 얘기는 예전에 한 번 적었다. 그때는 규칙만 설명했으니 이번에는 계약에 실제로 무엇을 담는지 보려고 한다.

이 저장소의 .curvez/handoff/에 실제로 오간 파일 세 건이 남아 있다. 먼저 그걸 열어 봤다.

파일 크기 verification status
curvez-architect.20260907-150907.json 2,642B 6항목 done
curvez-designer.20260907-162648.json 5,386B 8항목 partial
curvez-nextjs.20260907-164829.json 6,383B 8항목 partial

표 1. 실제로 오간 핸드오프 세 건

셋 다 최상위 키가 같은 여덟 개다 — from·to·status·summary·artifacts·decisions·blocked_on·verification. 형식이 정해져 있으니 python3 -c "json.load(...)" 한 줄로 열어 필드를 그대로 꺼낼 수 있다.

verification 안에는 이런 것들이 들어 있다. 셋 다 명령 문자열과 그 출력이 한 쌍으로 적혀 있다.

grep -cE '^\| ARCH-[0-9]{3} \|' .curvez/architecture.md   →  6
pnpm lint                                               →  exit 0, 0 errors 0 warnings
find .curvez/design/screens -name '*.md' | wc -l        →  screens=3

세 건 중 done은 하나뿐이고 둘은 partial이다. 형식이 done을 기본값으로 두지 않아서 그렇다.

1. 핸드오프는 보고서가 아니다

핸드오프는 보고서 같은 것으로 보기 쉽다. 사람이 읽고 다음을 정하는 요약문. 그런데 스킬 문서에는 그렇게 쓰지 말라고 따로 적혀 있다.

핸드오프는 curvez 팀 실행의 유일한 통신 수단이다. 에이전트끼리 실시간으로 대화하지 못하므로, 파일에 남긴 계약이 곧 API 다.

사용자에게 하는 최종 보고는 이 계약으로 쓰지 않는다. 핸드오프는 프로그램이 읽는 계약이고 사용자 보고는 사람이 읽는 문장이다. 둘을 섞으면 계약에 서사가 끼어들어 프로그램이 값을 꺼내다 실패한다. 반대로 보고에는 형식이 끼어들어 사람이 못 읽게 된다.

2. 계약에는 무엇이 적혀 있나

파일을 어디에 두는지부터 정해져 있다. 경로 형식이 이렇다.

.curvez/handoff/<from>.<timestamp>.json

<from>은 만든 에이전트의 name 그대로다. <timestamp>는 YYYYMMDD-HHmmss 다. curvez-architect.20260816-141203.json 같은 이름이 된다. 같은 에이전트가 다시 돌아도 앞 파일을 덮어쓰지 않는다. 회고 에이전트가 이 디렉터리를 시간순으로 읽어 실행 이력을 재구성해서다. 덮어쓰면 무엇이 언제 어긋났는지 복원할 수 없다.

내용은 JSON으로 적는다. 이렇게 생겼다.

{
  "from": "curvez-architect",
  "to": ["curvez-nextjs"],
  "status": "done",
  "summary": "DDD 4레이어로 확정. 도메인은 프레임워크 import 금지.",
  "artifacts": [
    {
      "path": ".curvez/architecture.md",
      "kind": "decision",
      "note": "레이어 경계 규칙 포함"
    }
  ],
  "decisions": [
    {
      "what": "application 레이어를 별도로 두지 않고 domain 에 유스케이스를 합쳤다",
      "why": "화면이 12개 이하라 레이어 하나를 더 두면 파일만 늘고 경계는 안 생긴다",
      "reversible_at": ".curvez/architecture.md:레이어 정의"
    }
  ],
  "blocked_on": [],
  "verification": [
    {
      "command": "node scripts/validate-handoff.mjs",
      "result": "오류 0개",
      "passed": true
    }
  ]
}

인수인계서로 치면 각 칸이 이런 것들이다.

필드 담는 것
from · to 누가 써서 누구에게 넘기는가
status 어디까지 끝났는가. done · partial · blocked 셋 중 하나
summary 무엇을 했는지 한 줄
artifacts 실제로 만들거나 고친 파일 목록
decisions 무엇을 왜 그렇게 정했는가
blocked_on 답을 못 얻어 멈춘 질문. 없으면 빈 배열
verification 실제로 돌린 명령과 그 출력

표 1. 핸드오프 JSON의 일곱 필드

to는 최소 한 개다. 다음 담당이 안 정해졌으면 ["curvez-orchestrator"]로 돌려준다. 빈 배열은 허용하지 않는다. 받는 쪽이 없으면 아무도 안 읽고 다음 라운드에서 같은 작업이 다시 돈다.

artifacts의 kind는 열 개로 정해져 있다 — decision code doc test spec research review retro commit pr. 커밋은 path에 git:<40자 해시>를 넣는다. PR은 URL 전문을 넣는다. 다음 회고에서 이전 회고를 찾을 때 프로젝트의 모든 문서를 훑지 않도록 retro와 doc도 구분했다.

decisions 안에서 실제로 쓸모가 있는 필드는 reversible_at이다. 파일:섹션 또는 파일:라인으로 적는다. 이게 없으면 나중에 그 결정을 뒤집을 때 전체를 다시 읽어야 한다. 대신 아무거나 다 남기지는 않는다. 파일명을 kebab-case로 했다 같은 관례적 기본값은 안 남긴다. 전부 남기면 아무도 안 읽는다.

3. 세 값 중 하나를 고르게 했다

status에 쓸 수 있는 값은 아래 셋뿐이다.

status 조건 강제 규칙
done 맡은 범위를 끝냈고 검증까지 마쳤다 verification 최소 1건. blocked_on이 비어야 한다
partial 일부만 끝냈다. 남은 것을 알고 있다 무엇까지 됐고 무엇이 남았는지 summary에 적는다
blocked 답 없이는 더 못 간다 blocked_on 최소 1건

표 2. status 세 값의 조건과 강제 규칙

done 아니면 실패, 둘이면 충분해 보이지만 blocked도 정상 상태로 다룬다. blocked를 받은 오케스트레이터는 사용자에게 묻거나 다른 에이전트에게 돌린다. 추측으로 메운 done은 아무도 못 잡아내고 나중에 되돌리는 비용이 훨씬 크다.

blocked_on에 적는 질문에도 형식이 있다. { "question": "주문 취소 가능 기간이 며칠인가", "who": "user" }처럼 답을 줄 주체를 같이 적는다. who는 user이거나 에이전트 name이다. 질문은 답하면 바로 진행 가능한 형태로 적어야 한다. “요구사항이 불명확하다” 고만 적어서는 답할 수 없다.

판정이 애매한 자리는 스킬에 예시로 박혀 있다.

상황 status
구현 끝, pnpm typecheck 0 errors done
구현 끝, 테스트는 못 돌림 partial
화면 3개 중 2개 구현 partial
API 응답 형식을 모름 blocked
앞 단계 결정에 이의 있음 blocked

표 3. 판정이 애매한 자리의 status 예시

4. “통과”는 왜 검증이 아닌가

verification은 실제로 돌린 명령과 그 결과를 옮겨 적는 자리다. 받는 쪽은 "통과" 두 글자로 아무것도 판정하지 못한다. 그래서 요약하지 말고 판정 가능한 값으로 적으라고 되어 있다.

좋음 나쁨
{ "command": "pnpm typecheck", "result": "0 errors" } { "command": "타입 확인", "result": "통과" }
{ "command": "pnpm test -- order", "result": "12 passed, 0 failed" } { "command": "pnpm test", "result": "정상 동작" }

표 4. verification에 적는 좋은 예와 나쁜 예

오른쪽 첫 줄은 명령을 재현할 수 없고 “통과”만으로는 판정 근거를 알 수 없다. 두 번째 줄은 더 나쁘다. 몇 개가 돌았는지가 없으니 0개 실행도 “정상 동작”이 되어 버린다.

실패한 검증도 그대로 적는다. { "command": "pnpm test", "result": "10 passed, 2 failed (order.spec.ts)", "passed": false }를 적는다. 그리고 status를 partial이나 blocked로 내린다. 실패를 숨기고 done 하는 것이 계약 위반이라고 못박혀 있다.

이 규칙은 검증 스크립트로도 확인한다. 핸드오프 파일을 읽고 규칙을 어긴 항목을 오류로 출력한다.

node "$CLAUDE_PLUGIN_ROOT/scripts/validate-handoff.mjs" .curvez/handoff/

무엇을 막는지 보면 이렇다.

  • done 인데 verification이 비었다 → 오류. 아무것도 안 돌려 보고 끝났다고 한 것이다
  • blocked 인데 blocked_on이 비었다 → 오류. 무엇을 물어야 하는지가 없다
  • done 인데 blocked_on이 남아 있다 → 오류. 끝났는데 답을 기다린다는 말이 된다
  • done 인데 artifacts와 decisions가 둘 다 비었다 → 경고. 남긴 게 없는 완료인지 확인하라고 한다

정해진 목록에 없는 최상위 키도 오류다. 왜 막는지는 스크립트 주석에 이유가 있다. 오타 난 키는 조용히 무시된다. 쓴 쪽은 데이터를 남겼다고 믿는다. 읽는 쪽은 그것을 못 본다.

5. 받은 쪽은 무엇부터 보나

쓰는 절차만 정해 두면 반쪽이다. 읽는 순서도 정해져 있다.

  1. status를 먼저 본다. blocked 나 partial이면 그 전제 위에서 작업을 시작하지 않는다
  2. blocked_on을 본다. who가 내 name 인 질문이 있으면 그것부터 답한다
  3. decisions를 본다. 내 작업과 충돌하는 결정이 있어도 뒤집지 않고 blocked_on에 이의를 남긴다
  4. artifacts의 파일을 실제로 읽는다

3번을 지켜야 하는 건 앞 단계의 결정을 뒤에서 조용히 뒤집으면 두 산출물의 전제가 서로 달라지고 어느 쪽이 맞는지 판정할 근거가 사라지기 때문이다. 그래서 이의는 남기되 뒤집지는 않는다.

제일 어기기 쉬운 건 4번이다. 한 줄짜리 summary를 읽는 것과 달리 artifacts는 파일 여러 개를 열어야 한다. 스킬에도 “summary만 읽고 진행하지 마라”라고 한 줄 따로 박혀 있다.

6. 도입 비용과 한계

무엇을 내주는가 어떻게 나타나는가
에이전트 한 번 실행이 길어진다 자기 검증 명령을 실제로 돌리고 출력에서 판정 가능한 부분을 옮겨 적어야 한다
파일이 계속 쌓인다 덮어쓰지 않으니 라운드마다 .curvez/handoff/가 늘어난다. 오래된 것을 언제 지우는지는 스킬에 적혀 있지 않다
검증기는 형태만 본다 result에 "0 errors"라고 적혀 있으면 통과시킨다. 그 명령을 실제로 돌렸는지는 판정하지 않는다

표 5. 이 계약을 쓰면서 내주는 것

이 계약으로 확인할 수 있는 범위는 마지막 줄까지다. 검증기는 “검증을 적었는가”까지만 보고 “적힌 게 사실인가”는 못 본다. 그 자리를 무엇으로 메우는지는 확인하지 못했다.

7. done에 검증 결과를 포함하기

done의 뜻이 “맡은 일을 마쳤다”에서 “돌려서 나온 값을 여기 적어 뒀다”로 좁혀졌다. 그 뜻을 안 지킨 파일은 검증기를 통과하지 못하게 해 뒀다.

Comments

답글 남기기

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