트러블슈팅

증상에서 시작해 원인을 찾아가는 순서로 정리했습니다. 작업이 막힌 사람에게 보이는 것은 "아무 일도 안 일어난다"라는 증상이고, "헤드리스 토큰이 만료됐다"라는 원인은 아직 모르는 상태이기 때문입니다.

어느 증상이든 화면에서 볼 자리가 먼저입니다. 앱이 이미 판정해서 화면에 표시한 내용을 터미널에서 다시 조회할 이유가 없습니다. 화면에서 원인을 확인하지 못했을 때 비로소 로그를 엽니다. 로그는 워커와 세션이 자기가 한 일을 시각과 함께 한 줄씩 적어 둔 파일입니다.

아래 회색 칸의 명령은 앱이 대신 하지 않는 확인 작업이라, 터미널에 직접 입력하시면 됩니다. 명령 안의 <루트>는 큐 루트, 그러니까 프로젝트 폴더 안의 .dira입니다.

시계 뒤에 멈춤:이 붙어 있다

화면: 화면 맨 아래 status bar에서 다음 디스패치 시계 아이콘 바로 뒤를 보세요. 디스패치가 멎었으면 그 자리에 멈춤: <이유> 한 줄이 붙습니다. 멎었다는 말은 idle 워커가 하나 이상 있는데, 아무도 가져가지 않은 열린 티켓도 한 장 이상 남아 있다는 뜻입니다. 열린 티켓이 없으면 일이 없을 뿐이라 아무것도 붙지 않습니다. 이 표시는 보드가 5초마다 새로 읽어 오므로, 앱을 껐다 켜야 바뀌는 것이 아닙니다.

여러 이유가 동시에 걸려도 한 줄에는 하나만 나옵니다. 아래 표에서 위에 있는 이유가 먼저입니다. 그 한 줄을 누르면 팝오버가 열립니다. 첫 줄에는 고른 이유와 할 일이 있고, 그 아래에 idle 워커마다 <이름> - <그 워커의 한 줄>이 한 줄씩 있습니다. 워커 줄을 펼치면 그 판정의 근거가 된 원문, 곧 runner.log에 그 워커가 남긴 줄이 그대로 보입니다. 워커를 하나씩 열어 로그를 읽지 않아도 됩니다. 감시자가 스스로 못 고쳐서 사람에게 넘긴 문제가 있으면 맨 아래 감시자 묶음에 따로 나옵니다.

원인과 할 일: 표의 {} 자리에는 화면에서 실제 값이 들어갑니다.

순서키멈춤: 뒤의 한 줄할 일
1migration마이그레이션 미적용 {id}앱을 다시 열면 이어서 적용합니다. 앱 없이 돌릴 때는 python3 migrate.py apply --root <루트>를 실행합니다 (업데이트한 뒤 화면이 흐려졌을 때)
2apm패키지 설치 상태가 lock과 다릅니다 {detail}python3 dira_apm.py install <프로젝트 루트>를 실행해 lock대로 다시 설치합니다. 그래도 다르면 .dira/team/apm.lock.yaml을 확인합니다 (설치)
3auth{engine} 인증 토큰이 없습니다설정 > 인증에서 토큰을 넣으면 다음 tick에 뜹니다 (인증)
4cooldown계정 한도 - {n}초 뒤 풀립니다기다리면 풀립니다. 바로 돌리려면 설정 > 인증에서 다른 계정을 켭니다
5machineCap머신 전체 세션 {a}/{b} - 다른 프로젝트가 세션 슬롯을 쓰고 있습니다다른 세션이 끝나면 풀립니다. 바로 돌리려면 설정 > 워커 > 머신 전체 세션 상한을 올립니다 (동시에 몇 개 돌릴까)
6personaCap{persona} 상한 {a}/{b}그 페르소나의 진행중 티켓이 끝나면 풀립니다. 상한은 페르소나 화면에서 바꿉니다
7priority우선순위 1 티켓 {hash}이 끝날 때까지 다른 티켓을 안 띄웁니다{hash}가 끝나면 풀립니다. 막으면 안 되는 일이면 그 티켓의 우선순위를 내립니다. 해시를 누르면 그 티켓이 열립니다
8shellCap셸이 {n}벌 쌓여 쉬고 있습니다감시자가 다음 tick에 정리합니다. 손댈 것이 없습니다
9watchdog감시자가 남긴 한 줄 그대로감시자가 스스로 못 고치는 문제입니다. 위 문장의 대상을 사람이 고칩니다
10noCandidate띄울 수 있는 티켓이 없습니다 - 선행 대기 {d}, 답변 대기 {w}, 폴링 {p}답변 대기 티켓에 답하거나 선행 티켓이 끝나기를 기다립니다. 답변 대기 {w}를 누르면 보드가 답변 대기 티켓만 보여 줍니다
11notWakingidle 워커가 90초 동안 한 번도 깨지 않았습니다워커 화면에서 결함 표시(cron 줄 없음 - 실행 비트 없음)를 확인합니다. 링크가 워커 화면을 엽니다(cron에 등록됐는데 안 깨어난다)
-unknown알 수 없는 이유 - 원문: {raw}원문을 홈 대화에 붙여 물어봅니다

기다리면 풀리는 이유(cooldown - machineCap - personaCap - priority - shellCap)는 껐다 켜도 빨리 풀리지 않습니다. 앱을 다시 띄워도 계정 한도와 상한과 쌓인 셸은 그대로이기 때문입니다. 기다리기 싫으면 할 일에 적힌 설정 하나만 바꾸세요. noCandidate는 고장이 아닙니다. 남은 티켓이 전부 선행 티켓이나 답이나 폴링 조건을 기다리고 있어서, 그중 하나가 풀려야 다음 티켓이 뜹니다(대기인데 아무도 안 가져간다).

2026-10-16 현재 migration 줄은 아직 구현 중입니다. migration은 업데이트가 프로젝트 형식을 바꿨는데 그 프로젝트에 아직 적용되지 않았다는 뜻입니다. 옮기는 중이면 화면이 흐려져 있으니 끝나기를 기다리면 됩니다. 화면이 멀쩡한데 이 줄이 남아 있다면 그 프로젝트를 건너뛰었거나 적용이 실패한 경우입니다. 건너뛴 항목은 apply에 --id를 붙여야 적용됩니다. 그 순서는 설치에 있습니다.

unknown은 표에 없는 줄을 만났다는 뜻입니다. 이때 화면은 이유를 짐작하지 않고 원문을 그대로 보여 줍니다. 팝오버에도 고치는 버튼은 없습니다. 할 일은 기존 설정 화면을 가리키기만 합니다.

티켓이 보드에 안 보인다

화면: 보드에서 필터와 검색을 전부 푸시고, 테이블 뷰(?view=table)로도 보세요. 완료 레인은 처음에 20건만 그립니다. 바닥에 닿으면 20건이 더 붙으니 오래된 완료는 끝까지 내려야 나옵니다. 그래도 없으면 알림 종을 여세요. 할당됨 티켓은 어느 레인에도 안 뜨고 거기 뜹니다.

원인: 양쪽 다 없다면 그 파일이 큐 판정을 통과하지 못한 것입니다. 셋 중 하나입니다.

  • tickets/ 바로 밑이 아닙니다. 큐는 평면 구조라 하위 디렉터리를 인식하지 않습니다. 이 경우 큐 판정이 WARN 구 레이아웃에 티켓 n건이 남아 있다를 냅니다.
  • 파일명이 .md로 안 끝나거나 .으로 시작합니다.
  • 이미 .wip이나 .done 접미사입니다. 그건 열린 티켓이 아닙니다(티켓이 지나는 상태).
ls <루트>/tickets/                          # 평면인지, 이름이 <해시>.md인지
<루트>/workers/w1.sh list                    # 엔진이 실제로 무엇을 열린 티켓으로 보나
bash

list에 없는데 ls에는 있다면 판정에서 빠진 것입니다. 저 WARN 줄은 runner.log에 남지 않고 표준에러로 나갑니다. 직접 list를 실행하면 터미널에 그대로 출력되고 cron이 실행한 것은 cron.log에 쌓입니다(로그 읽는 법 §cron.log).

보드에 티켓 파일 이상 경고가 뜬다

화면: 보드 칸반 위에 티켓 파일 이상 <n>건 경고가 뜹니다. 줄마다 파일명과 사유가 적혀 있고, 사유는 둘 중 하나입니다.

사유파일 상태
frontmatter 없음 - 보드와 엔진이 이 파일을 티켓으로 읽지 않습니다맨 위에 ---로 감싼 머리말(frontmatter)이 없습니다. 보드에 카드로 안 뜨고 워커도 안 가져갑니다
같은 해시가 두 상태로 있음 - 엔진은 하나만 봅니다x.md와 x.wip.md처럼 같은 해시의 파일이 두 상태로 있습니다

경고 제목 줄 오른쪽 끝에는 정리 티켓 발행 버튼이 하나 있습니다. 정리하는 길은 둘입니다. 버튼으로 정리 티켓을 내서 세션에 맡기거나, 파일을 손으로 고칩니다. 아래 규칙대로 정본을 고르면 되는 경우는 버튼이 편하고, 어느 파일을 남길지 직접 판단하고 싶으면 손으로 고칩니다.

원인: 흔한 경우는 진행중 티켓의 옛 경로에 덧붙여 쓴 것입니다. 워커가 <해시>.md를 가져가 <해시>.wip.md로 바뀐 뒤, 누군가 셸 명령으로 <해시>.md에 절을 덧붙이면 frontmatter 없는 <해시>.md가 새로 생깁니다. 거기 적은 내용은 도는 세션에 닿지 않습니다.

정리 - 버튼으로 맡기기: 정리 티켓 발행을 누르면 pm에게 가는 티켓 파일 이상 <n>건 정리 티켓이 한 장 생기고, 화면이 그 티켓으로 넘어갑니다. 보드로 돌아오면 버튼 자리에 정리 티켓 <해시> <상태> 링크가 떠 있고, 이 링크를 눌러 진행을 확인합니다. 정리 티켓이 끝나지 않은 동안에는 버튼을 두 번 누르거나 다른 탭에서 눌러도 새 티켓이 생기지 않고, 이미 있는 그 한 장으로 갑니다.

정리 티켓은 누른 시각의 이상 목록 전부를 다루고, 사유별로 이렇게 고칩니다.

사유정리 티켓이 하는 일
frontmatter 없음같은 해시의 다른 파일이 있으면 이 파일 내용을 그 파일 끝 ## 고아 파일에서 옮김 절로 붙이고 이 파일을 지웁니다. 다른 파일이 없으면 본문을 읽고 머리말을 되살립니다. 알 수 없는 값은 kind: work, persona: pm으로 채우고, 파일명(상태)은 바꾸지 않습니다
같은 해시가 두 상태로 있음.done, .wip, 접미사 없음 순서로 앞선 파일 하나를 정본으로 남깁니다. 나머지 파일에만 있는 내용을 정본 끝 ## 고아 파일에서 옮김 절로 붙이고 나머지를 지웁니다. 정본이 진행중이어도 도는 세션에 참견을 보내지 않고 본문에만 적습니다

정리 티켓이 끝나면 목록에 있던 파일이 경고에서 사라집니다. 정리 티켓을 낸 뒤에 새로 생긴 이상은 그 티켓의 대상이 아닙니다. 그래서 정리 티켓이 완료된 뒤에도 경고가 남아 있으면 버튼이 다시 나오고, 누르면 남은 이상을 다루는 새 정리 티켓이 생깁니다. 발행에 실패하면 버튼 왼쪽에 정리 티켓을 만들지 못했습니다와 이유가 뜹니다. 누르는 사이에 이상이 모두 사라졌다면 이유는 이상이 없습니다이고, 이때는 티켓이 생기지 않습니다.

정리 - 손으로 고치기: 파일을 직접 고칩니다.

  1. 그 해시의 파일을 전부 봅니다. 상태는 대기, 진행중, 완료 순서로만 바뀌므로 대개 접미사가 더 나아간 쪽(.done이 .wip보다, .wip이 접미사 없는 것보다)이 정본입니다.
  2. 지울 파일을 열어 정본에 없는 내용이 있는지 봅니다. 사람이 덧붙인 결정이 그 파일에만 있는 경우가 많습니다.
  3. 옮길 내용이 있으면 정본이 진행중일 때는 정본 끝에 절로 더하고, 도는 세션에도 참견으로 알립니다(도는 세션에 말 걸기). 정본이 완료면 정본을 고치지 말고 새 요구로 올립니다(요구사항 접수하기 §이미 처리 중인 요구에 결정을 더할 때).
  4. 지울 파일을 지웁니다. 해당 줄이 경고에서 사라지고, 이상이 0건이 되면 경고 자체가 사라집니다.

짝이 없는 파일 하나만 frontmatter 없음으로 뜬다면, 손으로 만든 티켓에 머리말을 빠뜨린 것일 수 있습니다. 그 파일을 티켓으로 살리려면 머리말을 넣고(티켓 직접 발행하기), 아니면 지웁니다.

ls <루트>/tickets/<해시>*                    # 그 해시로 파일이 몇 개인가
head -3 <루트>/tickets/<해시>.md             # 첫 줄이 ---인가
rm <루트>/tickets/<해시>.md                  # 옮길 내용을 정본에 옮긴 뒤에만
bash

대기인데 아무도 안 가져간다

화면: 카드의 배지와 태그를 먼저 보세요. 주황색 deps 태그가 붙었다면 선행 티켓이 아직 안 끝난 것이고, 선행이 끝나면 아무도 손대지 않아도 사라집니다. 폴링 대기 배지가 붙었다면 그 티켓은 큐 밖의 조건을 기다리는 중이라 다음 항입니다. 재시도 대기 배지가 붙었다면 이 티켓을 맡은 세션이 잇달아 죽어서 지금 잠시 대기하는 중이고, 다시 뜰 때까지 남은 시간이 그 배지에 같이 붙습니다. 셋 다 없으면 알림 종을 여세요. 답변을 기다리는 티켓과 되풀이해 죽어서 쉬는 티켓과 아무도 가져가지 않는 티켓이 거기 각각 자기 건수를 알려 줍니다.

원인은 종 항목마다 다릅니다.

  • 답변 대기 - 이 배지가 붙어 있으면 사람이 쓸 답이 반드시 있습니다. 대개는 세션이 막혀 ## 질문 n을 붙이고 스스로 잠근 자리입니다. 존재하지 않는 해시가 deps로 걸려 있어서, 사람이 답을 쓰기 전에는 어떤 워커도 가져가지 않습니다. 답을 쓰는 자리는 셋인데 폼은 한 벌입니다. 티켓 상세의 진행 기록 맨 끝 입력칸, 보드 카드의 답변 달기 버튼, 그리고 알림 종의 답변 쓰기 링크. 어디로 써도 결과가 같습니다. 세션이 안 물어도 엔진이 대신 잠그는 자리는 셋뿐입니다. 세션이 끝까지 정상으로 돌고도 티켓을 안 닫은 채 자동 회수가 세 번째일 때, 폴링이 상한까지 기다렸는데 조건이 안 왔을 때, 이어받기 사슬이 상한에 걸렸을 때. 셋 다 남은 범위를 어떻게 할지 묻는 자리라 사람이 판정해야 풀립니다. 세션이 죽어서 멎은 티켓은 여기 안 옵니다 - 요청이 잘못되어 죽은 경우도 다음 항의 재시도 대기로 갑니다.
  • 되풀이해 죽어서 쉬는 티켓 - 이 티켓을 맡은 세션이 잇달아 죽었습니다. 요청이 잘못됐거나, 세션이 아예 못 떴거나, 실행 상한을 넘겨 강제종료된 회차입니다. 어느 쪽도 사람이 답을 써서 달라지지 않으니 엔진은 묻지 않고 그 티켓을 잠시 후보에서 빼 둡니다. 잠긴 것이 아니라서 카드에 붙는 배지도 재시도 대기이고, 다시 뜰 때까지 남은 시간이 거기 붙습니다. 종에 뜨긴 하지만 고장 신고는 아닙니다. 지금 이 티켓이 대기 중이라고 알려 주는 항목입니다. 대기 시간은 되풀이할수록 길어집니다. 첫 대기는 몇 분이고, 같은 티켓이 또 죽으면 다음 대기가 앞선 두 대기를 더한 길이가 됩니다. 대기 횟수에는 상한이 없습니다. 간격이 얼마나 벌어지든 사람이 안 건드려도 대기가 끝나는 순간 다시 후보가 됩니다. 대기가 끝나기를 안 기다려도 됩니다. 티켓을 열면 오른쪽 단의 재시도 대기 절에 지금까지 몇 번 대기했는지(누적 횟수)와 다음이 언제인지(다음 시각)가 있습니다. 그 아래 지금 다시 보내기 버튼 하나로 대기를 끊으시면 됩니다. 누르면 대기 표식이 그 자리에서 사라지고 누적 횟수도 같이 지워집니다. 왜 그렇게 죽는지 보고 싶으시면 같은 화면의 진행 기록에서 매번 어디서 멈추는지 확인하시면 됩니다.
  • 아무도 가져가지 않는 티켓(할당됨) - 열린 파일인데 session_id가 채워져 있습니다. 엔진이 만들지 않는 조합입니다. 이 상태가 되면 선정에서 영구히 제외되어 아무 진척 없이 멎습니다. 종 안의 그 줄에 붙은 할당 해제 버튼 하나로 풀립니다.
<루트>/workers/w1.sh list                       # deps 대기 / 할당됨이 그대로 찍힌다
tail -20 <루트>/workers/runner.log | grep ASK   # 답변 요청으로 전환된 시점
grep 백오프 <루트>/workers/runner.log | tail -5 # 쉬게 한 티켓과 그때까지 센 횟수
bash

폴링 대기인데 조건이 안 온다

화면: 카드의 배지 아래 한 줄을 먼저 보세요. 대기를 건 세션이 무엇을 왜 기다리는지 적어 둔 문장입니다. 그다음 그 티켓을 열어 오른쪽 단의 폴링 대기 절을 보세요. frontmatter 표 바로 아래입니다. 같은 문장이 사유 행에 있고, 그 밑으로 무엇을 기다리는지(스크립트 본문), 얼마나 자주 돌려 보는지(주기), 언제까지 기다리는지(상한과 남은 시간), 마지막으로 돌린 시각, 그리고 마지막 출력이 거기 다 있습니다. 안 풀리는 이유는 대개 마지막 출력에 적혀 있습니다.

원인은 그 절을 위에서 아래로 읽으면 갈립니다.

  • 사유 행이 없습니다. 대기를 건 세션이 ## 결과 첫 줄에 사유를 안 적은 것입니다. 화면이 없는 값을 만들어 내지 않으므로 이 행은 표시되지 않습니다. 무엇을 기다리는지는 아래 스크립트 본문에서 읽으세요.
  • 마지막 폴링이 아직 안 돌림입니다. 대기를 건 뒤로 워커가 한 번도 안 깼다는 뜻입니다. 이건 폴링 문제가 아닙니다. 다음 항(§큐가 통째로 멈춰 있다)과 §cron에 등록됐는데 안 깨어난다를 차례로 보세요.
  • 마지막 폴링 시각은 계속 새로 바뀌는데 조건이 안 옵니다. 스크립트는 정상으로 실행되고 있고 큐 밖의 작업이 아직 안 끝난 것입니다. 정상입니다. 상한까지 그대로 두시면 됩니다.
  • 마지막 출력에 오류 메시지가 있습니다. 스크립트 자체에 문제가 생겼습니다. 연속 세 번 이렇게 끝나면 엔진이 폴링을 그만두고 그 티켓을 답변 대기로 옮깁니다.
  • 스크립트 파일 없음이 떠 있습니다. polls/ 밑에서 그 파일이 사라졌습니다. 이 티켓은 조건을 판정할 방법이 없어서 상한이 올 때까지 안 풀립니다.
cat <루트>/polls/<해시>.log                    # 상세에 뜨는 그 출력의 원본(마지막 200줄)
bash <루트>/polls/<파일명>; echo $?             # 손으로 한 번 돌려본다. 0=조건 도달, 1=아직, 그 밖=오류
bash

직접 실행해서 0이 나오는데 카드가 그대로 폴링 대기라면 스크립트는 정상입니다. 워커가 깨어나고 있는지부터 확인하세요.

끊는 법은 셋입니다. 앞의 둘은 폴링 대기 절 맨 아래에 있는 버튼 둘이고, 상한을 넘기기 전에도 누를 수 있습니다.

  • 지금 디스패치 - 누르면 조건을 안 채운 채로 그 티켓이 후보로 돌아갑니다. 다음 tick을 기다리지 않고 그 자리에서 워커까지 깨웁니다. 확인 다이얼로그는 없습니다.
  • 상한 늘리기 - 옆 칸의 시각을 새 상한으로 적습니다. 조건이 곧 충족될 것 같은데 상한이 먼저 지날 것 같을 때 씁니다. 카드의 남은 시간이 그 값을 따라갑니다.

셋째는 그냥 두는 것입니다. 상한이 지나면 배지가 상한 지남으로 바뀌고 다음 tick에서 엔진이 그 티켓을 답변 대기로 올립니다. 그때 뜨는 질문에는 마지막 폴링 출력이 인용으로 붙어 있고, 상한을 늘려 더 기다릴지 조건과 상관없이 지금 디스패치할지 아예 닫을지를 고르시면 됩니다. 이 화면은 여느 질문과 똑같습니다. 앞의 버튼 둘과는 다른 자리입니다.

버튼 둘은 답변 파일을 만들지 않습니다. 사람이 묻지 않은 질문이라 답도 없습니다. 티켓 파일에서 달라지는 것은 지금 디스패치가 비우는 polling과 polling_fails, 상한 늘리기가 다시 적는 polling_until뿐입니다(frontmatter 필드).

큐가 통째로 멈춰 있다

화면: 알림 종의 커밋 안 된 변경이 디스패치를 막고 있습니다 항목입니다. 티켓 몇 건이 걸린 게 아닙니다. 이 프로젝트의 워커 전부가 매분 깨기는 하는데 아무도 티켓을 안 가져갑니다. 보드에는 대기가 그대로 쌓이고 워커 화면은 전부 idle이라 종을 열기 전까지는 어디에도 경고 표시가 뜨지 않습니다.

항목은 받는 트리의 절대경로를 먼저 표시하고 그 밑에 git status가 출력한 줄을 그대로 펼칩니다. 앞 두 글자가 상태 코드고 그 뒤가 경로입니다. 몇 건이든 전부 펼칩니다 - 상위 몇 개만 남기고 자르지 않습니다. 받는 트리는 워커의 워크트리가 갈라져 나온 원본, 그러니까 프로젝트 폴더 자체입니다.

원인: 세션은 자기 워크트리에서 일하고 끝나면 받는 트리로 push합니다. 그 트리에 커밋 안 된 변경이 남아 있으면 git이 그 push를 전부 거부합니다(receive.denyCurrentBranch=updateInstead). 그러면 세션은 5분에서 25분을 전부 소비한 뒤 마지막 push에서야 실패합니다. 티켓은 회수돼 다음 워커에게 갔다가 같은 자리에서 또 막힙니다. 통합 게이트가 그 낭비를 미리 막습니다. 워커는 티켓을 고르기 전에 받는 트리의 상태를 확인하고 더러우면 그 tick을 거기서 끝냅니다.

확인하는 조건은 좁습니다. 받는 트리가 통합 브랜치를 체크아웃 중일 때만 막습니다. 다른 브랜치를 보고 있거나 rebase로 detached HEAD면 더러워도 그냥 디스패치합니다. 그 push는 원래 통과하니까요. 추적 안 되는 파일도 안 셉니다. 스크래치 파일 하나 때문에 큐를 멈추지는 않습니다.

.dira/push.sh가 있으면 게이트가 이 판정을 아예 건너뜁니다. 그 헬퍼가 push 직전에 받는 트리를 스스로 정리하기 때문에 게이트가 미리 막을 실패가 없습니다. 지금 만드는 프로젝트에는 헬퍼가 처음부터 설치됩니다. 이 항목은 헬퍼 없이 운영하던 시절에 만든 프로젝트에서만 뜹니다. 그 헬퍼가 무엇을 대신하고 받는 트리를 어떻게 정리하는지는 워커 §세션이 결과를 합치는 길에 있습니다.

게이트가 확인하는 그 브랜치 이름은 dispatch-gate.sh 안에 문자열로 적혀 있습니다. 설정에서 통합 브랜치를 바꾸면 앱이 이 파일을 새 이름으로 다시 만들어 줍니다. 직접 고쳐 둔 게이트 하나만 예외입니다. 저장한 자리에 뜨는 다시 쓴 파일: 목록에서 dispatch-gate.sh를 못 봤다면 그 파일을 직접 열어 브랜치 이름을 고치세요(첫 프로젝트 만들기 §통합 브랜치를 나중에 바꾸기).

게이트는 프로젝트마다 한 벌씩 있습니다. 새로 만든 프로젝트는 처음부터 설치되고 그 전에 만든 프로젝트는 워커 화면이 이 워커는 받는 트리가 더러워도 그냥 디스패치됩니다라고 안내하면서 통합 게이트 적용 버튼을 표시합니다. 게이트가 없는 프로젝트는 이 알림도 안 뜹니다. 대신 세션이 push 단계에서 연달아 실패합니다.

할 일은 하나입니다 - 그 트리에서 커밋하거나, 지울 것이면 지우세요. 당신이 편집하다 만 것이면 커밋하고, 경쟁하던 다른 push가 남긴 파일이면 지웁니다. 종에 펼쳐진 줄이 그 판단의 근거입니다.

앱에는 버튼이 없습니다. 커밋할 것인지 지울 것인지가 줄마다 다르고, 그건 사람이 파일을 보고 정하는 판단입니다. 화면이 둘 중 하나를 골라 버튼으로 놓으면 앱이 남의 작업 트리를 대신 커밋하거나 대신 지웁니다. 줄을 그대로 보여 주는 데까지가 이 항목의 역할입니다.

푸는 절차는 없습니다. 트리가 깨끗해지면 다음 tick에서 워커가 그냥 다시 가져갑니다. 종 항목도 5초 뒤 다음 판정에서 저절로 사라집니다. 눌러야 할 해제 버튼이 없고 플래그가 남아서 멀쩡한 트리가 계속 막히는 일도 없습니다.

git -C <받는 트리> status --porcelain -uno     # 종에 펴진 그 줄
grep GATE <루트>/workers/cron.log | tail -4    # 보류와 해제가 한 줄씩
bash

GATE 디스패치 보류와 GATE 해제가 짝으로 남습니다. 그 사이가 큐가 멈춰 있던 구간입니다. 이 두 줄은 runner.log에 남지 않고 cron.log에 있습니다(로그 읽는 법 §cron.log).

데스크톱 앱이면 OS 알림도 옵니다

dira.app을 쓰시면 창을 보고 있지 않아도 알 수 있습니다. 제목이 디스패치 보류, 본문이 프로젝트 이름과 커밋 안 된 변경 건수입니다. 누르면 창이 열리고 그 프로젝트 화면으로 이동합니다.

막히는 순간 한 번입니다. 막혀 있는 동안 다시 오지 않습니다. 한 시간이 지나도, 하루를 넘겨도 그 한 번이 전부입니다. 앱 안에서는 종 항목이 내내 떠 있으니 그걸로 충분합니다. 풀릴 때도 안 옵니다. 사람이 직접 커밋해서 푼 일이라 알릴 것이 없습니다.

앱을 켜기 전부터 막혀 있었다면 그 프로젝트는 알림 없이 넘어갑니다. 켠 직후 첫 조회는 지금 막혀 있는 프로젝트를 알림 없이 기록해 두기만 합니다. 앱을 켤 때마다 밀린 알림이 한꺼번에 몰려오면 사람은 그 알림을 꺼 버리니까요. 이때도 종에는 그대로 떠 있습니다.

이 알림에 없는 것

  • 끄는 스위치가 없습니다. 종 항목도 OS 알림도 마찬가지입니다.
  • 다시 보내기가 없습니다. 막힌 채로 하루가 지나도 알림을 한 번 더 보내지 않습니다.
  • 다른 프로젝트가 막힌 것은 여기 안 뜹니다. 종은 자기 프로젝트만 대상으로 합니다. 그 보류는 그 프로젝트의 종에 떠 있습니다. 등록된 프로젝트를 전부 확인하는 자리는 OS 알림 쪽입니다.

워커는 도는데 아무것도 안 한다

화면: 헤더 우측의 설정 버튼을 보세요. 인증이 없으면 그 버튼 자체에 인증 필요가 붙습니다. 알림 종의 Claude 토큰이 없습니다 항목도 같은 것을 말하고, 그 항목 안의 링크가 설정을 바로 엽니다. 왼쪽 트리에서 인증 › claude를 고르시면 그 자리에서 발급할 수 있습니다(인증).

원인: 헤드리스 토큰이 없거나 만료됐습니다. 엔진이 claude이고 비대화형(cron)인데 oauth-token이 없으면 그 엔진을 쓰는 후보를 통째로 건너뛰고(SKIP), 처음 한 번만 로그에 남깁니다(.authwarn 파일로 중복 로그를 막습니다). claude 아닌 엔진을 쓰는 다른 후보가 있다면 그쪽은 정상 디스패치됩니다. 굶는 것은 claude 후보뿐입니다. 토큰이 있지만 만료됐다면 디스패치는 일어나되 세션이 인증 오류로 곧바로 끝나 FAIL로 남습니다.

인증이 멀쩡한데도 이 증상이면 머신 전체 동시 세션 상한을 보세요. 이 컴퓨터가 상한에 차 있거나 이 프로젝트가 자기 몫을 다 썼으면 워커는 깨어나되 티켓을 안 가져갑니다. 판정은 동시에 몇 개 돌릴까 §상한 때문에 굶는지 알아보는 법에 있습니다.

ls -la ~/.config/dira/oauth-token             # 파일이 있는지, 언제 만들었는지
grep 'AUTH 대기' <루트>/workers/runner.log     # SKIP AUTH 대기 ... 최초 1회만 남는 줄
bash

FAIL이 찍혀 있다면 이유는 그 줄의 세션 로그에 있습니다(로그 읽는 법 §logs/).

세션이 뜨자마자 죽는다

화면: 알림 종의 세션이 열리자마자 죽는 워커 항목입니다. 한도 소진, API 과부하, 연결 끊김처럼 큐도 워커도 멀쩡한데 세션만 죽는 경우입니다. 사유 문구에 복구 시각이 들어 있으면 그대로 보여 줍니다(session limit · resets 7:40pm 같은 줄).

원인: 원인이 앱 밖에 있습니다. 여기서 하실 일은 기다리거나 한도를 올리는 것이며 큐를 고칠 일은 없습니다. 그동안 워커는 idle과 running을 오가고 티켓은 대기에 놓여 있어서 화면의 다른 곳은 전부 정상으로 보입니다. 이 항목이 따로 있는 이유가 그것입니다.

한 번 이렇게 죽으면 워커는 곧바로 다시 시도하지 않습니다. 그 엔진에 쿨다운을 걸어 두고 쿨다운이 끝날 때까지 돌아오는 tick을 전부 건너뜁니다. 기본은 300초, 한도 메시지가 복구 시각을 줬다면 그 시각까지 입니다. 그동안 runner.log에는 SKIP 엔진 쿨다운 · n초 남음만 쌓입니다. 아무 일도 안 일어나는 것처럼 보여도 그게 정상입니다.

이 알림은 저절로 안 꺼집니다. 워커가 다시 정상으로 동작해도 종에는 남습니다. 밤새 이렇게 죽었으면 아침에도 그대로 떠 있어야 자리를 비운 사람이 그 사실을 봅니다. 워커 화면 쪽은 다릅니다. 거기 붙는 실패 줄은 10분이 지나면 사라집니다.

읽고 나면 항목 오른쪽 아래 보관을 누르세요. 그때 나열돼 있던 실패가 한 번에 보관되고 항목이 접힙니다. 삭제하지 않고 옮기기만 하므로 팝오버 상단의 보관함 토글을 켜면 시각과 워커와 사유가 그대로 있습니다. 종에 뜨는 아홉 항목 중 이 버튼이 붙은 것은 여기와 큐가 멈춰 있던 구간 3건 둘뿐이고, 둘 다 한 번 누르면 항목이 통째로 사라집니다. 사흘이 지났어도 보관은 한 번이면 됩니다. 대신 보관함에는 사건마다 한 행이 남습니다 - 언제 몇 번 멈췄는지가 거기 그대로 있습니다(화면 소개 §알림 종).

보관해도 다음 사고를 놓치지는 않습니다. 보관 단위가 실패 하나씩이라서, 새 FAIL이 오면 그것은 별개의 실패이므로 항목이 다시 뜹니다.

grep -E 'FAIL|TIMEOUT' <루트>/workers/runner.log | tail -5
bash

FAIL 줄 꼬리의 로그 파일명이 사유의 원본입니다. 마지막 줄 JSON의 result가 그 문구입니다 (로그 읽는 법 §logs/).

워커가 stale이다

화면: 워커 화면의 상태 칸입니다. 행마다 pid가 같이 보입니다.

원인: 이 상태가 항상 오작동을 뜻하지는 않습니다. 회수(reap)는 죽은 세션이 점유하던 티켓을 다시 열린 상태로 돌려놓는 동작이며 그 판정 기준은 프로세스가 살아 있는지 여부입니다. 오래 걸리는 정상 세션과 멈춰 버린 세션은 경과 시간만으로 구분되지 않기 때문입니다. pid가 살아 있으면 running이라 아무리 오래 걸려도 그대로 두고, 죽었으면 stale이며 다음 tick이 회수합니다(디스패치 직후 3분 유예는 있습니다). stale을 보셨다면 대개 30초 안에 저절로 풀립니다.

<루트>/workers/w1.sh reap                                 # 지금 한 번 강제로 돌려본다
tail -20 <루트>/workers/runner.log | grep -E 'REAP|SKIP'
bash

REAP이 남으면 회수된 것이고, 아무것도 남지 않으면 세션이 아직 살아 있습니다. 그럴 때는 기다리시면 됩니다(로그 읽는 법 §runner.log).

cron에 등록됐는데 안 깨어난다

화면: 워커 화면의 상태 칸이 stopped면 crontab에 그 워커의 줄이 없다는 뜻입니다(파일만 있습니다). idle이면 등록은 돼 있습니다.

원인: 앱에서 워커를 등록할 때마다 macOS가 앱 관리 권한을 다시 묻습니다. 앱이 crontab에 줄을 쓰는 동작이라 그렇습니다. 전체 디스크 접근 권한과는 별개입니다. 이 승인은 다음 등록까지 남지 않습니다. 응답하기 전까지 등록 자체가 멈춰 있어서, 창을 놓치면 "만들었는데 안 돈다"로 보입니다(인증 §앱 관리).

crontab -l | grep <워커이름>     # 그 워커의 두 줄이 실제로 등록됐는지
bash

안 보이면 설정 › 개인정보 보호 및 보안 › 앱 관리에서 승인하고 워커를 다시 만드세요. 직접 등록하는 절차는 엔진만으로 돌리기 §crontab 두 줄에 있습니다. 등록은 됐는데 로그가 아예 안 자란다면 다음 항입니다(로그 읽는 법 §cron.log).

워커가 아예 안 뜬다

화면: 프로젝트 목록에서 그 프로젝트가 연결 안 됨이거나, 워커 화면에 있어야 할 행이 아예 없습니다. 앱도 그 경로를 읽지 못하고 있다는 뜻이라, 원인이 경로 쪽에 있습니다.

원인: 큐 루트가 클라우드 마운트(구글드라이브 등)인데 마운트가 안 붙어 있습니다. 워커 파일 자체가 그 마운트 안에 있으니, 마운트가 빠지면 cron이 실행하려는 경로에 스크립트가 없습니다. 오류 메시지 없이 실패해서 "큐가 비어서 안 도는구나"로 오해하기 쉬운 자리입니다(엔진만으로 돌리기 §큐를 프로젝트 밖에 둘 때).

ls <루트>/workers/     # 워커 파일이 실제로 보이는지부터 확인
bash

안 보이면 마운트 상태를 먼저 보세요. 마운트는 붙어 있고 행도 보이는데 runner.log가 자라지 않는다면 셔뱅이나 실행 권한에 문제가 생긴 것이고, 그건 cron.log에만 남습니다(로그 읽는 법 §cron.log).

예전에 만든 공통 워커가 남아 있다

화면: 워커 표에 이 프로젝트에서 만든 적 없는 행이 있습니다. pw1 같은 이름에 상태는 stopped입니다. 설정 › 워커를 열어도 그 워커를 만든 자리가 없습니다.

원인: 머신 하나에 워커를 만들어 두고 프로젝트마다 몇 명까지 빌릴지 정하는 기능이 있었습니다. 그 기능을 제거해서 지금은 만드는 자리도 빌리는 자리도 없습니다. 삭제하는 코드까지 함께 제거했기 때문에, 앱을 업데이트해도 예전에 만들어 둔 것은 이 컴퓨터에 그대로 남습니다. 남은 자리가 셋이고, 순서를 지켜 정리하면 됩니다.

먼저 crontab에서 그 줄을 뺍니다. 슬롯 하나에 두 줄씩 걸려 있어서 지금도 30초마다 깨어나고 있습니다. 파일을 먼저 지우면 그 1분 사이에 cron이 없는 파일을 실행해 로그에 에러가 쌓입니다.

crontab -l | grep 'dira/pool'                  # 몇 줄이 걸려 있는지 먼저 확인
crontab -l | grep -v 'dira/pool' | crontab -   # 그 줄만 빼고 나머지는 그대로 씁니다
bash

뒤 명령은 crontab을 통째로 다시 씁니다. 워커의 두 줄과 주석에는 dira/pool이 없어 그대로 남지만, 실행하기 전에 앞 명령으로 어느 줄이 빠지는지 확인하시는 편이 안전합니다.

그다음 이 컴퓨터에 있는 풀을 지웁니다. 방금 뺀 cron 줄이 실행하던 스크립트와 그 로그, 그리고 슬롯이 사용하던 잠금 파일이 여기 있습니다.

rm -rf ~/.config/dira/pool
rm -rf ~/.config/dira/run/pool-*.lock
rm -f ~/.config/dira/run/pool-turn-*
bash

마지막으로 큐마다 파일 둘을 정리합니다. 빌리던 프로젝트의 큐에는 워커 파일 한 장과 빌리기 상한 파일 하나가 들어가 있습니다. 어느 프로젝트가 빌리고 있었는지 기억나지 않아도 되고, 등록한 프로젝트를 한 번씩 확인하면 됩니다. 경로는 프로젝트 목록의 경로 열에 있습니다.

grep -l 'dira-pool:' <루트>/workers/*.sh   # 들어와 있던 워커 파일. 없으면 안 빌리던 큐입니다
rm -f <루트>/pool-limit                    # 빌리기 상한. 앱에는 이 값을 읽는 자리가 없습니다
bash

워커 파일은 지우지 않고 그냥 쓰셔도 됩니다. 파일 모양이 이 프로젝트가 만든 워커와 같아서, 그 행의 재등록을 누르면 이 프로젝트의 워커로 그대로 동작합니다. 이름을 바꾸시려면 행의 삭제로 지우고 워커 생성으로 다시 만드세요. 그 둘은 표에서 하는 조작이라 셸을 열 일이 없습니다.

다음은 로그 읽는 법입니다.