gadak 0.21: 자리를 비운 사이 무슨 일이 있었나
월요일. 트래커를 연다. 목록은 늘 보던 그대로다. 이슈 마흔 개, 같은 열, 같은 색. 주말 사이 뭔가 움직였다는 것은 안다. 리뷰어가 코멘트를 남겼고, 막고 있던 이슈 하나가 닫혔고, 누군가 내 이슈 둘의 담당자를 바꿨다. 목록은 그중 어느 것인지 말해 주지 않는다. 그걸 말해 줬을 알림은 금요일 저녁에 도착했고, 나는 금요일 저녁에 그것을 지웠다. 금요일 저녁의 알림은 원래 그렇게 되는 법이다.
트래커는 알고 있었다. 그 사건 하나하나가 히스토리에 있다. 다만 내가 묻고 있던 질문에 맞는 문장이 없었을 뿐이다. 내 질문은 “프로젝트의 상태가 어떤가”가 아니라 “내가 마지막으로 본 뒤로 뭐가 바뀌었나”였다. 트래커는 상태를 저장한다. 돌아온 사람에게 필요한 것은 변화다.
이 글은 그 틈에 대한 이야기이고, 그 틈의 일부를 메운 릴리스에 대한 이야기다. 0.18 에세이는 데이터가 어디에 있어야 하는지를 다뤘다. API 뒤인가, 내 기계의 파일 안인가. 이번 글은 언제에 대한 것이다. 히스토리를 줄곧 쌓아 온 미러가, 내가 돌아온 그 순간에 무엇을 말해 줄 수 있는가.
미러는 이미 알고 있다
gadak은 트래커를 로컬 SQLite 파일 하나에 미러링한다. 그 트래커가 Jira든 Linear든 gadak에 들어 있는 내장 트래커든 마찬가지다. 미러에는 모든 이슈의 변경 기록과 모든 코멘트가 있고, 그 옆의 두 번째 파일에는 내가 어떤 이슈를 언제 열었는지가 남는다. 이 중 새로운 것은 하나도 없다. 첫 동기화 때부터 거기 있었다.
0.21이 더한 것은 수집기가 아니다. 그 데이터에 드디어 던지기 시작한 질문들, 그리고 모든 답에 그것을 어떻게 계산했는지 정의를 나란히 찍는다는 규칙이다. 이 릴리스의 어느 부분도 네트워크에 손을 뻗지 않고, 새로 기계 밖으로 나가는 것도 없다. 아래 전부가 읽기다.
돌아와서 묻는 세 질문
뭐가 바뀌었나. 목록이 흐린 한 줄로 열린다. 지난 세션 이후 3시간 · 이슈 7건 변경 · 그중 이 계정 배정 2건. 기준은 자정도 아니고 “마지막 로그인 이후”도 아니다. 내 이전 세션이다. 읽기와 읽기 사이의 간격이 30분을 넘지 않는 한 묶음. 이슈를 열면 상세 패널이 같은 일을 더 작은 규모로 한다. 마지막 열람 3일 전 — 상태 변경 2건, 새 코멘트 1건. 이 줄은 페이지가 열릴 때 한 번 고정되고, 일하는 동안 숫자가 올라가지 않는다. 의도한 것이다. 올라가는 숫자는 알림이고, 도착해서 한 번 읽는 줄은 인지다. 우리가 원한 것은 뒤쪽이었다.
이건 얼마나 묵었나. 트래커는 나이를 고정된 숫자로 답한다. 72시간, 그
뒤로는 행이 호박색이 된다. 72시간은 누구의 숫자도 아니다. 누군가 설정
파일에 적어 넣은 기본값이다. 0.21은 그 기준을 워크스페이스 자체에서
배운다. 최근 90일간 팀이 완료한 이슈, 그중 재오픈된 적 없는 것들의 사이클
타임 85번째 백분위수. 팀이 대부분을 이틀에 닫는다면 나흘째 진행 중인 이슈는
이미 이례적이고, 표시는 그렇게 말한다. 3주에 닫는 팀이라면 표시는 3주 동안
조용하다. 이슈의 기간을 보여 주는 칩에 마우스를 올리면 팀의 p85가 보이고,
CLI의 list·ready·next에는 age_days가 붙어서 에이전트도 사람과 같은
숫자를 본다.
이번 주는 빨라졌나. gadak retro는 표 하나를 찍는다. 열은 ISO 주 단위.
행은 세션 수, 도착해서 첫 쓰기까지 걸린 시간, 진행 중인 일의 나이(p85와 가장
오래된 것 하나), 닫힌 이슈 수와 걸린 시간(중앙값과 p85), 그리고 불일치.
불일치는 잠시 뒤에 따로 다룬다. 표 아래에는 모든 행의 정의가 매번 찍힌다.
--open은 어느 칸이든 그 뒤의 이슈들을 앱에 띄우니, 숫자가 막다른 길이
되는 일은 없다. 표가 주 단위인 이유는 둘이다. 이 중 어느 것도 일주일보다
짧은 구간에서는 의미가 없고, 회고는 팀이 이미 기억에 의지해 이 질문들을 하고
있는 유일한 회의라서다.
미러만 만들 수 있는 행
그 표의 마지막 행이 불일치다. 일이 끝났다고 말하는 코멘트가, 상태는 아직 끝나지 않았다고 말하는 이슈에 달려 있는 경우. “머지했으니 닫습니다.” “릴리스 브랜치에 반영.” “완료했습니다.” 코멘트는 사람이 본 진실이고, 상태는 트래커가 기록한 진실이며, 둘이 어긋난다.
Jira는 이 행을 만들 수 없다. 코멘트 본문과 상태는 서로 다른 엔드포인트에 살고, JQL에는 조인이 없다. 미러는 둘을 한 파일에 갖고 있으니 질문은 쿼리 하나다. 열린 상태 아래 끝났다는 투의 코멘트를 패널이 감지하면, 조용한 버튼 하나를 내놓는다. 완료로 이동. 마우스를 올리면 그 버튼을 만든 사실이 보인다. 마지막 코멘트는 완료를 말하는데 상태는 아직 In Review. 한 번 닫으면 그 이슈에서는 다시 나오지 않는다.
이 휴리스틱의 첫 판은 틀린 채로 나갔다. 부분 문자열로 맞췄기 때문에 “abandoned” 안에 “done”이 있었고, “unresolved” 안에 “resolved”가 있었고, “미완료” 안에 “완료”가 있었다. 신호가 거꾸로 향했다. 정확히 아직 안 끝났다고 말하는 코멘트들을 가리킨 것이다. 수정은 파서가 아니라 가드다. 영어 단어는 홀로 서야 하고, CJK 완료 단어 앞뒤에 부정 접두·접미가 붙으면 안 되고, 인용문과 코드 펜스는 벗겨 내고, 질문은 제외하고, 마지막 상태 변경보다 새로운 코멘트만 센다. 열아홉 행짜리 테스트 표가 옛 규칙에서 먼저 실패한 뒤에 새 규칙에서 통과했다. retro의 그 행은 자기 정의 안에 휴리스틱이라고 적어 두었고, 그 말은 진심이다.
만들지 않기로 한 것
이 신호들 하나하나가, 우리가 원하지 않는 기능에서 한 걸음 거리였다.
점수 없음. retro는 이번 주 닫힌 이슈와 사이클 타임을 보여 주지만 사람을 순위 매기지 않고, 사람별 열도 없다. 사람에 대한 판정으로 읽히는 피드백은 일에 대한 피드백보다 행동을 덜 바꾸고, 바꿀 때는 나쁜 쪽으로 바꾸기 쉽다. 오래되고 튼튼한 연구 결과이고, gadak이 찍는 모든 문장의 주어가 이슈이지 읽는 사람이 아닌 이유다.
알림 없음. 0.21의 어느 것도 끼어들지 않는다. 세션 줄, 재개 카드, 나이 표시, 완료로 이동 버튼은 전부 다음에 시선이 갈 자리에서 기다리고, 할 말이 없으면 아무 말도 하지 않는다. 조용한 아침에는 세션 줄이 아예 나타나지 않는다.
고정 SLA를 규칙으로 삼지 않음. 정체 기준은 표본이 충분하기만 하면 언제나 워크스페이스 자체의 분포다. 최근 90일간 완료가 열한 건이 되기 전까지는 72시간으로 물러나고, 설정 화면이 그 사실을 그대로 적어 둔다. 고정된 숫자는 정의가 아니라 폴백이다.
밖으로 나가는 것 없음. retro도, 세션 줄도, 학습된 기준도 전부 디스크의 두 파일에서 계산된다. 어떤 집계도 어디로 보내지지 않고, 보낼 계정 자체가 없다.
에이전트의 한 줄
한 가지 변경이 더 이 글에 속한다. 텍스트 한 줄짜리지만. 코딩 에이전트가
gadak을 거쳐 Jira Cloud나 Linear에 무언가를 쓰면, 코멘트든 상태 전환이든 새
이슈든, 그 끝에 이제 — via gadak · 이름이 붙는다.
이유는 지루하고 중요하다. 그 origin들에서 에이전트는 사람의 자격증명으로
쓰고, 트래커는 둘을 구분하지 못한다. 0.21 전에는 에이전트가 남긴 코멘트가
내가 친 것과 똑같이 보였다. 틀리기 전까지는 괜찮다. 틀리고 나면 누구의
실수인지 아무도 모른다. 에이전트를 트래커 안에 들일 팀이라면 먼저 그 안에서
에이전트가 보여야 한다. 이 한 줄은 그 일을 하는 가장 작은 것이고, 팀이 달리
정했다면 actor.trailer = false로 뗀다.
지는 자리
지난 글의 정직 규칙은 여기에도 적용된다. 숫자가 보이는 것보다 약한 자리들.
이 글의 모든 숫자는 워크스페이스 하나에서 나왔다. 내 것이다. gadak의 백로그는 gadak 위에서 돈다. 실제 주가 쌓인 실제 워크스페이스이지만, 계측기를 만든 사람이 적은 데이터 점 하나이기도 하다. 내가 0.21에 걸어 둔 성공 조건은 기능 목록이 아니라 문장 하나였다. 내가 아닌 누군가가, 자기 미러의 숫자를 들고, 자기 말로, 돌아오는 비용이 줄었다고 말하는 문장. 그 문장은 아직 없다. 생기면 바꿔 쓰지 않고 그대로 인용할 것이다. 그때까지 이 릴리스는 미러가 무엇을 계산할 수 있는지에 대한 주장이고, 그것이 당신에게 무엇을 해 주는지에 대한 주장은 아니다.
불일치 행은 휴리스틱이다. 단어 경계와 부정 가드로 끌어올린 정밀도는, 한 번 닫으면 되는 버튼에는 공정한 거래다. 사람에 대한 보고서에 넣을 숫자로는 모자란다. 행 아래의 정의가 그렇게 말한다.
Linear에는 변경 기록이 없다. API가 이슈의 착수·완료 시각을 주니 흐름 열은 거기서 채워지는데, 그 이슈가 다음에 움직일 때에야 채워진다. 기존 Linear 미러는 그때까지 대시를 보인다.
30분은 추측이다. 세션 경계는 기본값이 있는 매개변수고, 5분과 하루 사이로 묶여 있으며, 어느 값이 그 행을 만들었는지 알 수 있게 표 아래에 찍힌다.
두 글을 합치면
0.18 에세이의 주장은 하나였다. 데이터를 파일에 넣으면, API가 물을 수 없던 질문이 쿼리 하나가 된다. 이 글은 두 번째 주장을 한다. 히스토리가 그 파일에 있으면, 돌아온 순간의 질문에도 거기서 답할 수 있다. 뭐가 바뀌었나, 이건 얼마나 묵었나, 이번 주는 괜찮았나. 새로 모으는 것 없이, 옆에 정의가 없는 숫자 없이.
두 주장 어느 쪽도 참이 되기 위해 에이전트가 필요하지 않다. 둘 다 에이전트가 방에 있으면 더 날카로워진다. 같은 미러를 읽는 에이전트는 같은 세션 줄, 같은 나이, 같은 p85를 보고, 같은 숫자로 답하기 때문이다.
써 보기
gadak retro # 주간 표, 아래에 정의
gadak retro --open closed --week 1
gadak list # 미해결 이슈, 우선순위 옆에 age_days
라이브 데모는 같은 UI를 534건 픽스처 위에서 브라우저에 띄운다. 세션 줄도 거기 있다. 신호 하나하나가 어떤 연구에 기대고 있고 그것으로 무엇을 하지 않기로 했는지는 THEORY.md에 있다. 행의 정의는 DERIVE.md에, 자기 미러에 바로 돌려 볼 수 있는 SQL로 적혀 있다.