Skip to content
Go back

AI 에이전트 하네스 개발기 #4 — Jira 이슈 하나로 Figma 분석부터 MR까지

#2에서 Hook, #3에서 학습 시스템을 만들었다. 파일 수정할 때마다 검증하고, 에러에서 학습하고, 메트릭을 수집한다. 그런데 여전히 에이전트한테 “뭘 해야 하는지”는 사람이 매번 알려줘야 한다. 이슈 내용을 복사해서 붙여넣고, Figma 링크를 열어서 스크린샷 찍어주고, 브랜치 이름 정해주고, MR은 직접 만들어야 한다.

이걸 /start <이슈번호> 한 줄로 줄였다.

두 개의 트랙

실제로 쓰다 보니 SDLC 워크플로우가 두 갈래로 나뉜다.

이슈 기반 — Jira 티켓이 있고 Figma 디자인이 있을 때. 대부분의 실무가 이쪽이다.

/start <이슈번호> → 이슈 조회 → Figma 분석 → 구현 계획 → 구현
/done              → 코드 리뷰 → 품질 게이트 → 커밋 → MR 생성

기획부터 — 백지에서 시작할 때. PoC나 사이드 프로젝트.

/plan → /analyze → /design → /generate → 구현 → /done

처음에는 하나로 만들었는데, 실제로 Jira + Figma가 있는 환경에서는 /plan이나 /analyze를 쓸 일이 없었다. 이슈 설명이 곧 기획이고, Figma가 곧 설계니까. 그래서 두 트랙으로 분리했다.

/start — 이슈 하나로 시작

/start <이슈번호>를 치면 이런 일이 벌어진다.

1. Jira 이슈 조회

Jira API로 이슈를 가져온다. 여기서 첫 번째 삽질이 있었다.

Jira Cloud Scoped API Token(ATATT)은 직접 호출이 안 된다. https://your-domain.atlassian.net/rest/api/3/에 curl을 날리면 401이 뜬다. api.atlassian.com/ex/jira/{cloudId}/rest/api/3/ 경로로 우회해야 한다. cloudId는 {org}.atlassian.net/_edge/tenant_info에서 가져온다.

CLOUD_ID=$(curl -s "$JIRA_BASE_URL/_edge/tenant_info" | jq -r .cloudId)
curl -s -u "$JIRA_USER_EMAIL:$JIRA_API_TOKEN" \
  "https://api.atlassian.com/ex/jira/$CLOUD_ID/rest/api/3/issue/<이슈번호>"

이걸 알아내는 데 시간을 좀 썼다. 기존 코드에서는 $JIRA_URL/rest/api/3/으로 직접 호출하고 있었는데, Scoped Token에서만 이 문제가 발생한다.

2. 티켓 검증

이슈를 가져오면 필수 항목을 체크한다.

부족하면 사용자에게 채워달라고 요청한다. 이게 없으면 에이전트가 추측으로 구현해버린다.

3. 이슈 상태 변경

“해야할 일(To Do)” 상태인 경우에만 “진행 중”으로 바꾼다. 처음에는 무조건 “진행 중”으로 바꿨는데, 이미 완료된 이슈를 다시 “진행 중”으로 돌려버리는 사고가 났다.

4. 브랜치 생성

이슈 타입에 따라 prefix가 결정된다.

이슈 타입prefix
Bugfix/
Feature / Story / Taskfeature/
Chore (설정/CI/배포)chore/

처음에는 Task를 chore/에 넣었는데, 실제로 쓰다 보니 Task 대부분이 UI 구현이었다. chore/<이슈번호>로 브랜치가 만들어지는 걸 보고 feature/로 옮겼다.

브랜치명은 이슈번호만 쓴다. feature/<이슈번호>. 예전에는 feature/<이슈번호>-login-page-ui 형태였는데, 번호만으로 충분하고 Jira에서 바로 매칭된다.

5. Figma 분석

이슈 설명에 Figma 링크가 있으면 MCP로 디자인을 읽는다. 여기서 토큰 절약이 핵심이다.

Figma MCP의 get_design_context를 무작정 호출하면 토큰이 폭발한다. 상위 Frame 하나를 통째로 가져오면 수천 토큰이 날아간다. 그래서 단계적 접근을 만들었다.

  1. get_metadata — 레이어 이름, 위치, 크기만 (경량)
  2. get_screenshot — 전체 레이아웃 시각 확인 (경량)
  3. 이 두 가지로 컴포넌트 구조를 파악한 뒤
  4. get_design_context확정된 최소 단위 노드에만 호출 (excludeScreenshot: true 기본)
  5. 400 에러 나면 하위 노드로 분할 호출

실제로 테스트해보니 기획 Figma(와이어프레임)와 디자인 Figma(시안) 두 개가 있었다. 기획에서 스펙/조건/플로우를 뽑고, 디자인에서 컴포넌트 구조/색상/간격을 뽑는다.

6. 구현 계획 → 사용자 확인

분석 결과를 기반으로 구현 계획을 제시하고, 사용자 확인을 받은 뒤 진행한다. 처음에는 에이전트가 계획까지 제시하고 바로 구현에 들어갔다. “진행할까요?”라고 물어보면서 동시에 구현을 시작하고 있었다. stop hook이 걸려서 2분 넘게 review가 돌아가는 문제까지 겹쳤다.

/done — 품질 게이트 + MR

구현이 끝나면 /done을 친다.

/code-review (1회) → 수정 → Gate 1~5 → /commit → /create-mr

코드 리뷰를 먼저 하는 이유

처음에는 Gate 5 다음에 코드 리뷰를 넣었다. 근데 코드 리뷰에서 수정사항이 나오면 Gate 1(lint)부터 다시 돌려야 하는 문제가 있었다. 무한루프 가능성도 있고.

코드 리뷰를 맨 앞에 넣으면: 리뷰 → 수정 → Gate 1~5(수정된 코드 검증) → 커밋. 코드 리뷰를 다시 돌리지 않으니 루프도 없다.

품질 게이트 5단계

Gate검증
1lint + type-check
2테스트 (실패 시 3회 self-heal 루프)
3정책 키워드(금액, 상태 전이) 변경 시 테스트 존재 확인
4의도하지 않은 파일 변경 없는지
5.env 같은 민감 파일 제외

MR 생성

/create-mr은 브랜치 prefix별로 Description을 다르게 만든다.

prefixDescription 섹션
fix/원인 · 조치
feature/구현 방법
refactor/리팩토링 이유 · 변경 사항

Description 작성 규칙도 정했다. 개조식 · 명사구 종결. “~했다” / “~합니다” 금지. 리뷰어가 30초 이내로 훑을 수 있는 분량.

그리고 MR 생성 시 이슈 상태를 “완료”로 바꾸지 않는다. MR이 머지되어야 완료인데, 머지는 리뷰어가 하는 거니까 하네스 범위 밖이다.

개별 스킬 분리

/start/done은 여러 단계를 묶어서 실행하는 파이프라인이다. 근데 실무에서는 “MR만 올리고 싶은데” 같은 상황이 자주 온다. 그래서 각 단계를 독립 스킬로 분리했다.

스킬역할
/fetch-issueJira 이슈 조회만
/branch브랜치 생성만
/figmaFigma 분석만
/lintlint + type-check만
/test테스트만
/commit커밋만
/create-mrMR 생성만
/code-review코드 리뷰만

/done = /code-review → Gate 1~5 → /commit/create-mr. 풀코스를 돌릴 수도 있고, 필요한 것만 쓸 수도 있다.

GitLab 프로젝트 자동 감지

MR을 만들려면 GitLab 프로젝트를 알아야 한다. 처음에는 .envGITLAB_PROJECT_ID를 수동으로 넣게 했는데, 매번 프로젝트 ID를 찾아야 하는 게 번거로웠다.

git remote get-url origin에서 group/project 경로를 자동 추출하는 방식으로 바꿨다. SSH(git@host:group/project.git)와 HTTPS(https://host/group/project.git) 둘 다 지원한다.

_R=$(git remote get-url origin); _R=${_R%.git}
case "$_R" in
  *://*) _NS=${_R#*://}; GL_PROJECT=${_NS#*/} ;;
  *:*)   GL_PROJECT=${_R##*:} ;;
esac

처음에 sed로 했는데 macOS의 sed가 non-greedy를 지원 안 해서 실패했다. 그리고 HTTPS URL에서 *:*/ 패턴이 https:에도 매칭되는 문제가 있어서 순서를 바꿔야 했다. 결국 sed 없이 bash 문자열 처리로 해결했다.

remote가 없는 경우(초기 세팅 직후)에는 .envGITLAB_PROJECT_ID를 fallback으로 사용한다.

시행착오

session-init에서 .env를 안 읽고 있었다

Jira API 호출이 계속 실패해서 한참 디버깅했는데, session-init hook에서 .env 파일을 source하지 않고 있었다. JIRA_BASE_URL이 비어있으니 당연히 안 된다. set -a; source .env; set +a를 추가해서 해결했다.

Stop hook이 /start 후에도 돌아간다

/start로 분석만 하고 구현 계획을 제시한 뒤 멈추려 하면, stop hook이 빌드 + 테스트를 전부 돌린다. 분석만 했는데 왜 빌드를 돌리나. “파일 변경이 없으면 stop-review를 스킵”하는 조건을 추가해서 해결했다.

커밋 메시지가 chore로 나온다

이슈 기반으로 구현했는데 커밋이 chore: initialize project로 나왔다. main 브랜치에서 직접 작업해서 이슈번호가 없었기 때문이다. /start로 브랜치를 먼저 따면 /commit이 브랜치명에서 이슈번호를 추출해서 자동으로 타입을 결정한다.

다음

워크플로우까지 완성했다. 다음 #5에서는 이 하네스를 실제 프로젝트에 적용하면서 겪은 이야기를 다룬다. init으로 기존 프로젝트에 붙이기, 실제 이슈로 /start/done 전체 사이클 돌리기, 메트릭 결과.


Share this post on:

Comments


Previous Post
AI 에이전트 하네스 개발기 #5 — 실제 프로젝트에 적용해보니
Next Post
AI 에이전트 하네스 개발기 #3 — 에이전트가 실수에서 학습하게 만들기