워커

워커는 30초마다 깨어나 큐를 훑고, 열려 있는 티켓 하나를 물어 세션을 띄우는 셸 스크립트입니다. 파일 하나가 워커 하나이고(<루트>/workers/<이름>.sh), 첫 프로젝트를 만들 때 w1이 이미 섰습니다.

워커 화면 — 한 줄이 말하는 것

워커 여덟이 전부 running으로 서서 각자 티켓 해시 하나를 물고 있습니다. 제목 옆에 최근 5시간 토큰 165M이 적혀 있고, 줄 오른쪽 끝은 reap, 스트림, 중단, 삭제 네 액션입니다.

왼쪽 내비의 워커. 한 줄이 워커 하나이고, 그 줄만 읽어도 지금 무슨 일이 벌어지는지 압니다. 열은 왼쪽부터 이름 · 상태 · 물고 있는 티켓 · 컨텍스트 · 마지막 활동 · pid · 토큰(5시간) · 액션 여덟입니다.

  • 상태는 넷입니다. running은 세션을 띄워 놓고 끝나기를 기다리는 중, idle은 cron에 걸려 있고 지금은 아무것도 안 물고 있는 상태입니다. stopped은 crontab에서 빠져 있어 아예 깨어나지 않고(배지 옆에 crontab 미등록이 붙습니다), stale은 세션 프로세스가 죽었는데 자리만 남았습니다.
  • 물고 있는 티켓은 지금 잡고 있는 티켓의 해시입니다. 누르면 그 티켓 상세로 갑니다.
  • 컨텍스트는 이 워커가 세션마다 딸려 보내는 참조 문서의 수입니다. 숫자를 누르면 행이 펼쳐져 그 자리에서 항목을 고칩니다. 표 아래 공통 컨텍스트는 워커 전원이 같이 받는 목록입니다.
  • 마지막 활동runner.log에 이 워커 이름으로 마지막에 찍힌 줄 그대로입니다. DISPATCH면 방금 티켓 하나를 세션에 넘겼습니다. SKIP이면 깨어났는데 앞 세션이 아직 안 끝나 그냥 돌아갔습니다. 누르면 최근 20줄이 펼쳐집니다.
  • pid는 지금 그 워커를 돌리고 있는 프로세스의 번호입니다. runningstale은 이 번호가 살아 있는지로 갈립니다(화면 소개 §워커).
  • **토큰(5시간)**은 최근 5시간 안에 끝난 세션이 쓴 토큰 합입니다. 한도는 워커별이 아니라 계정 하나에 붙어 있어서, 이 열이 한 통을 누가 얼마나 먹었는지 보여 줍니다.

30초 주기 — 깨어났다 죽는 스크립트

워커는 늘 떠 있는 프로세스가 아닙니다. cron이 30초에 한 번 그 스크립트를 처음부터 실행하고, 스크립트는 할 일을 마치면 죽습니다. 물 티켓이 없으면 큐를 훑고 그 자리에서 끝나며 로그에도 아무것도 안 남깁니다.

그래서 idle은 "차례를 기다리는 중"이 아닙니다. 지금 이 순간 그 워커의 프로세스가 아예 없다는 뜻이고, 다음 :00이나 :30에 새 프로세스가 하나 뜹니다. 그 30초는 crontab에 들어간 줄 둘이 만듭니다(아래 §새 워커에 딸려 오는 것 다섯). 화면에 조절할 칸이 없는 것도 그래서입니다.

앱과는 무관하게 돕니다. 앱을 닫아도 티켓은 계속 처리되고, 켜 둔다고 더 빨리 돌지도 않습니다. 반대로 중단으로 crontab 줄을 빼면 앱이 떠 있어도 그 워커는 다시 깨어나지 않습니다.

워커와 세션 — 한 번에 티켓 하나

티켓을 잡은 워커가 그다음에 하는 일은 세션을 띄우는 것입니다. 세션은 그 티켓을 들고 도는 에이전트 프로세스이고(기본은 claude -p), 페르소나 프로필과 협업 프로토콜에 어느 티켓을 잡았는지를 이어 붙인 프롬프트를 받습니다. 티켓 본문은 세션이 그 자리에서 직접 읽습니다. 워커는 그 세션이 끝날 때까지 같은 자리에서 기다립니다. 30분이 걸리는 티켓이면 30분을 기다립니다.

기다리는 동안에도 cron은 30초마다 그 워커를 부릅니다. 새로 뜬 쪽은 앞 실행이 아직 일하는 중인 것을 보고 SKIP 한 줄만 남기고 물러납니다. 한 워커가 세션 둘을 띄우는 일도, 같은 티켓이 두 번 디스패치되는 일도 그래서 없습니다. 그 자리를 막는 장치와 그것이 워커 수와 어떻게 이어지는지는 동시에 몇 개 돌릴까 §워커 락에 있습니다.

워커는 짧게 살고 세션은 오래 삽니다. running이 한참 서 있다면 그 워커가 띄운 세션이 아직 일하는 중이라는 뜻입니다. 지금 그 세션에 할 말이 있으면 도는 세션에 말 걸기로 가세요.

티켓을 무는 일 — 파일 이름 하나 바꾸기

"문다"는 비유가 아닙니다. 후보를 고른 워커가 실제로 하는 동작은 <해시>.md<해시>.wip.md로 만드는 것 하나입니다. 성공하면 그 티켓은 이 워커 것이 되고, 이미 다른 워커가 바꿔 놨으면 그 자리에서 실패해 다음 후보로 넘어갑니다.

워커 여럿이 같은 큐를 동시에 봐도 둘이 같은 티켓을 물지 않는 이유가 이것입니다. 확인하는 순간이 곧 잡는 순간이라 그 사이에 다른 워커가 끼어들 틈이 없습니다. 이름 바꾸기가 어떻게 락이 되는지는 티켓이 지나는 상태 §파일명이 곧 락인 이유에 있습니다.

후보는 열려 있고 선행 티켓(deps)이 다 끝난 티켓입니다. 그중 우선순위가 높은 것부터 집고 같은 값끼리는 만든 날짜가 오래된 쪽이 먼저입니다. 우선순위는 티켓 frontmatter의 priority:이고 1~5에서 5가 가장 높습니다. 안 적으면 3입니다.

열려 있는데도 안 물어가는 티켓이 있다면 우선순위 아래에 게이트가 둘 더 있습니다. 페르소나마다 동시에 물 수 있는 티켓 수에 상한이 걸릴 수 있고, 우선순위 1짜리는 진행중이 0건일 때만 후보입니다. 둘 다 동시에 몇 개 돌릴까 §워커 수보다 낮아지는 두 자리에 있습니다.

워커마다 트리가 따로인 이유

두 세션이 한 디렉터리에서 같은 브랜치에 커밋하면 서로의 작업을 덮습니다. 이 프로젝트에서 실제로 났던 사고입니다. 새 워커가 템플릿 워커의 작업 디렉터리 값을 그대로 물려받는 바람에 세 세션이 한 트리에서 한 브랜치에 커밋하고 있었고, 화면은 그걸 결함으로 보여주지도 못했습니다.

그래서 지금은 그 경로를 워커 이름에서 유도합니다. 이름이 곧 디렉터리라서 두 워커가 같은 트리를 가리키는 값이 애초에 생기지 않습니다.

프로젝트 폴더를 git이 관리하고 있지 않으면 트리는 만들지 않고 그 사실만 알립니다. 워커 파일도 crontab 등록도 그대로 유효합니다. 워커가 하나뿐인 배치에는 트리가 필요 없습니다.

워커 생성 한 번

워커 하나가 한 번에 티켓 하나만 뭅니다. 그래서 대기 레인에는 티켓이 쌓이는데 진행중 레인이 늘 한 장이면, 그 워커로는 부족해진 것입니다. 늘리는 자리는 워커 화면입니다(동시에 몇 개 돌릴까 참고).

우상단 워커 생성을 누르면 다이얼로그에서 정할 것이 이름 하나뿐입니다. w2처럼 짧게 지으세요. 영문·숫자·_·-만 되고, 이 이름이 그대로 파일명이자 작업 디렉터리 이름이 됩니다.

엔진이나 모델을 고르는 칸은 여기 없습니다. 새 워커는 기존 워커를 복사해서 만들어지므로 엔진 설정도 그대로 물려받습니다. 어느 CLI로 세션을 띄울지 고치는 자리는 아래 §엔진과 모델은 페르소나가 정합니다에 있습니다.

성공 화면에 등록 명령어가 없다는 것이 이 동작의 전부입니다. 만들었다는 문장과 crontab에 등록했습니다 — 30초 뒤부터 티켓을 물어갑니다.가 뜨고, 목록의 그 워커는 stopped가 아니라 idle로 섭니다. 셸을 열 일이 없습니다.

새 워커에 딸려 오는 것 다섯

  1. 워커 파일 <루트>/workers/<이름>.sh. 기존 워커를 템플릿으로 복사하고 755로 둡니다.
  2. crontab 두 줄:00:30. cron은 정해 둔 시각마다 명령을 대신 실행해 주는 macOS의 기본 기능이고, crontab은 그 예약 목록입니다. 워커 하나에 두 줄인 이유는 cron이 다룰 수 있는 가장 잔 단위가 분이라서입니다.
  3. git 워크트리 <루트>/worktrees/<이름>과 브랜치 wt/<이름>. 브랜치는 같은 코드에서 갈라져 나온 작업 갈래고, 워크트리는 그 갈래를 따로 펼쳐 둔 폴더입니다. 워커마다 자기 폴더 안에서 자기 갈래로 일한다고 보시면 됩니다.
  4. .dira 심링크 — 그 워크트리 안에서 큐를 가리키는 바로가기입니다. 세션이 .dira/tickets/…를 상대경로로 쓰기 때문에 이게 없으면 워커가 자기 티켓을 찾지 못합니다.
  5. 검증. 그 심링크를 되짚어 이 프로젝트의 큐로 풀리는지까지 확인합니다. 링크가 걸려 있다는 것만 보고 넘어가지 않습니다.

의존성 설치(npm install 같은 것)는 하지 않습니다. 이게 이 기능의 천장입니다. 새 워커의 작업 디렉터리에서 프론트엔드 티켓을 처음 물면 node_modules가 없습니다. 프로젝트마다 명령이 달라서 앱이 알 방법이 없습니다.

행 오른쪽 버튼 넷

행마다 reap · 스트림 · 중단(또는 재등록) · 삭제가 붙어 있습니다.

  • reap은 스테일 수거를 그 자리에서 한 번 돌립니다. 세션이 죽었는데 진행중으로 남은 티켓을 대기로 되돌리는 일이고, 원래 워커가 30초마다 알아서 하는 것을 지금 당장 시키는 버튼입니다.
  • 스트림은 도는 세션의 진행 기록을 엽니다. running이면서 물고 있는 티켓을 알 때만 열리고, 참견도 그 안에서 합니다(도는 세션에 말 걸기). 못 여는 행에서도 버튼은 지워지지 않고 흐리게 남습니다. 진행 기록을 남기는 엔진은 claudegrok뿐이라 codexagy로 도는 세션은 이 버튼이 끝내 열리지 않고, 참견은 claude에서만 됩니다. 고장이 아니라 기능 집합이 다를 뿐이고, 티켓 수행은 같습니다.

나머지 셋이 건드리는 것은 서로 다릅니다.

버튼하는 일남는 것
중단crontab에서 그 워커 줄을 뺍니다파일과 작업 디렉터리는 그대로입니다
재등록crontab 줄을 다시 넣습니다중단의 정확한 역방향이고 그것뿐입니다
삭제crontab 줄을 뺀 뒤에 파일을 지웁니다티켓은 하나도 지워지지 않습니다
  • 중단재등록은 같은 자리에 있고 동시에 뜨지 않습니다. 판정은 상태가 아니라 crontab이 합니다. 뺄 줄이 있으면 중단, 없으면 재등록입니다.
  • 중단은 도는 세션을 죽이지 않습니다. 지금 티켓을 물고 있으면 그 티켓이 끝난 뒤 멈춘다고 알려 줍니다.
  • 삭제는 순서가 정해져 있습니다. crontab 먼저, 파일 나중입니다. 뒤집으면 그 1분 사이에 cron이 없는 파일을 실행해 로그에 에러가 쌓입니다. crontab 해제가 실패하면 파일을 지우지 않고 멈춥니다.
  • running인 워커는 삭제가 막힙니다. 돌고 있는 세션과 그 세션이 잡아 둔 자리가 붕 뜨기 때문입니다. 먼저 중단하시고, 물고 있는 티켓이 끝난 뒤에 지우세요.

엔진과 모델은 페르소나가 정합니다

워커 표에 엔진 열이 없습니다. 어느 CLI로 세션을 띄울지는 그 티켓의 persona:가 정하고, 고치는 자리는 페르소나 §디스패치 정책입니다. 엔진 값을 누르면 claude·codex·grok·agy 넷 가운데 하나와 그 안의 모델을 고릅니다. 목록에 없는 모델 이름은 직접 입력으로 넣습니다. 지정 없음으로 두면 그 티켓을 문 워커 자신의 엔진을 씁니다.

같은 절의 상한은 그 페르소나가 한 번에 쥘 수 있는 진행중 티켓 수입니다. 워커를 늘려도 그 페르소나만은 그 수를 안 넘습니다.

도중에 막혔을 때 — 남은 명령만 복사

crontab 쓰기는 macOS에서 앱 관리 승인이 걸려 있어 막힐 수 있고, 워크트리는 이름이 겹치거나 권한에서 걸릴 수 있습니다. 그래도 앱은 되돌리지 않습니다. 어디까지 갔는지와 사유를 그 자리에 보여 주고, 아직 남은 명령만 복사 버튼으로 줍니다. 이미 끝난 단계를 다시 돌리면 오히려 함정을 밟습니다.

그 명령을 이 문서에 적어 두지 않는 까닭도 같습니다. 성공하면 안 보이고, 실패한 화면이 그때의 정확한 경로를 채워서 주니까요.


앱 없이 워커를 손으로 만들고 cron에 직접 거는 절차는 부록 엔진만으로 돌리기에 있습니다.

다음은 동시에 몇 개 돌릴까입니다.