AIForge - Claude Code 무인 자동화 워크플로우 시스템

Table of Contents
- Claude Code, 사람 없이 돌릴 수 없을까?
- 어떤 시스템인가
- 기술 스택
- 아키텍처
- 데이터 모델
- 두 가지 실행 모드
- Fire-and-Forget (비동기 모드)
- Synchronous (동기 모드)
- JIRA 통합: API 없이 브라우저로
- 토큰 관리
- 백그라운드 로그 모니터링
- 설정 시스템: Config vs Status 분리
- Config 키 - 사용자가 변경
- Status 키 - 시스템이 업데이트
- 웹 대시보드 UI
- 시작부터 실행까지: 전체 흐름
- 실제 사용 시나리오: 매일 코드 리뷰 자동화
- JIRA 이슈 자동 처리 시나리오
- 프로젝트 구조
- 설계 결정과 트레이드오프
- Claude CLI를 범용 실행기로 사용한 이유
- PID 기반 상태 추적
- SQLite 선택
- 실행 방법
- 의존성 설치
- 실행
- 또는
- 한계와 향후 계획
- 마무리
Claude Code, 사람 없이 돌릴 수 없을까?
Claude Code CLI는 강력하다. 코드 리뷰, 문서 생성, 버그 수정까지 터미널 한 줄이면 된다. 하지만 매번 사람이 직접 명령어를 입력해야 한다는 점이 아쉽다.
매일 아침 8시에 JIRA 이슈를 확인하고, 새 이슈가 있으면 Claude에게 분석을 맡기고, 토큰 사용량이 넘치면 알아서 멈추고... 이런 걸 자동으로 할 수 있다면?
AIForge는 이 문제를 해결하기 위해 만든 시스템이다. Claude Code CLI를 크론 스케줄링과 JIRA 이슈 폴링으로 무인 운영하는 웹 대시보드다.
GitHub: nasodev/claude-aiforge
어떤 시스템인가
AIForge는 FastAPI 기반의 웹 애플리케이션으로, 핵심 기능은 세 가지다.
| 기능 | 설명 |
|---|---|
| 크론 스케줄링 | APScheduler로 원하는 시간에 Claude Code 자동 실행 |
| JIRA 이슈 폴링 | JIRA에서 조건에 맞는 이슈를 찾아 이슈별 자동 처리 |
| 토큰 관리 | claude.ai 사용량을 모니터링하고 한도 초과 시 자동 일시정지 |
기술 스택
| 영역 | 기술 |
|---|---|
| Backend | Python 3.12+, FastAPI, Uvicorn |
| Database | SQLite (WAL mode), aiosqlite |
| Scheduler | APScheduler 3 (AsyncIO) |
| Frontend | Jinja2 SSR, Tailwind CSS (Dark theme) |
| Process | psutil (PID 모니터링) |
| Port | 8010 |
아키텍처
전체 시스템은 네 개의 레이어로 구성된다.
┌─ FastAPI Web Server (:8010)
│ ├─ Routes (Dashboard, Projects, Schedules, Executions, Settings, Logs)
│ ├─ Jinja2 Templates (SSR 다크 테마 UI)
│ └─ Static Assets (CSS, Icons)
│
├─ APScheduler (AsyncIO)
│ ├─ System Jobs (토큰 체크, 로그 모니터링)
│ └─ Project Schedules (크론 기반 실행)
│
├─ Services Layer
│ ├─ Executor (async/sync 모드 Claude CLI 실행)
│ ├─ Log Checker (PID 모니터링, 로그 파싱)
│ └─ Scheduler (Job 등록 및 생명주기 관리)
│
└─ SQLite Database (WAL mode)
├─ projects
├─ schedules
├─ executions
└─ settings핵심 설계 원칙은 Claude CLI를 범용 실행기로 사용한다는 점이다. JIRA API 클라이언트를 별도로 만들지 않고, Claude CLI + 브라우저 자동화(--chrome)로 JIRA 검색과 작업을 처리한다.
데이터 모델
네 개의 테이블이 전체 시스템 상태를 관리한다.
CREATE TABLE IF NOT EXISTS projects (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL,
type TEXT NOT NULL CHECK (type IN ('jira', 'schedule')),
description TEXT,
jira_search_conditions TEXT, -- JIRA 타입일 때 검색 조건
enabled INTEGER DEFAULT 1,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE IF NOT EXISTS schedules (
id INTEGER PRIMARY KEY AUTOINCREMENT,
project_id INTEGER NOT NULL,
name TEXT NOT NULL,
prompt_template TEXT NOT NULL, -- Claude에게 보낼 프롬프트
work_dir TEXT NOT NULL, -- Claude CLI 작업 디렉토리
cron_expr TEXT NOT NULL, -- 5-field 크론 표현식
status TEXT DEFAULT 'idle'
CHECK (status IN ('idle', 'running', 'paused', 'error')),
FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS executions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
schedule_id INTEGER NOT NULL,
pid INTEGER, -- OS 프로세스 ID
status TEXT DEFAULT 'running'
CHECK (status IN ('running', 'success', 'error', 'timeout', 'killed')),
command TEXT, -- 실행된 CLI 명령어
issue_key TEXT, -- JIRA 이슈 키 (있는 경우)
log_path TEXT, -- Claude 로컬 로그 경로
duration_seconds INTEGER,
FOREIGN KEY (schedule_id) REFERENCES schedules(id) ON DELETE CASCADE
);관계는 단순하다: Project → Schedules → Executions. CASCADE DELETE로 프로젝트를 지우면 하위 데이터가 모두 정리된다.
두 가지 실행 모드
AIForge는 작업 성격에 따라 두 가지 모드로 Claude CLI를 실행한다.
Fire-and-Forget (비동기 모드)
프로젝트 작업용이다. Claude CLI를 서브프로세스로 띄우고 PID만 기록한 뒤 즉시 반환한다.
async def execute_async(
prompt: str,
work_dir: str,
issue_key: str | None = None
) -> dict:
"""Fire-and-forget 실행. PID 기록 후 즉시 반환."""
env = os.environ.copy()
env.pop("CLAUDECODE", None) # 재귀 실행 방지
cmd = [
"claude", "--chrome",
"--dangerously-skip-permissions",
"-p", prompt
]
process = await asyncio.create_subprocess_exec(
*cmd,
cwd=work_dir,
env=env,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
return {"pid": process.pid, "status": "running"}CLAUDECODE 환경 변수를 제거하는 것이 핵심이다. 이걸 안 하면 Claude CLI가 자기 자신 안에서 호출된 것으로 인식하여 재귀 실행 문제가 발생한다.
Synchronous (동기 모드)
시스템 작업용이다. 토큰 사용량 확인, JIRA 이슈 폴링 등 결과를 바로 파싱해야 하는 작업에 사용한다.
async def execute_sync(
prompt: str,
work_dir: str,
timeout: int = 180
) -> dict:
"""동기 실행. 결과를 JSON으로 파싱하여 반환."""
# ... subprocess 실행 ...
stdout, stderr = await asyncio.wait_for(
process.communicate(), timeout=timeout
)
# 3단계 JSON 파싱 폴백
result = stdout.decode()
try:
return json.loads(result) # 1. 순수 JSON
except json.JSONDecodeError:
# 2. ```json ... ``` 마크다운 블록에서 추출
match = re.search(r'```json\s*(.*?)\s*```', result, re.DOTALL)
if match:
return json.loads(match.group(1))
# 3. { ... } 객체 리터럴에서 추출
match = re.search(r'\{.*\}', result, re.DOTALL)
if match:
return json.loads(match.group(0))Claude의 출력이 항상 깔끔한 JSON은 아니기 때문에, 3단계 폴백으로 JSON을 추출한다. 순수 JSON → 마크다운 코드 블록 → 객체 리터럴 순으로 시도한다.
JIRA 통합: API 없이 브라우저로
일반적으로 JIRA 연동은 REST API를 사용한다. 하지만 회사 JIRA는 인증이 복잡하고 API 권한 설정이 번거롭다.
AIForge는 다른 접근을 했다. Claude CLI + Chrome 브라우저 자동화로 JIRA를 직접 조작한다.
1. 크론 트리거 → 스케줄 실행
2. 토큰 한도 체크
3. Claude 동기 실행: "다음 조건에 해당하는 JIRA 검색 해줘 {conditions}"
4. JSON 파싱 → 이슈 키 목록 추출
5. 이슈별로:
- 이미 실행 중인지 확인
- prompt_template에서 {issue_key} 치환
- Claude CLI 비동기 실행
- PID 기록
6. 백그라운드 로그 체커가 완료 모니터링JIRA 검색 프롬프트는 이렇게 구성된다:
poll_prompt = (
f"다음 조건에 해당하는 JIRA 검색 해줘 {conditions} "
"조회해서 json 으로 알려줘 출력에 다른값은 넣지 않고 json 만 넣어줘"
)Claude가 반환하는 형식:
[{"key": "FAS-130"}, {"key": "FAS-131"}]이 방식의 장점은 JIRA API 인증 설정이 필요 없다는 것이다. 브라우저에 이미 로그인되어 있으면 그대로 사용한다.
토큰 관리
Claude Code는 사용량 제한이 있다. AIForge는 주기적으로 토큰 사용량을 체크하고, 한도에 가까워지면 자동으로 작업을 일시정지한다.
1. 시스템 잡 (기본 60분 간격)
2. Claude 동기 실행: "claude.ai/settings/usage 접속해서 사용량 확인"
3. 세션 사용률 / 주간 사용률 파싱
4. settings 테이블에 상태 저장
5. 한도 초과 시 → 모든 프로젝트 자동 일시정지설정은 UI에서 쉽게 변경할 수 있다.
token_check_config = {
"enabled": True,
"interval_minutes": 60,
"session_limit_percent": 80, # 세션 사용량 80% 이상이면 정지
"weekly_limit_percent": 90 # 주간 사용량 90% 이상이면 정지
}백그라운드 로그 모니터링
Fire-and-Forget으로 실행한 Claude CLI의 완료 여부는 어떻게 알까? 백그라운드 로그 체커가 주기적으로 확인한다.
async def check_running_executions():
"""실행 중인 모든 작업의 상태를 확인한다."""
running = await get_running_executions()
for execution in running:
pid = execution["pid"]
# PID가 아직 살아있는지 확인
if not psutil.pid_exists(pid):
# 프로세스가 종료됨 → 로그 파일 분석
log_content = await read_log_file(execution["log_path"])
if has_error_pattern(log_content):
await update_execution_status(execution["id"], "error")
else:
await update_execution_status(execution["id"], "success")
# 실행 시간 계산 및 스케줄 상태 업데이트
await update_schedule_status(execution["schedule_id"], "idle")psutil로 PID 존재 여부를 확인하는 방식이다. stdout을 지속적으로 폴링하는 것보다 가볍고, 수백 개의 동시 실행도 문제없이 모니터링할 수 있다.
설정 시스템: Config vs Status 분리
설정 데이터를 두 종류로 나눈 것이 설계 포인트다.
| 구분 | 설명 | 예시 |
|---|---|---|
| Config (사용자 의도) | UI에서 편집 가능한 설정값 | token_check_config, log_monitor_config, global |
| Status (런타임 상태) | 시스템이 자동 업데이트하는 상태값 | token_check_status, log_monitor_status |
# Config 키 - 사용자가 변경
CONFIG_KEYS = ["token_check_config", "log_monitor_config", "global"]
# Status 키 - 시스템이 업데이트
STATUS_KEYS = ["token_check_status", "log_monitor_status"]Config를 변경하면 APScheduler가 자동으로 재로드된다. 예를 들어 토큰 체크 간격을 60분에서 30분으로 바꾸면, 기존 잡이 제거되고 새 간격으로 재등록된다.
웹 대시보드 UI
다크 테마 기반의 SSR 웹 UI를 통해 전체 시스템을 관리한다.
| 페이지 | 기능 |
|---|---|
| Dashboard | KPI 통계, 토큰 사용량, 실행 중 작업 현황 |
| Projects | 프로젝트 생성/편집/토글 (JIRA 또는 스케줄 타입) |
| Schedules | 크론 스케줄 관리, 수동 실행 트리거 |
| Executions | 실행 이력 조회, 상태별 필터링 |
| Settings | 토큰 체크, 로그 모니터링, 글로벌 설정 |
| Logs | claude-code-log HTML 리포트 생성 |
대시보드에서 한눈에 파악할 수 있는 정보:
- 전체 프로젝트 수와 활성 상태
- 실행 중인 작업 수
- 최근 실행 결과 (성공/실패)
- 토큰 사용률 (세션/주간)
시작부터 실행까지: 전체 흐름
서버가 시작되면 다음 순서로 초기화된다.
run.py (uvicorn 엔트리)
↓
app.main:lifespan (FastAPI lifespan)
├─ init_db()
│ ├─ schema.sql 읽기
│ ├─ 테이블 생성 (없으면)
│ └─ 기본 설정값 삽입
│
└─ init_scheduler()
├─ _register_system_jobs()
│ ├─ 로그 모니터 (interval)
│ └─ 토큰 체크 (interval)
│
└─ _register_project_schedules()
└─ DB에서 활성 스케줄 로드
└─ APScheduler 크론 잡으로 등록실제 사용 시나리오: 매일 코드 리뷰 자동화
- 프로젝트 생성: type=schedule, name="daily-review"
- 스케줄 설정: cron=
0 8 * * 1-5(평일 오전 8시) - 프롬프트 작성: "최근 코드 변경사항을 리뷰하고 팀별로 정리해줘"
- 작업 디렉토리:
/home/dev/project-alpha
월요일 오전 8시가 되면:
- APScheduler가 잡을 트리거
- 토큰 사용량 체크 (동기 호출)
- 세션 한도 80% 미만이면 진행
claude --chrome -p "최근 코드 변경사항을..."비동기 실행- PID가 executions 테이블에 기록
- 스케줄 상태 → running
- 10분마다 로그 체커가 PID 확인
- 완료 시 → execution status=success, 스케줄 idle 복귀
JIRA 이슈 자동 처리 시나리오
- 프로젝트 생성: type=jira, 검색 조건="프로젝트=FAS AND 상태=대기중"
- 스케줄 설정: cron=
*/30 * * * *(30분마다) - 프롬프트 템플릿: "
{issue_key}이슈를 분석하고 해결 방안을 코멘트로 작성해줘"
30분마다:
- Claude가 JIRA에서 조건에 맞는 이슈 검색
[{"key": "FAS-130"}, {"key": "FAS-131"}]반환- 각 이슈별로 프롬프트에 키 치환 후 비동기 실행
- 이미 실행 중인 이슈는 스킵
프로젝트 구조
aiforge/
├── run.py # uvicorn 엔트리포인트
├── schema.sql # DB 스키마
├── requirements.txt # Python 의존성
│
├── app/
│ ├── main.py # FastAPI 앱, lifespan, 라우트 등록
│ ├── database.py # SQLite 헬퍼, 설정 관리
│ │
│ ├── services/
│ │ ├── scheduler.py # APScheduler, 실행 로직 (352 LOC)
│ │ ├── executor.py # Claude CLI 서브프로세스 관리 (200 LOC)
│ │ └── log_checker.py # PID 모니터링, 로그 파싱 (124 LOC)
│ │
│ ├── routes/
│ │ ├── dashboard.py # 대시보드 통계
│ │ ├── projects.py # 프로젝트 CRUD
│ │ ├── schedules.py # 스케줄 CRUD + 수동 실행
│ │ ├── executions.py # 실행 이력
│ │ ├── settings.py # 설정 관리
│ │ ├── logs.py # 로그 리포트
│ │ └── templates.py # 워크스페이스 템플릿
│ │
│ └── templates/ # Jinja2 HTML 템플릿 (11개)
│
└── docs/
├── ARCHITECTURE.md
└── PROJECT_ANALYSIS.md총 코드량은 약 1,600 LOC. 전체 서비스를 이 정도 코드로 구현할 수 있는 이유는 Claude CLI를 범용 실행기로 활용했기 때문이다. JIRA 클라이언트, 웹 스크래핑 로직 등을 별도로 구현하지 않아도 된다.
설계 결정과 트레이드오프
Claude CLI를 범용 실행기로 사용한 이유
JIRA REST API를 직접 호출하면 인증 설정(OAuth, API Token)이 필요하고, 응답 파싱 로직도 작성해야 한다. Claude CLI + --chrome 플래그를 사용하면 브라우저에 이미 로그인된 세션을 그대로 활용할 수 있다.
단점도 있다. API보다 느리고, Claude의 응답이 항상 예측 가능하지 않다. 하지만 자동화 시스템의 핵심은 속도가 아니라 무인 운영이다. 30분마다 한 번 실행되는 폴링에서 API 대비 10초 더 걸리는 것은 문제가 되지 않는다.
PID 기반 상태 추적
stdout을 지속적으로 감시하는 대신 PID 존재 여부만 확인한다.
- 장점: 리소스 소비가 적다. 수백 개의 동시 실행도 관리할 수 있다.
- 단점: 실시간 진행 상황을 알 수 없다. 완료 후에야 성공/실패를 판단한다.
SQLite 선택
PostgreSQL이나 Redis 같은 외부 DB 대신 SQLite를 선택했다. WAL 모드를 사용하면 동시 읽기/쓰기가 가능하고, 별도의 DB 서버 관리가 필요 없다. 단일 머신에서 돌아가는 시스템에 SQLite는 충분하다.
실행 방법
# 의존성 설치
pip install -r requirements.txt
# 실행
python run.py
# 또는
uvicorn app.main:app --host 0.0.0.0 --port 8010 --reload브라우저에서 http://localhost:8010에 접속하면 대시보드가 열린다.
주의사항: Claude Code CLI가 시스템에 설치되어 있어야 한다.
--chrome플래그를 사용하므로 Chrome 브라우저도 필요하다. 또한--dangerously-skip-permissions플래그가 포함되므로, 신뢰할 수 있는 네트워크에서만 사용해야 한다.
한계와 향후 계획
현재 v0.1.0이고, 몇 가지 한계가 있다.
| 한계 | 설명 |
|---|---|
| 인증 없음 | UI에 로그인 기능이 없다 (신뢰 네트워크 전제) |
| 단일 머신 | 분산 스케줄링 미지원 |
| 로그 파싱 | 에러 패턴 매칭이 기본적인 수준 |
| API 없음 | HTML 폼만 있고 REST API 엔드포인트가 없다 |
| 웹훅 없음 | 외부 이벤트 트리거 미지원 |
다만 이런 한계는 의도된 것이다. 단일 머신에서 Claude Code를 무인 자동화하는 것이 핵심 목적이고, 그 목적에는 현재 구조로 충분하다.
마무리
AIForge의 핵심 아이디어는 단순하다. Claude Code CLI를 cron처럼 스케줄링하자. 여기에 JIRA 이슈 폴링, 토큰 관리, 실행 모니터링을 얹어서 무인 자동화 시스템을 만들었다.
총 1,600줄의 Python 코드로 이 모든 것을 구현할 수 있었던 것은, Claude CLI 자체가 이미 강력한 실행기이기 때문이다. JIRA 검색도, 토큰 사용량 확인도, 코드 리뷰도 전부 Claude에게 프롬프트로 지시하면 된다. AIForge는 그 지시를 자동으로, 반복적으로, 무인으로 수행할 수 있게 해주는 껍데기다.
AI 시대의 자동화는 복잡한 코드를 작성하는 게 아니라, AI에게 적절한 지시를 적절한 타이밍에 보내는 것이 핵심이다. AIForge가 그 첫 번째 시도다.