Agent에게 개발 이력 관리까지 맡겨보기: Azure DevOps Work Items + Skill

Kim jisu·2026년 8월 11일

TIL

목록 보기
43/43

개발하면서 생각보다 어려운 일 중 하나는 코드를 작성하는 것보다, 어떤 요청 때문에 무엇을 수정했는지 계속 기록하는 것이다.

기능 요청은 메신저로 들어오고, 실제 수정은 Git에 남고, 일정은 별도 문서에 관리하다 보면 시간이 지나면서 서로 연결이 끊긴다.

Notion에 별도 페이지를 만들어 관리할 수도 있고 Jira 같은 도구를 사용할 수도 있지만, 개인적으로는 개발 과정에서 별도의 관리 작업이 하나 더 생기는 것이 부담스러웠다.

최근 Agent를 활용하면서 이 부분에서 작은 aha moment가 있었다.

Agent가 코드를 수정하는 시점에 Work Item까지 같이 조회하고 연결하게 만들면 어떨까?

이미 프로젝트에서 사용하고 있는 Azure DevOps를 활용, Work Items와 Agent Skill을 연결해보기로 했다.


1. Azure DevOps Work Items를 개발 이력으로 사용하기

Azure DevOps에는 코드 저장소인 Repos 외에도 Work Item을 관리할 수 있는 Boards가 있다.

Task, Bug 등의 단위로 작업을 만들고 상태와 담당자를 관리할 수 있다.

나는 우선 기능 단위로 Task를 생성하는 방식으로 사용하기 시작했다.

예를 들어 아래와 같은 기능을 개발한다고 하자.

[모니터링] ET별 LLM 토큰 사용량 집계 조회 API를 추가한다.

Work Item에는 단순히 제목만 기록하지 않고 구현 목적과 주요 사양도 같이 기록한다.

목적
워크스페이스(ET)별 LLM 토큰 사용량을 집계 조회하는
모니터링 API를 추가한다.

엔드포인트
GET /api/monitoringfile/llm-usage/

주요 사양
- ET 식별
- 날짜 필터
- pagination
- total 집계
- 인증
- 테스트 케이스

이렇게 하면 나중에 코드를 다시 봤을 때 단순히

"이 API가 언제 추가됐지?"

뿐만 아니라

"왜 만들었고 당시 요구사항이 무엇이었지?"

까지 Work Item 하나에서 확인할 수 있다.


2. 문제는 Work Item을 만드는 것이 아니라 계속 연결하는 것

Work Item 자체는 새로운 기능이 아니다.

문제는 개발하다 보면 결국 관리가 귀찮아진다는 것이다.

요청 접수
   ↓
Work Item 생성
   ↓
코드 수정
   ↓
테스트
   ↓
commit
   ↓
Work Item 업데이트

처음 몇 번은 잘 기록하지만 일정이 바빠지면 Git commit만 남고 Work Item은 방치되기 쉽다.

그래서 이 과정을 Agent의 작업 흐름에 넣었다.

내가 원하는 흐름은 다음과 같다.

코드 변경
    ↓
Agent가 staged diff 확인
    ↓
열려 있는 Work Item 조회
    ↓
현재 변경과 관련된 Item 탐색
    ↓
#ID가 포함된 commit message 추천

사람이 Work Item을 찾아 Git과 연결하는 것이 아니라 Agent가 현재 변경 내용을 보고 연결 후보를 찾도록 하는 것이다.


3. Local Repository에서 Work Item 읽기 (그런데 az login부터 막혔다)

여기서 처음에는 한 가지 의문이 있었다.

Local clone repository에서 Azure DevOps Work Item을 읽을 수 있을까?

Git repository 자체에는 Work Item 정보가 들어있지 않다. 대신 Azure DevOps CLI를 이용하면 Agent가 로컬 터미널에서 직접 Boards를 조회할 수 있다.

문제는 그 첫 단계인 az login부터 실패했다는 것이다.

PermissionError: [Errno 13] Permission denied:
'C:\Users\<user>\.azure\cliextensions\bastion\azext_bastion\azext_metadata.json'

로그인 명령인데 엉뚱하게 bastion이라는 확장의 파일을 못 읽는다고 한다. 게다가 나는 WSL에서 실행했는데 에러의 경로는 Windows 경로다.

확인해보니 원인이 두 겹이었다.

which -a az
# /mnt/c/Program Files/Microsoft SDKs/Azure/CLI2/wbin/az

WSL에 Linux용 az가 없어서 PATH interop으로 Windows az가 실행되고 있었다. 그리고 그 Windows az의 확장 폴더 권한이 깨져 있었다.

ls -l ~/.azure/cliextensions/bastion/azext_bastion/
# ---------- 1 user user 73 ... azext_metadata.json

여기서 알게 된 것이 하나 있다.

az CLI는 명령 하나를 실행할 때마다
설치된 모든 확장의 azext_metadata.json을 스캔한다.

bastion 하나가 안 읽히면 명령 테이블 로딩 단계에서 전부 죽는다. 실제로 az login뿐 아니라 az account show, az devops --help도 같은 에러였다. 확장을 로드하지 않는 az version만 유일하게 동작했다.

Bastion은 이 프로젝트와 아무 관련이 없는 확장인데, 그것 때문에 CLI 전체가 멈춘 셈이다.

WSL에 Linux native az를 따로 설치하기

Windows 쪽 파일 권한을 손보는 방법도 있었지만, 어차피 Agent는 WSL에서 동작하므로 Linux용 az를 따로 설치해 ~/.azure를 분리하기로 했다.

curl -sL https://aka.ms/InstallAzureCLIDeb | sudo AZ_DIST=noble bash

여기서 AZ_DIST를 지정한 이유가 있다. Ubuntu 26.04(resolute)는 Microsoft 저장소에 아직 없다.

curl -o /dev/null -w "%{http_code}\n" \
  https://packages.microsoft.com/repos/azure-cli/dists/resolute/Release   # 404
curl -o /dev/null -w "%{http_code}\n" \
  https://packages.microsoft.com/repos/azure-cli/dists/noble/Release      # 200

배포판을 명시해주면 설치된다. az deb 패키지는 자체 Python을 번들하므로 시스템 Python 버전과 무관하게 동작한다.

설치 후에는 셸의 명령 캐시를 비워야 Windows 경로가 안 잡힌다.

hash -r
which az        # /usr/bin/az
az version      # "extensions": {}  ← 깨진 확장과 완전히 분리됨

기존 로그인은 재사용되지 않는다

Windows에서 이미 az login을 해둔 상태였기에 그대로 쓸 수 있을 줄 알았는데 아니었다.

head -c 8 ~/.azure/msal_token_cache.bin | xxd
# 01 00 00 00 d0 8c 9d df   ← Windows DPAPI 암호화 blob

Windows의 토큰 캐시는 DPAPI로 암호화되어 있어 Linux az가 복호화할 수 없다. 파일을 복사해도 소용이 없고, 한 번은 다시 로그인해야 한다.

WSL에는 브라우저를 띄울 xdg-open/wslview가 없으므로 device code 방식을 썼다.

az login --use-device-code

출력된 코드를 Windows 브라우저의 microsoft.com/devicelogin에 입력하면 끝이다. 이후로는 Linux ~/.azure에 토큰이 캐시되어 재로그인 없이 계속 쓸 수 있다.

참고로 여기서 구독(Subscription)을 고르라는 화면이 나오는데, Boards 조회에는 구독이 전혀 관계없다. az boards는 Azure 리소스가 아니라 dev.azure.com 조직에 대한 토큰만 쓰기 때문에 그냥 Enter를 눌러도 된다.

이제야 확장과 기본값을 설정할 수 있다.

az extension add --name azure-devops
az devops configure --defaults \
  organization=https://dev.azure.com/<ORG> \
  project="<PROJECT>"

그리고 WIQL로 현재 열려 있는 Work Item만 가져온다.

az boards query --wiql "
SELECT
    [System.Id],
    [System.WorkItemType],
    [System.State],
    [System.Title]
FROM WorkItems
WHERE
    [System.TeamProject] = '<PROJECT>'
    AND [System.State] NOT IN ('Closed','Removed','Done')
"

Agent 입장에서는 결국 다음 두 종류의 정보를 동시에 볼 수 있게 된다.

Local Repository
├─ git diff
├─ staged files
├─ source code
└─ commit history

Azure DevOps
├─ Work Item ID
├─ Title
├─ State
├─ Description
└─ Assigned To

이 두 정보를 Agent가 비교할 수 있다는 것이 핵심이다.


4. Agent Skill로 규칙 만들기

단순히 프롬프트로

관련 Work Item도 찾아줘.

라고 요청할 수도 있다.

하지만 반복적으로 사용할 목적이라면 Skill로 규칙을 만들어두는 편이 낫다.

현재는 대략 다음과 같은 역할로 나누었다.

---
name: azure-devops-work-items
description: >
  git 커밋 메시지를 추천할 때 또는
  Azure DevOps Work Item, Board, Backlog,
  Sprint Task, Bug, #ID가 언급될 때 사용한다.
---

Skill의 역할은 커밋 형식을 결정하는 것이 아니다.

커밋 메시지 컨벤션은 저장소마다 다르고 이미 각자의 규칙이 있다. 그래서 기존 개발 규칙과 분리해서 다음 세 가지만 담당하도록 했다.

조회
태깅
생성 제안

5. WIQL은 문서대로 써도 조용히 0건이 나온다

Work Item 조회 자체는 금방 됐지만, 쿼리를 다듬는 과정에서 세 번 막혔다.

공통점은 에러가 아니라 빈 결과가 돌아온다는 것이다.

@project 매크로가 동작하지 않는다

문서 예제를 그대로 쓰면 0건이 나온다.

WHERE [System.TeamProject] = @project    -- 0건

--project 옵션을 명시해도 마찬가지였다. 반면 @Me는 정상 동작한다. 매크로 전체가 아니라 @project만의 문제다.

WIQL은 기본이 조직 전체 범위다

더 헷갈렸던 건 이쪽이다.

--project를 줘도, REST API의 URL에 프로젝트 GUID를 넣어도 결과가 필터링되지 않는다. 프로젝트 A와 B를 각각 지정해도 똑같은 항목이 돌아왔다.

WIQL 쿼리는 조직 전체를 대상으로 하고, URL의 프로젝트는 매크로 해석용 컨텍스트일 뿐이다. 결국 스코핑은 WHERE 절에서 리터럴로 해야 한다.

WHERE [System.TeamProject] = 'KR ASR DA KSOX'    -- 이것만 실제로 필터링된다

SELECT에 없는 필드는 조용히 None이 된다

출력 투영에서 필드를 꺼내 쓸 때, 그 필드가 WIQL SELECT에 없으면 에러 없이 전 행이 None으로 채워진다.

334   None   Active        TOD
335   None   In Progress   TOD

SELECT에 넣어주면 바로 정상이 된다.

334   Test Plan    Active        TOD
335   Test Suite   In Progress   TOD

세 가지 모두 예외가 안 나기 때문에 Skill에 하드룰로 못 박아두지 않으면 Agent가 매번 다시 밟는다. 그래서 규칙 파일에 그대로 적어두었다.


6. Skill은 매번 실행된다 — 출력 크기가 곧 비용이다

이 Skill은 "가끔 쓰는 도구"가 아니라 커밋 메시지를 만들 때마다 실행되는 것이다.

그러면 조회 결과가 그대로 매번 context에 들어간다. 그래서 만들면서 제일 신경 쓴 것이 출력 크기였다.

같은 조회를 형식만 바꿔 실제로 재본 결과다.

조회형식크기
열린 항목 9건기본 JSON2,557 B
열린 항목 9건TSV 투영296 B
단건 상세전체 JSON5,171 B
단건 상세--query 투영67 B

단건 상세가 특히 인상적이었다. az boards work-item show를 그냥 부르면 Work Item 하나가 5KB다. 필요한 필드만 뽑으면 67B다. 77배 차이다.

그래서 조회는 항상 투영해서 TSV로 받게 했다.

--query "[].[fields.\"System.Id\",
             fields.\"System.WorkItemType\",
             fields.\"System.State\",
             fields.\"System.Title\"]" -o tsv

그 외에 넣은 장치들이다.

열린 항목만 조회한다. 전체는 79건이지만 닫힌 것을 빼면 9건이다. 어차피 지금 작업과 연결될 항목은 열려 있는 것뿐이다.

AND [System.State] NOT IN ('Closed','Removed','Done')

조직/프로젝트/저장소 식별자를 Skill 파일에 박아두었다. 이걸 안 하면 매 세션마다 az devops project list, az repos show 같은 탐색 명령을 먼저 돌리게 된다. 값을 적어두니 그 호출이 0회가 됐다.

파일을 둘로 나눴다.

SKILL.md      # 커밋 흐름에 필요한 것만 — 식별자, 명령 3개, 하드룰
reference.md  # 생성·수정·링크·문제해결 — 필요할 때만 읽는다

커밋 메시지를 만드는 흔한 경우에는 SKILL.md만 로드된다. Work Item을 새로 만들거나 관계를 걸 때만 reference.md를 읽으면 된다.

조회는 요청당 1회로 제한했다. 파일마다, 후보마다 다시 조회하면 같은 목록이 몇 번씩 쌓인다.

결과적으로 커밋 메시지 하나를 추천하는 데 드는 조회 비용이 300B 남짓이다. 이 정도면 매번 돌려도 부담이 없다.


7. commit message를 작성할 때 자동으로 Work Item 찾기

Agent에게 commit message 작성을 요청하면 먼저 열린 Work Item을 조회한다.

az boards query \
  --wiql "SELECT [System.Id],[System.WorkItemType],[System.State],[System.Title]
          FROM WorkItems
          WHERE [System.TeamProject] = 'KR ASR DA KSOX'
          AND [System.State] NOT IN ('Closed','Removed','Done')" \
  --query "[].[fields.\"System.Id\",
               fields.\"System.WorkItemType\",
               fields.\"System.State\",
               fields.\"System.Title\"]" \
  -o tsv

그리고 staged 변경과 비교한다.

예를 들어 현재 변경사항이

monitoringfile/
├─ views.py
├─ serializers.py
├─ urls.py
└─ tests.py

이고 Work Item에 다음 Task가 있다면,

#472
[모니터링] ET별 LLM 토큰 사용량 집계 조회 API를 추가한다.

Agent가 다음처럼 추천한다.

#472 [모니터링] ET별 LLM 토큰 사용량 집계 조회 API를 추가한다. (To Do)

feat(monitoringfile): ET별 LLM 토큰 사용량 조회 API 추가 #472

태깅 근거:
monitoringfile의 API, 집계 로직 및 테스트 추가가 #472의 구현 범위와 일치

중요한 것은 Agent가 임의로 가장 비슷한 Item을 붙이지 않도록 하는 것이다.

매칭이 불확실하면 후보를 보여주게 했다. (아래는 설명을 위한 가상의 예시다.)

후보

#472 [모니터링] ET별 LLM 토큰 사용량 집계 조회 API 추가
#465 [모니터링] Pilot User 사용량 조회 기능 추가

그리고 사람이 최종 선택한다.


8. Work Item과 실제 Commit 연결

이 방식의 가장 만족스러운 부분은 Work Item이 단순 Task 목록으로 끝나지 않는다는 점이다.

현재 사용하는 Azure DevOps 조직에서는 commit message에 Work Item ID를 넣어 push하면 실제 Work Item의 Development 영역에서 해당 commit을 확인할 수 있었다.

예를 들어:

feat(monitoringfile): ET별 LLM 토큰 사용량 조회 API 추가 #472

로 commit하면 #472에서 실제 구현 commit을 추적할 수 있다.

화면으로만 확인하면 찜찜해서 CLI로도 확인했다. push 후 Work Item에 관계가 실제로 생긴다.

az boards work-item show --id 472 --expand relations \
  --query "relations[].{name:attributes.name,url:url}" -o json
[{ "name": "Fixed in Commit",
   "url": "vstfs:///Git/Commit/{project}%2F{repo}%2F{sha}" }]

반대로 commit에서 Work Item을 찾을 수도 있다. commits/{sha}/workitems 같은 엔드포인트는 존재하지 않고, artifact URI로 역조회해야 한다. short SHA는 매칭되지 않으니 full SHA를 써야 한다.

az rest --method post --resource "499b84ac-1321-427f-aa17-267ca6975798" \
  --url "https://dev.azure.com/<ORG>/_apis/wit/artifacturiquery?api-version=7.1" \
  --body '{"artifactUris":["vstfs:///Git/Commit/{project}%2F{repo}%2F{full_sha}"]}'

참고로 GitHub 연동이 아니라 Azure Repos이므로 AB# 접두어 없이 #472로 충분하다.

결과적으로 하나의 기능에 대해 다음 정보가 연결된다.

Work Item #472
│
├─ 왜 개발했는가
│   └─ Description
│
├─ 무엇을 구현해야 하는가
│   └─ 주요 사양
│
├─ 누가 담당하는가
│   └─ Assigned To
│
├─ 현재 어디까지 됐는가
│   └─ State
│
└─ 어떤 코드로 구현됐는가
    └─ Commit

이 구조가 생기니 과거 기능을 추적하기가 상당히 편해졌다.


9. Work Item이 없으면 생성도 Agent가 제안한다

모든 코드 변경에 기존 Work Item이 존재하는 것은 아니다.

그런 경우 Agent가 바로 Task를 생성하게 하지는 않았다.

대신 다음처럼 생성 제안만 하게 했다.

매칭 없음

생성 제안:
[Task] [모니터링] 사용자별 LLM 사용량 조회 API를 추가한다.

사용자가 승인하면 생성한다.

az boards work-item create \
  --title "[모니터링] 사용자별 LLM 사용량 조회 API를 추가한다." \
  --type Task \
  --assigned-to <USER> \
  --area "<AREA>" \
  --description "<HTML>"

이 부분은 의도적으로 Human-in-the-loop로 남겨두었다.

Agent에게 무조건적인 Work Item 생성 권한을 주면 작은 수정 하나마다 Task가 만들어져 오히려 관리 비용이 증가할 수 있기 때문이다.


10. 자동화한다고 모든 권한을 주지는 않았다

Agent 자동화를 사용하면서 중요하게 보는 것은 할 수 있는 것과 해도 되는 것을 구분하는 것이다.

그래서 Skill에 몇 가지 hard rule을 넣었다.

조회는 자유롭게

Work Item 검색
Work Item 상세 조회
상태 확인

생성은 사용자 승인 후

Task / Bug 생성

Git 변경 작업은 하지 않음

git commit
git push
git fetch

특히 commit과 push는 Agent가 추천만 하고 직접 실행하지 않도록 했다.

git fetch까지 막은 것은 정책 때문만은 아니다. WSL의 git에는 Azure DevOps 자격증명이 없어서 애초에 실패한다.

fatal: could not read Password for 'https://<org>@dev.azure.com': No such device or address

push는 Windows 터미널에서 해야 하므로, Agent가 원격 git 명령을 시도하는 것 자체가 의미가 없었다.

결과적으로 역할은 다음처럼 나뉜다.

Agent
├─ 변경사항 분석
├─ Work Item 조회
├─ 관련 Item 추천
├─ commit message 추천
└─ 신규 Task 생성 제안

Developer
├─ 매칭 결과 판단
├─ Work Item 생성 승인
├─ commit
└─ push

단순 자동화보다 개인적으로는 이런 구조가 훨씬 안정적이었다.


11. Board까지 연결하면 밀린 요청이 보이기 시작한다

Work Item을 계속 생성하기 시작하면 자연스럽게 Board도 활용할 수 있다.

To Do
   ↓
In Progress
   ↓
Done

코드에서는 Git history를 보면 구현된 것을 알 수 있지만,

지금 들어와 있는 요청이 몇 개인지
어떤 기능이 대기 중인지
무엇이 진행 중인지

는 Git만으로 파악하기 어렵다.

Work Item을 개발 단위로 유지하면 이 정보를 Board에서 바로 확인할 수 있다.


12. 결국 좋았던 것은 Azure DevOps 자체가 아니었다

처음에는 Azure DevOps Boards를 잘 활용하는 방법에 대한 고민이었다.

하지만 실제로 사용해보면서 느낀 핵심은 조금 달랐다.

Agent가 개발 과정과 프로젝트 관리 도구 사이의 수작업을 대신할 수 있다는 점이었다.

기존에는 서로 떨어져 있었다.

메신저
   ↓
요구사항

Azure Boards
   ↓
Task

Git
   ↓
Code / Commit

Agent를 중간에 넣으면 연결할 수 있다.

                 ┌─ Azure DevOps Work Item
                 │
요구사항 → Agent ├─ Source Code
                 │
                 └─ Git Commit

Agent에게 코딩만 시키는 것이 아니라,

지금 하고 있는 개발이 어떤 작업의 일부인지 계속 인지하게 만드는 것

에 가깝다.

이 부분이 개인적으로 이번에 가장 큰 aha moment였다.


마치며

현재 사용 방식은 아직 단순하다.

1. 기능 요청을 Work Item으로 관리한다.
2. Agent가 열린 Work Item을 조회한다.
3. staged diff와 Work Item을 비교한다.
4. commit message에 관련 #ID를 추천한다.
5. Work Item이 없다면 생성을 제안한다.
6. 최종 판단과 commit/push는 사람이 한다.

돌아보면 정작 시간이 많이 든 곳은 Agent 쪽이 아니라 그 앞단이었다. WSL과 Windows에 az가 섞여 있는 환경을 정리하는 데 한참이 걸렸고, WIQL은 문서대로 써도 조용히 0건을 뱉었다. 그리고 매번 실행되는 Skill이라 조회 출력을 어떻게 줄일지가 생각보다 중요한 설계 문제였다.

그래도 이 정도만으로 별도의 개발 일지를 작성하지 않고

요구사항 → 구현 → commit → 진행 상태

를 하나의 흐름으로 연결할 수 있었다.

앞으로는 PR, 배포 Pipeline, Work Item 상태 변경까지 연결해보면 개발 이력을 더 자연스럽게 관리할 수 있을 것 같다.

Agent 활용에서 코드 생성 자체보다 이런 개발 과정의 작은 수작업들을 없애는 것이 오히려 장기적으로 더 큰 생산성 향상을 만들 수도 있겠다는 생각이 든다.

profile
Dreamer

0개의 댓글