30개의 cron cron이 n이 돌고 있었다 — 아무도 그게 진짜 살아있는지 확인하지 않았다

2026년 4월, launchd 서비스 6개 중 5개가 죽어 있었다. 6일 동안 아무도 몰랐다. 그때 만든 게 launchd watchdog이었다. 하지만 그걸로 충분하지 않았다.

launchd는 프로세스가 살아있는지만 본다. cron job이 jobs.json에서 next_run_at=null로 멈춰 있어도 launchd는 모른다. 스크립트가 조용히 에러를 내도 모른다. output 파일이 3개월째 비어 있어도 모른다.

자동화가 늘어날수록, 조용히 죽는 지점도 늘어난다. 그리고 그걸 가장 늦게 알아차리는 사람은 나다.

이 글에서 다루는 것

launchd KeepAlive가 커버하지 못하는 실패 유형 세 가지 cron health check — jobs.json을 읽고 죽은 job을 찾아내는 단일 Python 스크립트 output audit — cron output이 멈춰 있으면 job 자체보다 먼저 감지하는 패턴 AGENTS.md checklist — 사람이 새 cron job을 추가할 때 실수 방지 자동화 검증도 자동화해야 하는 이유 — Tiered Autonomy의 Tier 1 확장

Slice 1: launchd는 프로세스만 보고, 논리는 모른다

Drewgent의 launchd 서비스는 전부 KeepAlive로 설정되어 있다:

KeepAlive { SuccessfulExit: false, ThrottleInterval: 10 }

크래시하면 10초 후 재시작. 재부팅하면 RunAtLoad로 자동 실행. 이건 작동한다. opencode serve, discord bot, cloudflared tunnel — 프로세스가 죽으면 알아서 살아난다.

문제는 프로세스는 살아있는데 job이 죽은 경우다.

drewgent_cron.py는 launchd가 60초마다 실행해준다. 프로세스는 잘 돈다. 그런데 그 안의 개별 job 중 하나가 next_run_at=null이 되어도, 프로세스는 계속 잘 돈다. 아무도 알려주지 않는다.

이걸 처음 발견한 건 trend-evaluate-legacy였다. n8n을 제거하는 과정에서 이 cron job이 비활성화되었는데, 남아있는 jobs.json 엔트리 때문에 파싱 에러가 나고 있었다. cron_runner는 그 job을 건너뛰었고, 나는 3주 동안 몰랐다. 에러 로그조차 없었다.

Slice 2: cron health check — jobs.json을 정기적으로 검사하라

Phase 3 overhaul에서 만든 cron_health_check.py가 하는 일은 단순하다:

  1. jobs.json을 읽는다 (enabled: true만)
  2. 각 job의 last_run_at을 현재 시간과 비교
  3. 예정된 다음 실행 시간이 지났는데 실행 기록이 없으면 WARNING
  4. output 파일이 있는 job은 마지막 output 생성일 확인
  5. 상태는 healthy / stale / stalled / dead 네 단계로 분류
  6. Discord로 요약 리포트 전송

핵심은 LLM이 전혀 필요하지 않다는 거다. pure Python 스크립트 하나. schedule 라이브러리도 필요 없다. 그냥 datetime 비교와 json 읽기, 그리고 Discord webhook 전송. 토큰 비용 0.

매일 05:00에 실행된다. 사람이 일어나기 전에 전날 cron 상태 리포트가 Discord에 도착해 있다.

Slice 3: output audit — 로그가 멈추면 job보다 먼저 감지하라

cron job 중에는 output을 파일로 남기는 것들이 있다. SEO 분석 결과, trend collector 로그, housekeeper 리포트. 이 파일들의 마지막 수정일(mtime)을 추적하면 job 자체의 상태보다 더 빨리 이상을 감지할 수 있다.

예를 들어 seo_analyzer.sh는 매일 실행된다. output 디렉토리에 오늘 날짜 파일이 없으면, job이 실행 중인지 여부와 관계없이 뭔가 잘못됐다.

output audit은 이걸 확인한다:

출력 파일이 예상 주기보다 오래되었는가 → stale output 출력 파일은 있는데 크기가 0인가 → empty output 예상되는 출력 파일이 아예 없는가 → missing output

이 세 가지 신호는 cron runner가 정상 실행됐다고 보고하더라도 발생할 수 있다. 실행과 결과는 다르다.

Slice 4: 사람도 실수한다 — AGENTS.md checklist

cron job이 죽는 원인 중 상당수는 처음 추가할 때 발생한다:

schedule 필드를 string으로 넣음 → parse_schedule() crash Discord 채널 ID 오타 → 알림이 엉뚱한 채널로 감 script 파일 경로 틀림 → job은 실행됐는데 아무 일도 안 함 output 디렉토리 미생성 → PermissionError

이걸 막기 위해 AGENTS.md에 Cron Job Creation Checklist를 박았다. 새 cron job을 추가할 때 반드시 확인할 7개 항목이다. 에이전트가 cron job을 추가하는 코드를 짤 때 이 체크리스트를 먼저 읽게 되어 있다.

규칙 자체를 시스템 프롬프트 안에 심은 셈이다. 사람이 일일이 기억하지 않아도, 에이전트가 실수를 막아준다.

실패 모드 — 무엇이 어떻게 죽는가

실패 유형원인launchd가 감지?health check가 감지?

프로세스 크래시메모리 부족, segfaultKeepAlive로 재시작프로세스 다운 감지 job next_run_at=null스케줄 파싱 에러, 설정 누락감지 불가last_run_at 비교 스크립트 에러Python exception, 파일 누락감지 불가output audit으로 간접 감지 output 정체스크립트가 중간에 멈춤감지 불가mtime 감시 의존성 변경pip 업데이트, API 변경감지 불가수동 확인 필요 디스크 풀로그 누적, DB 증가감지 불가디스크 사용량 감시 추가 예정

Tiered Autonomy의 Tier 1 확장

앞서 Tiered Autonomy를 설계하면서 Tier 1은 “문서/코멘트/오타”처럼 위험도가 낮은 작업에 full autonomy를 주는 단계로 정의했다. 하지만 거꾸로 생각하면, Tier 1은 “실패해도 괜찮은” 작업이 아니라, “실패를 감지할 수 있는” 작업이어야 한다.

자동화의 신뢰도는 이렇게 정해진다:

Tier 1: 실패를 즉시 감지하고 보고할 수 있음 → full auto 가능 Tier 2: 실패 감지는 되지만 복구는 수동 → semi-auto 가능 Tier 3: 실패 감지도 안 됨 → 자동화하면 안 됨

0승 8패 자동매매 이야기가 바로 Tier 3의 사례다. 실패를 감지할 방법이 없는데 자동화했다. 자동화의 품질은 실행의 품질이 아니라, 검증의 품질로 결정된다.

이걸 만들고 나서 달라진 것

크게 세 가지다.

첫째, 신뢰. 30개가 넘는 cron job이 돌고 있지만, 이제는 매일 아침 Discord에 health report가 올라온다. “모든 job healthy”라는 한 줄이 주는 안도감은, 시스템을 만든 사람만이 안다.

둘째, 조기 경보. seo_analyzer.sh가 실패했을 때, 나는 그걸 1주일이 아니라 24시간 안에 알게 됐다. 문제가 작을 때 잡을 수 있다. 작은 문제는 고치는 데 10분, 큰 문제는 3시간이다.

셋째, 새 job 추가가 쉬워졌다. AGENTS.md checklist 덕분에, 새로운 cron job을 만들 때 “이거 health check에서 감지되나?”라는 질문이 자동으로 따라붙는다. 검증 가능성(verifiability)이 설계의 일부가 된 거다.

아직 안 되는 것들

정직하게 말하면, 이 시스템도 완벽하지 않다.

output 내용 검증은 안 된다. 파일은 있는데 내용이 엉망이어도 health check는 통과한다. 내용 검증은 LLM이 필요하고, 그건 비용 문제가 있다. health check 자체가 죽으면? 지금은 cron job 중 하나일 뿐이다. 이 job이 실패해도 health check 리포트가 안 온다는 것 외에는 알 방법이 없다. watchdog의 watchdog이 필요할까, 아니면 충분한가. 디스크 full 감지. 아직 안 했다. 로그가 쌓여서 디스크가 꽉 차면 모든 cron job이 동시에 죽을 수 있다.

이건 다음 phase의 숙제다. 지금은 “완벽한 health check”보다 “있는 health check”가 낫다고 판단했다. 완벽을 기다리다 시작도 못 하는 것보다, 불완전하지만 작동하는 게 낫다.

자동화는 믿음이 아니라 설계다

Drewgent를 5개월 운영하면서 배운 게 하나 있다면 이거다. 자동화 시스템이 제일 무서운 순간은 “잘 돌고 있겠지”라고 생각하는 순간이다.

에이전트가 매일 블로그에 글을 쓰고, cron이 24시간 작업을 돌리고, Discord 봇이 명령을 받아 subagent를 호출한다. 이 모든 게 멋져 보인다. 하지만 이 시스템이 조용히 죽고 있다면, 그게 더 무섭다. 눈에 보이지 않는 실패가 가장 회복 비용이 크다.

그래서 Phase 3에서 health check를 만들었다. 30개의 cron job이 진짜 살아있는지, 매일 아침 나 대신 확인해주는 Python 스크립트 하나. 이게 없으면 자동화는 반쪽이다.

자동화한 만큼, 죽었을 때 알려주는 구조도 자동화해야 한다. 그게 Tiered Autonomy의 진짜 시작점이다.