diff --git a/ai/ralph-loop/AGENTS.md b/ai/ralph-loop/AGENTS.md new file mode 100644 index 00000000..669d3477 --- /dev/null +++ b/ai/ralph-loop/AGENTS.md @@ -0,0 +1,62 @@ +# AGENTS.md - 에이전트 운영 가이드 + + + +## 빌드 & 테스트 명령어 [필수] + +```bash +# 빌드 +go build -o server . + +# 테스트 실행 +go test -v ./... + +# 테스트 + 커버리지 +go test -cover ./... + +# 린트 (설치 필요: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest) +golangci-lint run ./... +``` + +## 검증 규칙 [필수] + +- 커밋 전 `go test ./...` 통과 필수 +- `go build .` 성공 필수 +- `go vet ./...` 경고 없어야 함 + +## 프로젝트 구조 [필수] + +``` +ai/ralph-loop/ +├── main.go # HTTP 서버 (엔트리포인트) +├── main_test.go # 테스트 +├── specs/ # 요구사항 스펙 (1 파일 = 1 관심사) +├── prd.json # 진행 상태 추적 +└── IMPLEMENTATION_PLAN.md # 구현 계획 (Ralph가 생성/관리) +``` + +## 코딩 컨벤션 [필수] + +- Go 표준 프로젝트 레이아웃 준수 +- 에러는 즉시 처리 (`if err != nil`) +- JSON 태그는 camelCase가 아닌 snake_case 사용 +- 테스트 함수명: `TestXxx_설명` 형식 + +## 발견된 패턴 [권장] + + + +- `writeJSON()` 헬퍼로 JSON 응답 통일 +- 새 엔드포인트는 `Server.routes()`에 등록 +- 환경변수 `PORT`로 포트 설정 (기본값: 8080) diff --git a/ai/ralph-loop/CLAUDE.md b/ai/ralph-loop/CLAUDE.md new file mode 100644 index 00000000..cf9c63b6 --- /dev/null +++ b/ai/ralph-loop/CLAUDE.md @@ -0,0 +1,33 @@ +# CLAUDE.md - Ralph Loop용 설정 + + + +## 프로젝트 개요 + +간단한 Go HTTP API 서버 프로젝트. Ralph Loop 데모용. + +## 핵심 규칙 + +### 작업 방식 +- IMPLEMENTATION_PLAN.md를 참조하여 작업 선택 +- **한 번에 하나의 작업만** 수행 +- 구현 전 코드베이스 검색으로 중복 확인 + +### 코드 품질 +- `go test ./...` 통과 필수 +- `go build .` 성공 필수 +- 플레이스홀더/TODO 금지 — 완전한 구현만 + +### 커밋 규칙 +- 테스트 통과 후에만 커밋 +- 커밋 메시지: 한국어, `[#이슈번호] 간결한 설명` 형식 + +## 빌드 & 테스트 + +```bash +go build -o server . +go test -v ./... +``` diff --git a/ai/ralph-loop/PROMPT_build.md b/ai/ralph-loop/PROMPT_build.md new file mode 100644 index 00000000..ea6577c5 --- /dev/null +++ b/ai/ralph-loop/PROMPT_build.md @@ -0,0 +1,57 @@ +# Ralph Loop - Building Mode + +## 역할 + +당신은 소프트웨어 엔지니어입니다. 계획에 따라 한 번에 하나의 작업을 구현합니다. + +## 지시사항 + +### 0. 컨텍스트 로드 + +0a. `specs/` 디렉토리의 모든 스펙 파일을 읽어라 +0b. `IMPLEMENTATION_PLAN.md`를 읽어라 +0c. 프로젝트 소스 코드를 파악하라 + +### 1. 작업 선택 + + + +IMPLEMENTATION_PLAN.md에서 **가장 중요한 미완료 항목 하나**를 선택하라. +구현 전에 코드베이스를 검색하여 이미 구현되어 있지 않은지 확인하라. + +### 2. 구현 + + + +- 스펙에 맞게 완전한 구현을 하라 +- 테스트가 없으면 추가하라 +- 관련 없는 코드를 수정하지 마라 + +### 3. 검증 + + + +- 변경한 코드에 대한 테스트를 실행하라 +- 빌드가 성공하는지 확인하라 +- 모든 테스트가 통과해야 다음 단계로 진행하라 + +### 4. 커밋 & 플랜 업데이트 + +- 테스트 통과 후 `git add -A && git commit` +- IMPLEMENTATION_PLAN.md에서 완료한 항목을 업데이트하라 +- 새로운 이슈를 발견했으면 플랜에 추가하라 + +## 중요 규칙 + +**루프당 하나의 작업만 수행하라.** +가정하지 마라 — 코드 검색으로 확인하라. +빌드/테스트 에러가 있으면 커밋하지 마라. diff --git a/ai/ralph-loop/PROMPT_plan.md b/ai/ralph-loop/PROMPT_plan.md new file mode 100644 index 00000000..cdbc4da9 --- /dev/null +++ b/ai/ralph-loop/PROMPT_plan.md @@ -0,0 +1,46 @@ +# Ralph Loop - Planning Mode + +## 역할 + +당신은 소프트웨어 아키텍트입니다. 요구사항을 분석하고 구현 계획을 수립합니다. + +## 지시사항 + + + +### 0. 컨텍스트 로드 + +0a. `specs/` 디렉토리의 모든 스펙 파일을 읽어라 +0b. `IMPLEMENTATION_PLAN.md`가 존재하면 읽어라 (없으면 새로 생성) +0c. `src/` 또는 프로젝트 소스 코드를 분석하라 + +### 1. Gap Analysis (스펙 vs 현재 코드) + + + +- specs/의 각 요구사항과 현재 코드를 비교하라 +- 누락된 기능, TODO, 플레이스홀더, 불일치 패턴을 찾아라 +- 코드에 존재하지 않는다고 가정하지 마라 — 반드시 검색으로 확인하라 + +### 2. IMPLEMENTATION_PLAN.md 생성/업데이트 + +- 우선순위가 높은 순서대로 정렬된 태스크 목록을 작성하라 +- 각 태스크는 한 문장으로 설명 가능해야 한다 +- 의존성 순서를 고려하라 + +## 중요 규칙 + + + +**절대로 코드를 구현하지 마라. 분석과 계획만 수행하라.** +구현되지 않은 기능이 정말로 없는지 코드 검색으로 확인하라. +가정하지 말고 확인하라. diff --git a/ai/ralph-loop/README.md b/ai/ralph-loop/README.md new file mode 100644 index 00000000..a9476a89 --- /dev/null +++ b/ai/ralph-loop/README.md @@ -0,0 +1,64 @@ +# Ralph Loop Demo + +Ralph Loop(Ralph Wiggum Technique) 패턴을 보여주는 데모 프로젝트입니다. + +## 프로젝트 구조 + +``` +ai/ralph-loop/ +├── loop.sh # Ralph Loop 실행 스크립트 +├── PROMPT_plan.md # Planning 모드 프롬프트 +├── PROMPT_build.md # Building 모드 프롬프트 +├── AGENTS.md # 에이전트 운영 가이드 +├── CLAUDE.md # Claude Code 설정 +├── specs/ # 요구사항 스펙 파일 +│ ├── api-server.md # API 서버 스펙 +│ └── health-check.md # 헬스체크 스펙 +├── prd.json # PRD JSON (진행 상태 추적) +├── main.go # HTTP API 서버 +├── main_test.go # 테스트 +└── go.mod # Go 모듈 (루트 모듈에 포함) +``` + +## 실행 방법 + +### 서버 실행 + +```bash +go run . +# http://localhost:8080/api/hello +# http://localhost:8080/health +``` + +### 테스트 + +```bash +go test -v ./... +``` + +### Ralph Loop 실행 + +```bash +# Planning 모드 (계획 수립, 최대 3회) +./loop.sh plan 3 + +# Building 모드 (구현, 최대 10회) +./loop.sh build 10 + +# Building 모드 (무제한) +./loop.sh +``` + +## Ralph Loop 워크플로우 + +1. **Phase 1 - Specs**: `specs/` 디렉토리에 요구사항 스펙 작성 +2. **Phase 2 - Planning**: `./loop.sh plan 3` 으로 IMPLEMENTATION_PLAN.md 생성 +3. **Phase 3 - Building**: `./loop.sh build 10` 으로 자율 구현 + +## Best Practices + +각 파일에 DO/DON'T 주석이 포함되어 있습니다. 자세한 내용은 각 파일을 참조하세요. + +## 관련 블로그 + +- [Ralph Loop 완벽 가이드 - AI 에이전트 자율 개발 패턴](https://blog.advenoh.pe.kr) diff --git a/ai/ralph-loop/loop.sh b/ai/ralph-loop/loop.sh new file mode 100755 index 00000000..49f064ca --- /dev/null +++ b/ai/ralph-loop/loop.sh @@ -0,0 +1,78 @@ +#!/bin/bash +# +# Ralph Loop 실행 스크립트 +# +# ✅ DO: 모드와 반복 횟수를 파라미터로 제어 +# ❌ DON'T: 하드코딩된 무한 루프만 사용 +# +# Usage: +# ./loop.sh # Build 모드, 무제한 +# ./loop.sh plan # Plan 모드, 무제한 +# ./loop.sh plan 3 # Plan 모드, 최대 3회 +# ./loop.sh build 10 # Build 모드, 최대 10회 +# + +set -euo pipefail + +# === 설정 === +MODE="build" +PROMPT_FILE="PROMPT_build.md" +MAX_ITERATIONS=0 +ITERATION=0 +CURRENT_BRANCH=$(git branch --show-current) + +# === 인자 파싱 === +if [ "${1:-}" = "plan" ]; then + MODE="plan" + PROMPT_FILE="PROMPT_plan.md" + MAX_ITERATIONS=${2:-0} +elif [[ "${1:-}" =~ ^[0-9]+$ ]]; then + MAX_ITERATIONS=$1 +elif [ "${1:-}" = "build" ]; then + MAX_ITERATIONS=${2:-0} +fi + +# === 사전 검증 === +# ✅ DO: 프롬프트 파일 존재 여부 확인 +# ❌ DON'T: 파일 없이 실행하여 에러 발생 +if [ ! -f "$PROMPT_FILE" ]; then + echo "Error: $PROMPT_FILE 파일이 없습니다." + exit 1 +fi + +echo "=== Ralph Loop ===" +echo "Mode: $MODE" +echo "Prompt: $PROMPT_FILE" +echo "Max iterations: $([ $MAX_ITERATIONS -gt 0 ] && echo $MAX_ITERATIONS || echo 'unlimited')" +echo "Branch: $CURRENT_BRANCH" +echo "===================" +echo "" + +# === 메인 루프 === +while true; do + # 반복 횟수 제한 체크 + if [ $MAX_ITERATIONS -gt 0 ] && [ $ITERATION -ge $MAX_ITERATIONS ]; then + echo "최대 반복 횟수($MAX_ITERATIONS)에 도달했습니다." + break + fi + + ITERATION=$((ITERATION + 1)) + echo "--- Iteration $ITERATION ---" + + # ✅ DO: headless 모드(-p)로 실행 + 구조화된 출력 + # ❌ DON'T: 대화형 모드로 실행 (자동화 불가) + cat "$PROMPT_FILE" | claude -p \ + --dangerously-skip-permissions \ + --output-format=stream-json \ + --model opus \ + --verbose + + # ✅ DO: 매 반복마다 push하여 진행 상태를 원격에 보관 + # ❌ DON'T: 로컬에만 커밋하여 작업 유실 위험 + git push origin "$CURRENT_BRANCH" 2>/dev/null || true + + echo "--- Iteration $ITERATION 완료 ---" + echo "" +done + +echo "=== Ralph Loop 종료 (총 ${ITERATION}회 실행) ===" diff --git a/ai/ralph-loop/main.go b/ai/ralph-loop/main.go new file mode 100644 index 00000000..a0f5394c --- /dev/null +++ b/ai/ralph-loop/main.go @@ -0,0 +1,82 @@ +package main + +import ( + "encoding/json" + "fmt" + "log" + "net/http" + "os" + "time" +) + +// Server는 HTTP API 서버 설정을 담는 구조체이다. +type Server struct { + Port string + Router *http.ServeMux +} + +// Response는 API 응답 구조체이다. +type Response struct { + Status string `json:"status"` + Message string `json:"message"` + Time string `json:"time"` +} + +// HealthResponse는 헬스체크 응답 구조체이다. +type HealthResponse struct { + Status string `json:"status"` + Uptime string `json:"uptime"` +} + +var startTime = time.Now() + +// NewServer는 새로운 Server 인스턴스를 생성한다. +func NewServer(port string) *Server { + s := &Server{ + Port: port, + Router: http.NewServeMux(), + } + s.routes() + return s +} + +func (s *Server) routes() { + s.Router.HandleFunc("GET /api/hello", handleHello) + s.Router.HandleFunc("GET /health", handleHealth) +} + +func handleHello(w http.ResponseWriter, r *http.Request) { + resp := Response{ + Status: "ok", + Message: "Hello from Ralph Loop demo!", + Time: time.Now().Format(time.RFC3339), + } + writeJSON(w, http.StatusOK, resp) +} + +func handleHealth(w http.ResponseWriter, r *http.Request) { + resp := HealthResponse{ + Status: "healthy", + Uptime: time.Since(startTime).Round(time.Second).String(), + } + writeJSON(w, http.StatusOK, resp) +} + +func writeJSON(w http.ResponseWriter, status int, v any) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(status) + if err := json.NewEncoder(w).Encode(v); err != nil { + log.Printf("JSON 인코딩 실패: %v", err) + } +} + +func main() { + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + + srv := NewServer(port) + fmt.Printf("서버 시작: http://localhost:%s\n", port) + log.Fatal(http.ListenAndServe(":"+port, srv.Router)) +} diff --git a/ai/ralph-loop/main_test.go b/ai/ralph-loop/main_test.go new file mode 100644 index 00000000..f8bb713a --- /dev/null +++ b/ai/ralph-loop/main_test.go @@ -0,0 +1,48 @@ +package main + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestHandleHello_정상응답(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/api/hello", nil) + w := httptest.NewRecorder() + + handleHello(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + assert.Equal(t, "application/json", w.Header().Get("Content-Type")) + + var resp Response + err := json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(t, err) + assert.Equal(t, "ok", resp.Status) + assert.Equal(t, "Hello from Ralph Loop demo!", resp.Message) + assert.NotEmpty(t, resp.Time) +} + +func TestHandleHealth_정상응답(t *testing.T) { + req := httptest.NewRequest(http.MethodGet, "/health", nil) + w := httptest.NewRecorder() + + handleHealth(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var resp HealthResponse + err := json.NewDecoder(w.Body).Decode(&resp) + assert.NoError(t, err) + assert.Equal(t, "healthy", resp.Status) + assert.NotEmpty(t, resp.Uptime) +} + +func TestNewServer_라우터설정(t *testing.T) { + srv := NewServer("8080") + assert.NotNil(t, srv.Router) + assert.Equal(t, "8080", srv.Port) +} diff --git a/ai/ralph-loop/prd.json b/ai/ralph-loop/prd.json new file mode 100644 index 00000000..cbf2cfea --- /dev/null +++ b/ai/ralph-loop/prd.json @@ -0,0 +1,39 @@ +{ + "name": "ralph-loop-demo", + "description": "Ralph Loop 데모용 Go HTTP API 서버", + "branchName": "feat/ralph-loop-demo", + "userStories": [ + { + "id": "US-001", + "title": "GET /api/hello 엔드포인트 구현", + "priority": 1, + "passes": true, + "acceptanceCriteria": [ + "GET /api/hello가 200 상태코드를 반환한다", + "응답이 JSON 형식이다", + "status, message, time 필드가 포함된다" + ] + }, + { + "id": "US-002", + "title": "GET /health 헬스체크 엔드포인트 구현", + "priority": 2, + "passes": true, + "acceptanceCriteria": [ + "GET /health가 200 상태코드를 반환한다", + "status가 healthy이다", + "uptime이 비어있지 않다" + ] + }, + { + "id": "US-003", + "title": "단위 테스트 작성", + "priority": 3, + "passes": true, + "acceptanceCriteria": [ + "모든 핸들러에 테스트가 존재한다", + "go test ./... 통과" + ] + } + ] +} diff --git a/ai/ralph-loop/specs/api-server.md b/ai/ralph-loop/specs/api-server.md new file mode 100644 index 00000000..41a45ed7 --- /dev/null +++ b/ai/ralph-loop/specs/api-server.md @@ -0,0 +1,29 @@ +# API Server + + + +## 요구사항 + +HTTP API 서버가 `/api/hello` 엔드포인트에서 JSON 응답을 반환한다. + +## 세부 사항 + +- `GET /api/hello`로 접근 가능 +- 응답 형식: JSON +- 응답 필드: `status`, `message`, `time` +- HTTP 상태 코드: 200 +- Content-Type: `application/json` + +## 수락 기준 + +- [ ] GET /api/hello가 200 상태코드를 반환한다 +- [ ] 응답이 올바른 JSON 형식이다 +- [ ] status 필드가 "ok"이다 +- [ ] time 필드가 RFC3339 형식이다 diff --git a/ai/ralph-loop/specs/health-check.md b/ai/ralph-loop/specs/health-check.md new file mode 100644 index 00000000..09d21fe1 --- /dev/null +++ b/ai/ralph-loop/specs/health-check.md @@ -0,0 +1,23 @@ +# Health Check + + + +## 요구사항 + +헬스체크 엔드포인트가 서버 상태와 가동 시간을 반환한다. + +## 세부 사항 + +- `GET /health`로 접근 가능 +- 응답 형식: JSON +- 응답 필드: `status`, `uptime` +- 서버 시작 시점부터의 가동 시간을 초 단위로 반환 + +## 수락 기준 + +- [ ] GET /health가 200 상태코드를 반환한다 +- [ ] status 필드가 "healthy"이다 +- [ ] uptime 필드가 비어있지 않다