docs: 완전 가이드 추가 — 처음 보는 사람용 전 구간 해설
- docs/GUIDE.md: 20 장 + 3 부록, 배경/원칙/아키텍처/데이터 모델/FSM/계층/ sister-agent/LLM/파일 추출/Git push/대시보드/E2E 시나리오/API/Contract/ 보안/복원력/설치/디렉토리/로드맵/용어집 망라 - docs/GUIDE.pdf: weasyprint 로 렌더, 목차 + 페이지 헤더/풋터 포함 - README.md: 가이드 링크 추가, 상태 섹션 v0.1.2 까지 갱신, 아키텍처 그림 과 설계 원칙 정리
This commit is contained in:
169
README.md
169
README.md
@@ -1,97 +1,136 @@
|
||||
# hanarang-rails
|
||||
|
||||
> **4자매가 달릴 결정론적 레일** — HaNaRang Rails
|
||||
> **4 자매가 달릴 결정론적 레일** — HaNaRang Rails
|
||||
>
|
||||
> _사용자는 출발 버튼만 누른다. 나머지는 자매들이 자동으로 달린다._
|
||||
|
||||
`hanarang-harness`의 후계작. 기존 하네스가 "권고 기반 파이프라인"이라 자매들이 레일을 벗어나 끊기고 엇갈리던 문제를 **강제 기반 결정론 파이프라인**으로 재설계한다.
|
||||
`hanarang-rails` 는 4 개의 AI "자매" 에이전트 (하랑 / 나랑 / 다랑 / 이랑) 가 하나의 요청을 받아 **기획 → 구현 → 리뷰 → 배포**를 자동으로 완주하는 결정론적 파이프라인 오케스트레이터다. 전임자 `hanarang-harness` 가 권고 기반이라 자매들이 중간에 길을 잃던 문제를, XState 유한 상태 기계 (FSM) 와 Sprint Contract 로 물리적으로 강제한다.
|
||||
|
||||
## 왜 다시?
|
||||
- **처음 보는 사람을 위한 완전 가이드**: [`docs/GUIDE.md`](docs/GUIDE.md) / [`docs/GUIDE.pdf`](docs/GUIDE.pdf)
|
||||
- **설계 문서**: [`.plans/design/`](.plans/design/)
|
||||
- **스프린트 명세**: [`.plans/sprints/`](.plans/sprints/)
|
||||
- **실패 감사 (F1–F6)**: [`.plans/failure-audit.md`](.plans/failure-audit.md)
|
||||
|
||||
[`hanarang-harness`](https://git.nabomhalang.co.kr/hanarang/openclaw-harness)에서 발견된 6가지 실패 모드:
|
||||
---
|
||||
|
||||
## 한 문단 요약
|
||||
|
||||
사용자가 `"todo 앱 만들어 줘"` 한 줄을 던지면, rails 오케스트레이터가 **하랑이 (기획) → 나랑이 (구현) → 다랑이 (리뷰) → 이랑이 (배포)** 순서로 파이프라인을 돌린다. 각 자매는 내부에서 **부장/수석/선임/신입** 4 단계 계층으로 태스크를 쪼개서 병렬 실행하고, 만들어낸 코드 파일은 자동으로 Gitea 에 public repo 로 push 되어 즉시 접근 가능한 URL 로 바뀐다. 대시보드에서는 이 모든 과정이 실시간으로 트리 형태로 보인다.
|
||||
|
||||
## 왜 다시 만들었는가
|
||||
|
||||
[`hanarang-harness`](https://git.nabomhalang.co.kr/hanarang/openclaw-harness) 에서 4 개월 운영하며 발견한 6 가지 고질 실패 모드:
|
||||
|
||||
| 코드 | 증상 | 원인 |
|
||||
|---|---|---|
|
||||
| F1 | 하네스 skill bypass — 자매가 worker 혼자 스폰하고 처리 | skill 진입 강제 없음 |
|
||||
| F2 | DoD 자동 강제 실패 — build 통과 = 완료로 간주 | sprint contract / validator 없음 |
|
||||
| F1 | 하네스 skill bypass — 자매가 혼자 worker 스폰 | skill 진입 강제 없음 |
|
||||
| F2 | DoD 자동 강제 실패 — `build` 통과 = 완료로 판정 | sprint contract / validator 없음 |
|
||||
| F3 | QA 단계 누락 — 사용자가 수동으로 다랑이 호출 | 자동 라우팅 없음 |
|
||||
| F4 | 핸드오프 멘션 불안정 — 잘못된 자매 호출 | Lobster 분기가 LLM에 의존 |
|
||||
| F5 | 중간 끊김 — request-timed-out 반복, xhigh 무한대기 | 재시도/fallback 정책 없음 |
|
||||
| F6 | 환경 검증 누락 — "서버에 Docker 없음" 으로 skip 용인 | 환경 전제 검사 없음 |
|
||||
| F4 | 핸드오프 멘션 불안정 — 잘못된 자매 호출 | 분기가 LLM 판단에 의존 |
|
||||
| F5 | 중간 끊김 — `request-timed-out` 반복 | 재시도/에스컬레이션 정책 없음 |
|
||||
| F6 | 환경 검증 누락 — "Docker 없음" 으로 skip 허용 | 환경 전제 검사 없음 |
|
||||
|
||||
## 6가지 원칙
|
||||
본질 한 줄: **"자매가 하네스를 안 타고 본인이 처리한다."**
|
||||
|
||||
1. **결정론적 라우터** — LLM 판단이 아니라 XState FSM으로 자매 간 전이
|
||||
2. **Sprint Contract 강제** — DoD를 Zod 스키마로 정의, validator가 pass/fail 판정
|
||||
3. **Skill 강제 진입** — skill bypass를 hook으로 감지해 차단
|
||||
4. **상태 전이 기반 핸드오프** — 멘션은 사용자 알림 전용, 자매 간 통신은 FSM 상태
|
||||
5. **재시도/에스컬레이션** — timeout 자동 재시도, N회 실패 시 사용자 에스컬레이션
|
||||
6. **QA 체크리스트 강제** — 스프린트 타입별 템플릿, 다랑이가 체크박스 다 채워야 pass
|
||||
## 6 가지 설계 원칙 (하드 룰)
|
||||
|
||||
1. **강제 > 권고** — 모든 파이프라인 전이는 코드로 강제한다.
|
||||
2. **결정론적 FSM** — 자매 간 핸드오프는 XState 상태 전이다.
|
||||
3. **Sprint Contract = 불변 계약** — DoD 를 Zod 스키마로 정의, validator 가 pass/fail 판정.
|
||||
4. **Skill 강제 진입** — skill bypass 를 hook 이 감지해 차단.
|
||||
5. **QA 체크리스트 의무** — 다랑이가 체크박스 전부 채워야 PASS.
|
||||
6. **환경 검증 선행** — 실기동 검증 환경 없으면 스프린트 시작 자체를 거부.
|
||||
|
||||
## 아키텍처 개요
|
||||
|
||||
```
|
||||
사용자 (디스코드)
|
||||
│
|
||||
▼
|
||||
┌────────────────────────────────────┐
|
||||
│ hanarang-rails orchestrator │
|
||||
│ (XState FSM + SQLite + validator) │
|
||||
└──────────────────┬─────────────────┘
|
||||
│
|
||||
┌───────────┼───────────┬───────────┐
|
||||
▼ ▼ ▼ ▼
|
||||
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐
|
||||
│ 하랑 │ │ 나랑 │ │ 다랑 │ │ 이랑 │
|
||||
│Planner│ │ Impl │ │ QA │ │Deploy│
|
||||
└──────┘ └──────┘ └──────┘ └──────┘
|
||||
│ │ │ │
|
||||
└───────────┴─ OpenClaw spawn ──────┘
|
||||
│
|
||||
▼
|
||||
┌────────────┐
|
||||
│ Discord 알림│ ← 사용자 알림 전용
|
||||
└────────────┘
|
||||
사용자 (Discord / Dashboard Web)
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ hanarang-dashboard │ Next.js 16 + NestJS
|
||||
│ /rails, /office, /sisters │
|
||||
└────────────┬────────────────┘
|
||||
│ HTTP
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ hanarang-rails orchestrator │ XState + Prisma + MariaDB
|
||||
│ FSM ─ Contract ─ Hierarchy │
|
||||
└────────────┬────────────────┘
|
||||
│ HTTP invoke
|
||||
┌──────────┼──────────┬──────────┐
|
||||
▼ ▼ ▼ ▼
|
||||
┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐
|
||||
│하랑 │ │나랑 │ │다랑 │ │이랑 │ sister-agent × 4 LXC
|
||||
│plan │ │impl │ │review│ │deploy│
|
||||
└──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘
|
||||
└─────────┴─ openclaw CLI ────┘ (LLM: gpt-5.4 등)
|
||||
│
|
||||
▼
|
||||
┌───────────────┐
|
||||
│ Gitea SSOT │ git.nabomhalang.co.kr
|
||||
│ (auto-push) │ public repo per pipeline
|
||||
└───────────────┘
|
||||
```
|
||||
|
||||
## 기술 스택
|
||||
|
||||
| 레이어 | 선택 |
|
||||
|---|---|
|
||||
| 런타임 | Node 22 + TypeScript (strict) |
|
||||
| 런타임 | Node 22 + TypeScript strict |
|
||||
| 상태 머신 | XState v5 |
|
||||
| 스키마 | Zod |
|
||||
| 영속화 | SQLite (better-sqlite3) |
|
||||
| DB | MariaDB (Prisma) |
|
||||
| 프로세스 | execa + AbortController |
|
||||
| CLI | citty |
|
||||
| 로그 | pino |
|
||||
| 디스코드 | discord.js v14 |
|
||||
| 테스트 | Vitest |
|
||||
| 프론트엔드 (대시보드) | Next.js 16 + styled-components |
|
||||
| 백엔드 (대시보드) | NestJS + Socket.IO |
|
||||
|
||||
## 상태
|
||||
|
||||
**v0.1.0** — Sprint 000~007 완료. 6가지 실패 모드 전부 코어에서 해결.
|
||||
|
||||
105 테스트 통과. CLI 13 서브커맨드. 마이그레이션 도구 + QA 6 템플릿 포함.
|
||||
- **v0.1.0** — Sprint 000~007 완료. FSM / Contract / QA / Migration 코어. 105 테스트 통과.
|
||||
- **v0.1.1** — 실 LLM 통합 (OpenClaw infer), 4 계층 재귀 스폰, 파일 추출, Gitea auto-push, deploy URL.
|
||||
- **v0.1.2** — 대시보드 아티팩트 뷰, FileViewerModal (MD 파일 클릭 → 모달).
|
||||
|
||||
## 빠른 시작
|
||||
|
||||
```bash
|
||||
# 설치
|
||||
bash install.sh --repo <repo-url> --dir /path/to/rails
|
||||
cd /path/to/rails
|
||||
# 1. 클론 + 설치
|
||||
git clone https://git.nabomhalang.co.kr/hanarang/hanarang-rails.git
|
||||
cd hanarang-rails
|
||||
pnpm install
|
||||
|
||||
# 환경 확인
|
||||
pnpm rails doctor
|
||||
|
||||
# .env 설정 후 DB 마이그레이션
|
||||
# 2. 환경 설정
|
||||
cp .env.example .env
|
||||
# DATABASE_URL 등 채우기
|
||||
pnpm prisma migrate deploy
|
||||
# DATABASE_URL, GITEA_TOKEN 등 채우기
|
||||
|
||||
# Mock 모드로 E2E 스모크 테스트
|
||||
# 3. DB 마이그레이션
|
||||
pnpm prisma migrate deploy
|
||||
pnpm prisma generate
|
||||
|
||||
# 4. 빌드
|
||||
pnpm build
|
||||
|
||||
# 5. Mock 모드로 스모크 테스트
|
||||
pnpm rails run hello-world --mock -r "Try a pipeline"
|
||||
pnpm rails status
|
||||
|
||||
# 6. 서버 기동 (18800 포트)
|
||||
pnpm rails serve
|
||||
```
|
||||
|
||||
Sister-agent 는 각 LXC 에서 별도 기동:
|
||||
|
||||
```bash
|
||||
cd sister-agent
|
||||
pnpm install && pnpm build
|
||||
AGENT_NAME=harang RAILS_API_URL=http://dev-vm:18800 node dist/server.js
|
||||
```
|
||||
|
||||
대시보드는 별도 repo `hanarang-dashboard` 참조.
|
||||
|
||||
## CLI 서브커맨드
|
||||
|
||||
| 명령 | 용도 |
|
||||
@@ -103,23 +142,39 @@ pnpm rails status
|
||||
| `rails abort <id>` | 강제 종료 |
|
||||
| `rails contract generate/freeze/validate/show` | Sprint Contract 관리 |
|
||||
| `rails qa run/show/templates` | QA 템플릿 실행 |
|
||||
| `rails skill-context create/show/clear` | 스킬 강제 진입 |
|
||||
| `rails skill-context create/show/clear` | Skill 강제 진입 |
|
||||
| `rails skill-trace show/blocked` | 도구 사용 감사 로그 |
|
||||
| `rails doctor` | 환경 헬스체크 |
|
||||
| `rails scaffold` | 신규 프로젝트 `.plans/` 생성 |
|
||||
| `rails migrate from-hanarang-harness <path>` | 레거시 하네스 스캔 |
|
||||
| `rails serve` | 오케스트레이터 서버 (v0.2 완성 예정) |
|
||||
| `rails serve` | 오케스트레이터 HTTP 서버 |
|
||||
|
||||
## HTTP API (orchestrator, 18800)
|
||||
|
||||
| 메서드 | 경로 | 설명 |
|
||||
|---|---|---|
|
||||
| GET | `/health` | 헬스체크 |
|
||||
| GET | `/pipelines?limit=N` | 파이프라인 리스트 |
|
||||
| GET | `/pipelines/:id` | 파이프라인 상세 |
|
||||
| POST | `/pipelines/start` | 새 파이프라인 실행 |
|
||||
| POST | `/pipelines/:id/abort` | 강제 종료 |
|
||||
| GET | `/api/pipelines/:id/sub-tasks` | SubTask 트리 |
|
||||
| GET | `/api/sub-tasks/:id` | 서브태스크 상세 |
|
||||
| GET | `/api/transitions` | 상태 전이 이력 |
|
||||
| GET | `/api/escalations` | 에스컬레이션 큐 |
|
||||
|
||||
## 문서
|
||||
|
||||
- [`docs/GUIDE.md`](docs/GUIDE.md) — **처음 보는 사람을 위한 완전 가이드 (전 구간 해설)**
|
||||
- [`docs/GUIDE.pdf`](docs/GUIDE.pdf) — 위 문서의 PDF 버전
|
||||
- [`docs/migration-guide.md`](docs/migration-guide.md) — 레거시 → rails 이전 가이드
|
||||
- [`docs/operations.md`](docs/operations.md) — 운영 가이드 (PM2, 로그, DB)
|
||||
- [`docs/discord-setup.md`](docs/discord-setup.md) — Discord 봇 연동 + marker 프로토콜
|
||||
- [`.plans/OVERVIEW.md`](.plans/OVERVIEW.md) — 프로젝트 전체 개요
|
||||
- [`.plans/failure-audit.md`](.plans/failure-audit.md) — F1–F6 실패 감사
|
||||
- [`.plans/design/`](.plans/design/) — 설계 문서 9종
|
||||
- [`.plans/design/`](.plans/design/) — 설계 문서 9 종
|
||||
- [`.plans/sprints/`](.plans/sprints/) — 스프린트 상세
|
||||
|
||||
## 라이선스
|
||||
|
||||
MIT
|
||||
MIT — 나봄하랑 / hanarang
|
||||
|
||||
906
docs/GUIDE.md
Normal file
906
docs/GUIDE.md
Normal file
@@ -0,0 +1,906 @@
|
||||
---
|
||||
title: "hanarang-rails 완전 가이드"
|
||||
subtitle: "4자매 AI가 달리는 결정론적 파이프라인 — 처음 보는 사람을 위한 전 구간 해설"
|
||||
author: "나봄하랑 / hanarang"
|
||||
date: "2026-04-10"
|
||||
geometry: margin=22mm
|
||||
mainfont: "Noto Sans CJK KR"
|
||||
monofont: "JetBrains Mono"
|
||||
fontsize: 11pt
|
||||
linkcolor: "NavyBlue"
|
||||
urlcolor: "NavyBlue"
|
||||
toc: true
|
||||
toc-depth: 3
|
||||
numbersections: true
|
||||
---
|
||||
|
||||
\newpage
|
||||
|
||||
# 0. 이 문서는 누구를 위한 문서인가
|
||||
|
||||
이 문서는 **hanarang-rails 프로젝트를 처음 보는 사람**이 한 번 읽고 다음 세 가지를 완전히 이해할 수 있게 하는 것이 목표다.
|
||||
|
||||
1. **이 시스템이 무엇이고**, 왜 만들었으며, 어떤 문제를 해결하는지
|
||||
2. **코드 한 줄부터 사용자 요청까지** 어떤 경로로 흐르는지
|
||||
3. 직접 클론해서 **E2E 로 돌려보려면** 무엇이 필요한지
|
||||
|
||||
기존 AI 코딩 도구 (Claude Code, Cursor, Codex, OpenClaw) 를 써 본 경험이 있다면 이해가 빠르겠지만, 없어도 모든 용어는 문서 안에서 정의한다. LLM / 에이전트 / 파이프라인이라는 단어만 대충 알면 된다.
|
||||
|
||||
\newpage
|
||||
|
||||
# 1. 한 문단 요약
|
||||
|
||||
**hanarang-rails 는 4 개의 AI "자매" 에이전트가 하나의 요청을 받아서 기획 → 구현 → 리뷰 → 배포를 자동으로 끝내는 결정론적 파이프라인 오케스트레이터다.** 기존 하네스는 "이 단계가 끝나면 다음 자매를 호출해 줘" 라고 LLM 에게 부탁하는 방식이었고, 그래서 자매가 중간에 길을 잃으면 사용자가 끼어들어 중재해야 했다. hanarang-rails 는 그 흐름을 XState 유한 상태 기계 (FSM) 와 Sprint Contract (DoD 의 기계 검증본) 로 물리적으로 강제한다. 자매는 "권고"를 받는 것이 아니라 **레일 위를 달리는 열차**처럼, 갈 수 있는 다음 상태가 코드로 고정되어 있다.
|
||||
|
||||
\newpage
|
||||
|
||||
# 2. 배경 — 왜 다시 만들었는가
|
||||
|
||||
## 2.1 전임자 hanarang-harness 의 실패 모드
|
||||
|
||||
이전 프로젝트 `hanarang-harness` (Gitea 에 private archive 로 보존) 는 **권고 기반** 파이프라인이었다. 각 단계가 끝나면 LLM 이 "다음에 누구를 부르면 좋을지" 판단했고, 핸드오프는 Discord 멘션으로 전달되었다. 4 개월 운영하면서 다음 6 가지 고질 문제가 반복됐다.
|
||||
|
||||
| 코드 | 증상 | 원인 |
|
||||
|---|---|---|
|
||||
| F1 | 하네스 skill 우회 — 자매가 혼자 worker 스폰해서 처리 | skill 진입 강제 부재 |
|
||||
| F2 | DoD 자동 강제 실패 — `build` 통과만 보고 완료 판정 | sprint contract / validator 부재 |
|
||||
| F3 | QA 단계 누락 — 사용자가 수동으로 다랑이 호출 필요 | 자동 라우팅 없음 |
|
||||
| F4 | 핸드오프 멘션 불안정 — 잘못된 자매 호출 | 분기가 LLM 판단에 의존 |
|
||||
| F5 | 중간 끊김 — `request-timed-out` 반복, `xhigh` 무한 대기 | 재시도/에스컬레이션 정책 없음 |
|
||||
| F6 | 환경 검증 누락 — "Docker 없음" 으로 작업 skip 허용 | 환경 전제 검사 없음 |
|
||||
|
||||
본질은 단 한 줄로 요약된다: **"자매가 하네스를 안 타고 본인이 처리한다."**
|
||||
|
||||
## 2.2 해결 전략 — 6 가지 설계 원칙
|
||||
|
||||
`.claude/rules/principles.md` 에 명시된 하드 룰이다. 이 원칙은 타협하지 않는다.
|
||||
|
||||
1. **강제 > 권고.** 모든 파이프라인 전이는 코드로 강제한다. LLM 판단에 맡기지 않는다.
|
||||
2. **결정론적 FSM.** 자매 간 핸드오프는 XState 상태 전이다. 멘션은 사용자 알림 전용이다.
|
||||
3. **Sprint Contract = 불변 계약.** 모든 스프린트는 시작 전에 `sprint-contract.json` 을 생성하고, DoD 를 Zod 스키마로 표현한 validator 가 pass / fail 을 판정한다. `build` 통과 = 완료는 금지다.
|
||||
4. **Skill 강제 진입.** OpenClaw 자매가 하네스 skill 을 우회하면 post-hook 이 감지해 작업을 revert 한다.
|
||||
5. **QA 체크리스트 의무.** 다랑이는 스프린트 타입별 체크리스트를 전부 체크해야 PASS 를 낼 수 있다.
|
||||
6. **환경 검증 선행.** 실기동 검증 환경이 없으면 스프린트를 시작하지 않는다. "Docker 없음 → skip" 같은 escape hatch 는 contract 에서 사전 차단한다.
|
||||
|
||||
## 2.3 레일 메타포
|
||||
|
||||
왜 이름이 "rails" 인가?
|
||||
|
||||
- **레일 (rail) = XState FSM**: 갈 수 있는 경로를 물리적으로 제한
|
||||
- **신호등 = Sprint Contract**: 다음 역으로 갈 수 있는 조건
|
||||
- **역 (station) = 자매 작업 단계**: Plan / Implement / Review / Deploy
|
||||
- **차단봉 = Skill 강제 진입 hook**
|
||||
- **긴급 정차 버튼 = 에스컬레이션 policy**
|
||||
- **중앙 통제소 = MariaDB orchestrator state**
|
||||
|
||||
사용자는 출발 버튼만 누르고, 긴급 상황에서만 호출된다.
|
||||
|
||||
\newpage
|
||||
|
||||
# 3. 4 자매는 누구인가
|
||||
|
||||
4 자매는 4 개의 서로 다른 LLM 에이전트다. 각자 성격/말투/역할이 다르고, OpenClaw 런타임 위에서 독립된 LXC 컨테이너에 돌아간다.
|
||||
|
||||
| 자매 | 영문 | 역할 | 단계 | 주 모델 |
|
||||
|---|---|---|---|---|
|
||||
| 하랑 | harang | Planner — 요구사항 해석, 계획 작성 | `plan` | gpt-5.4 |
|
||||
| 나랑 | narang | Implementer — 코드/문서 생성 | `implement` | gpt-5.4 |
|
||||
| 다랑 | darang | Reviewer — QA, 체크리스트 검증 | `review` | gpt-codex-5.3 |
|
||||
| 이랑 | erang | Deployer — 배포 검증, 인프라 | `deploy` | glm-5-turbo |
|
||||
|
||||
각 자매는 내부적으로 **manager → principal → lead → junior** 4 단계 계층을 가진다. 사용자가 "X 를 만들어 줘" 라고 하면, 각 자매의 manager 가 태스크를 받고 복잡도에 따라 하위 junior / lead 에게 분배한다. 복잡한 태스크일수록 더 깊게 파고들어가 병렬 처리된다 (자세한 내용은 §7).
|
||||
|
||||
\newpage
|
||||
|
||||
# 4. 시스템 구성도
|
||||
|
||||
## 4.1 하이 레벨
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ 사용자 (자기야) │
|
||||
│ Discord / Dashboard Web │
|
||||
└──────────────────────────────────┬───────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ hanarang-dashboard │
|
||||
│ Next.js 16 (프론트) + NestJS (API) │
|
||||
│ │
|
||||
│ /rails, /rails/log, /rails/escalations, /office │
|
||||
└──────────────────────────────────┬───────────────────────────┘
|
||||
│ HTTP
|
||||
▼
|
||||
┌──────────────────────────────────────────────────────────────┐
|
||||
│ hanarang-rails orchestrator │
|
||||
│ │
|
||||
│ XState FSM ─┬─ Sprint Contract Validator │
|
||||
│ ├─ SubTask Hierarchy Store │
|
||||
│ ├─ MariaDB (Prisma) │
|
||||
│ └─ HTTP API Server (citty + http) │
|
||||
└──────────────────────────────────┬───────────────────────────┘
|
||||
│ HTTP invoke
|
||||
▼
|
||||
┌────────────┬────────────┼────────────┬────────────┐
|
||||
▼ ▼ ▼ ▼
|
||||
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
|
||||
│ harang │ │ narang │ │ darang │ │ erang │
|
||||
│ (LXC) │ │ (LXC) │ │ (LXC) │ │ (LXC) │
|
||||
│ │ │ │ │ │ │ │
|
||||
│sister- │ │sister- │ │sister- │ │sister- │
|
||||
│ agent │ │ agent │ │ agent │ │ agent │
|
||||
└───┬────┘ └───┬────┘ └───┬────┘ └───┬────┘
|
||||
│ │ │ │
|
||||
└───────────┴─────┬─────┴───────────┘
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ openclaw CLI │ (LLM 호출: gpt-5.4 등)
|
||||
│ infer model │
|
||||
└──────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────┐
|
||||
│ Gitea SSOT │ git.nabomhalang.co.kr
|
||||
│ (output) │ auto-push, public repos
|
||||
└──────────────┘
|
||||
```
|
||||
|
||||
## 4.2 물리 토폴로지
|
||||
|
||||
| 역할 | 호스트 | IP | 내용 |
|
||||
|---|---|---|---|
|
||||
| 사용자 | 개인 PC | — | Discord 클라이언트, 대시보드 웹 브라우저 |
|
||||
| Proxmox hypervisor | `192.168.1.31` | — | VM / LXC 전체 호스트 |
|
||||
| Dev VM (SSOT) | VM 200 | `10.10.10.169` | `hanarang-rails` + `hanarang-dashboard` 실제 구동, PM2 |
|
||||
| 하랑이 LXC | LXC | 내부망 | sister-agent daemon + OpenClaw 런타임 |
|
||||
| 나랑이 LXC | LXC | 내부망 | 〃 |
|
||||
| 다랑이 LXC | LXC | 내부망 | 〃 |
|
||||
| 이랑이 LXC | LXC | 내부망 | 〃 |
|
||||
| Gitea | Docker | `git.nabomhalang.co.kr` | SSOT 저장소 (public + private), SSH 2222 |
|
||||
| MariaDB | Dev VM | `10.10.10.169:3306` | `hanarang_rails` DB |
|
||||
|
||||
하나의 파이프라인 요청은 최대 10 개 이상의 서브 프로세스로 확장될 수 있다 (4 자매 × manager/principal/lead/junior 계층). 병렬 실행은 `Promise.all` 기반이고, 동시 실행 한도는 자매별로 설정 가능하다 (기본 8, 나랑이는 6).
|
||||
|
||||
\newpage
|
||||
|
||||
# 5. 데이터 모델 — MariaDB 스키마
|
||||
|
||||
`prisma/schema.prisma` 에 정의되어 있다. 파이프라인 한 번의 실행이 각 테이블에 남기는 흔적을 따라가면 시스템 전체가 보인다.
|
||||
|
||||
## 5.1 테이블 요약
|
||||
|
||||
| 테이블 | 설명 | 키 |
|
||||
|---|---|---|
|
||||
| `pipelines` | 하나의 파이프라인 실행 (= 사용자 요청 1 회) | ULID |
|
||||
| `state_transitions` | FSM 상태 전이 로그 (감사 용) | auto |
|
||||
| `sub_tasks` | 자매/역할별 서브 태스크 트리 | ULID |
|
||||
| `sub_task_events` | 서브 태스크 수명 이벤트 (spawned/started/completed/failed) | auto |
|
||||
| `contracts` | Sprint Contract 스냅샷 (DoD + validator 정의) | ULID |
|
||||
| `escalations` | 사용자 개입이 필요해진 예외 상황 | ULID |
|
||||
| `actor_spawns` | 자매 프로세스 스폰 로그 (레거시) | auto |
|
||||
|
||||
## 5.2 Pipeline 레코드의 생애
|
||||
|
||||
```
|
||||
idle ─(START)─▶ running ─(ALL_STAGES_DONE)─▶ completed
|
||||
│
|
||||
├─(TIMEOUT 3회)──▶ escalated
|
||||
└─(FATAL_ERROR)──▶ failed
|
||||
```
|
||||
|
||||
`currentState` 는 XState 의 현재 노드, `contextJson` 은 FSM 의 context (전 단계 결과물 포함) 을 serialize 한 것이다. 매 전이마다 `StateTransition` row 가 한 줄씩 추가되므로, 나중에 `GET /api/transitions?pipelineId=…` 로 전체 이력을 재생할 수 있다.
|
||||
|
||||
## 5.3 SubTask 트리
|
||||
|
||||
각 파이프라인은 여러 개의 `sub_tasks` 를 만든다. 예를 들어 "todo 앱 만들어 줘" 라는 요청 하나가 다음 트리를 만들 수 있다.
|
||||
|
||||
```
|
||||
harang-manager (role=manager, stage=plan)
|
||||
└─ harang-principal (plan 의 세부 항목 3 개를 쪼갬)
|
||||
├─ harang-lead-1
|
||||
└─ harang-lead-2
|
||||
narang-manager (role=manager, stage=implement)
|
||||
├─ narang-lead-frontend
|
||||
│ ├─ narang-junior-html
|
||||
│ ├─ narang-junior-css
|
||||
│ └─ narang-junior-js
|
||||
└─ narang-lead-backend
|
||||
└─ narang-junior-api
|
||||
darang-manager (role=manager, stage=review)
|
||||
erang-manager (role=manager, stage=deploy)
|
||||
```
|
||||
|
||||
`parentId` 체인으로 트리를 재구성할 수 있고, 대시보드의 "서브태스크 상세 드로어" 가 이 트리를 직접 렌더링한다.
|
||||
|
||||
\newpage
|
||||
|
||||
# 6. Orchestrator — XState FSM 엔진
|
||||
|
||||
`src/orchestrator/` 는 파이프라인의 심장이다.
|
||||
|
||||
## 6.1 파일 구조
|
||||
|
||||
| 파일 | 역할 |
|
||||
|---|---|
|
||||
| `machine.ts` | XState `setup({types}).createMachine(…)` 로 FSM 정의 |
|
||||
| `runner.ts` | 파이프라인 실행 루프 (actor 생성, 이벤트 dispatch, 단계 간 체이닝) |
|
||||
| `persist.ts` | `getPersistedSnapshot()` 으로 FSM 상태를 DB 에 왕복 저장 |
|
||||
| `context.ts` | FSM context 타입 (pipelineId, stage 결과, priorStages…) |
|
||||
| `events.ts` | `START`, `STAGE_DONE`, `TIMEOUT`, `FATAL_ERROR` 등 이벤트 스키마 |
|
||||
|
||||
## 6.2 상태 흐름
|
||||
|
||||
```
|
||||
[ idle ]
|
||||
│ START
|
||||
▼
|
||||
[ running ]
|
||||
│
|
||||
├─ stage="plan" ──▶ spawn harang ──▶ priorStages.push
|
||||
│ │
|
||||
├─ stage="implement" ──▶ spawn narang ──┤
|
||||
│ │
|
||||
├─ stage="review" ──▶ spawn darang ─────┤
|
||||
│ │
|
||||
└─ stage="deploy" ──▶ spawn erang ──────┤
|
||||
│
|
||||
▼
|
||||
[ completed ]
|
||||
```
|
||||
|
||||
각 stage 는 순차적으로 실행되지만, **stage 내부** 에서는 계층 구조 (manager → principal → lead → junior) 가 `Promise.all` 로 병렬 실행된다. 그래서 한 stage 안에 10 개 이상의 junior 가 동시에 코드를 쓰는 일이 자주 생긴다.
|
||||
|
||||
## 6.3 priorStages 체이닝
|
||||
|
||||
가장 중요한 구조적 결정. `plan` 의 결과물 텍스트가 `implement` 의 프롬프트에 통째로 들어간다. `implement` 가 만든 파일 목록이 `review` 의 입력이 되고, `review` 의 verdict 가 `deploy` 의 컨텍스트가 된다. 자매는 다음 자매의 결과물을 모른 채 일하지 않는다.
|
||||
|
||||
구현:
|
||||
|
||||
```ts
|
||||
// src/orchestrator/runner.ts
|
||||
const priorStages: PriorStageOutput[] = [];
|
||||
for (const stage of ["plan", "implement", "review", "deploy"]) {
|
||||
const result = await invokeSister(stage, { priorStages });
|
||||
priorStages.push({ stage, text: extractStageText(result) });
|
||||
}
|
||||
```
|
||||
|
||||
`extractStageText` 는 결과물 JSON 에서 `summary`, `repoUrl`, `rawUrlBase`, `producedFiles`, `filesCount` 를 뽑아 자연어 요약으로 합친다.
|
||||
|
||||
\newpage
|
||||
|
||||
# 7. 역할 계층 — Manager/Principal/Lead/Junior
|
||||
|
||||
## 7.1 왜 계층이 있는가
|
||||
|
||||
LLM 한 개에게 "todo 앱 풀스택으로 만들어 줘" 라고 던지면 컨텍스트 한계에 부딪힌다. 사람 팀과 똑같이, 부장은 방향을 결정하고 신입은 코드를 친다. 이걸 구조적으로 강제하면 LLM 의 약점 (컨텍스트 파편화, 집중력 분산) 을 회피할 수 있다.
|
||||
|
||||
## 7.2 역할 정의 (`src/hierarchy/roles.ts`)
|
||||
|
||||
| Role | 한국어 | 주 모델 | 하위 스폰 가능 | 최대 스폰 |
|
||||
|---|---|---|---|---|
|
||||
| manager | 부장 | gpt-5.4 | principal, lead, junior | 4 |
|
||||
| principal | 수석 | gpt-5.4 | lead, junior | 3 |
|
||||
| lead | 선임 | gpt-codex-5.3 | junior | 4 |
|
||||
| junior | 신입 | glm-5-turbo | (없음) | 0 |
|
||||
|
||||
manager 는 직접 코드를 짜지 않는다. 대신 하위 직원에게 쪼개서 던진다. junior 는 리프 노드이며 실제 파일 생성을 책임진다.
|
||||
|
||||
## 7.3 복잡도 스코어 (`src/hierarchy/complexity.ts`)
|
||||
|
||||
태스크가 들어오면 먼저 complexity 점수를 계산한다.
|
||||
|
||||
```
|
||||
score = (길이_점수 × 0.3)
|
||||
+ (키워드_점수 × 0.5)
|
||||
+ (범위_점수 × 0.2)
|
||||
```
|
||||
|
||||
키워드 "풀스택", "데이터베이스", "인증", "배포", "아키텍처" 등은 가산점. 최종 score (0–100) 는 tier 로 매핑된다.
|
||||
|
||||
| Tier | 점수 | 권장 분해 |
|
||||
|---|---|---|
|
||||
| trivial | 0–20 | junior 한 명 |
|
||||
| simple | 21–40 | lead 한 명 또는 junior 2 |
|
||||
| moderate | 41–60 | principal 1, lead 1, junior 2–3 |
|
||||
| complex | 61–80 | principal 1, lead 2, junior 4 |
|
||||
| massive | 81–100 | principal 2, lead 3, junior 6+ |
|
||||
|
||||
이 분해는 `planner.ts` 의 `DecompositionPlan` 으로 표현되고, `spawn.ts` 의 재귀 트리 워커가 그걸 받아 실제 LLM 호출 그래프를 만든다.
|
||||
|
||||
\newpage
|
||||
|
||||
# 8. Sister Agent — 레일 위를 달리는 열차
|
||||
|
||||
`sister-agent/` 는 각 LXC 에 독립적으로 배포되는 daemon 이다. 네 자매 모두 동일한 코드 베이스를 쓰지만, 환경변수 `AGENT_NAME` (harang / narang / darang / erang) 로 정체성을 구분한다.
|
||||
|
||||
## 8.1 엔드포인트
|
||||
|
||||
```
|
||||
POST /invoke
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"pipelineId": "01HXXXX...",
|
||||
"stage": "implement",
|
||||
"task": { "title": "...", "description": "...", "workdir": "" },
|
||||
"priorStages": [ { "stage": "plan", "text": "..." } ],
|
||||
"timeoutMs": 600000,
|
||||
"railsApiUrl": "http://10.10.10.169:18800"
|
||||
}
|
||||
```
|
||||
|
||||
리턴은 `HandoffMessage` 디스크리미네이티드 유니온이다.
|
||||
|
||||
```
|
||||
{ "stage": "implement", "verdict": "IMPL_DONE",
|
||||
"payload": { "branch":"main", "commits":[...], "workdir":"...",
|
||||
"selfTestReport": { "producedFiles":[...], "repoUrl":"..." }}}
|
||||
```
|
||||
|
||||
## 8.2 실행 파이프라인 (`src/spawn.ts`)
|
||||
|
||||
```
|
||||
runSpawnNode(ctx, node)
|
||||
│
|
||||
├─ prompts.build(role, stage, task, priorStages) // 한국어 역할 프롬프트
|
||||
│
|
||||
├─ llm.infer(prompt, model) // openclaw CLI 호출
|
||||
│
|
||||
├─ maybeExtractFiles(llmText, role, stage) // 코드 블록 파싱
|
||||
│ ├─ ```lang:path 패턴 감지
|
||||
│ ├─ 파일 경로 sanitize
|
||||
│ └─ ctx.producedFiles.push(`${stage}/files/${path}`)
|
||||
│
|
||||
├─ for child of node.children: // 하위 직원 재귀
|
||||
│ await runSpawnNode(ctx, child) // Promise.all
|
||||
│
|
||||
└─ buildSuccessResult(node, producedFiles)
|
||||
```
|
||||
|
||||
## 8.3 LLM 호출 — `openclaw infer model run`
|
||||
|
||||
각 자매는 로컬에서 `openclaw infer model run --model gpt-5.4 --json` 서브프로세스를 실행한다. stdout 은 Zod 로 검증된 후 쓴다. LLM 응답의 결정론성은 아래 세 가지로 관리한다.
|
||||
|
||||
1. **엄격한 프롬프트 템플릿** — 역할/단계별 한국어 템플릿이 `prompts.ts` 에 고정
|
||||
2. **구조화 응답 요구** — "이 형식 밖으로 나가면 재시도" 지시를 프롬프트 끝에 삽입
|
||||
3. **코드 블록 규약** — ` ```lang:path/to/file.ext` 형태로 내놓으라고 명시, 파서가 이걸 기대
|
||||
|
||||
## 8.4 코드 블록 추출 (`src/code-extractor.ts`)
|
||||
|
||||
LLM 응답에서 파일을 꺼내는 로직이다. 기대 포맷:
|
||||
|
||||
````
|
||||
```html:frontend/index.html
|
||||
<!doctype html>
|
||||
...
|
||||
```
|
||||
|
||||
```css:frontend/style.css
|
||||
body { ... }
|
||||
```
|
||||
````
|
||||
|
||||
파서는:
|
||||
|
||||
1. 정규식으로 ` ``` ` 블록 탐지
|
||||
2. 언어 뒤의 `:path` 힌트 추출
|
||||
3. path sanitize: `..`, 절대 경로, 백슬래시 금지
|
||||
4. path 가 없으면 언어별 기본 파일명 (`snippet.html` 등)
|
||||
5. `{ path, lang, content }` 리스트 반환
|
||||
|
||||
이 결과는 `maybeExtractFiles` 가 받아서 실제 파일로 쓰고 `ctx.producedFiles` 에 상대 경로를 기록한다.
|
||||
|
||||
\newpage
|
||||
|
||||
# 9. Git 자동 푸시 — Gitea 연동 (`git-ops.ts`)
|
||||
|
||||
파이프라인이 만든 파일은 즉시 Gitea 에 올라가 실행 가능한 URL 로 바뀐다.
|
||||
|
||||
## 9.1 흐름
|
||||
|
||||
1. implement stage 가 끝나면 `commitAndPush(pipelineId, workdir)` 호출
|
||||
2. 리포 이름은 `rails-${pipelineId.slice(-10).toLowerCase()}` (예: `rails-abcd012345`)
|
||||
3. Gitea API 로 `hanarang` org 에 public repo 자동 생성
|
||||
`POST /api/v1/orgs/hanarang/repos`
|
||||
4. 로컬 `git init` → 커밋 → `git push https://user:TOKEN@git.nabomhalang.co.kr/…`
|
||||
5. 리턴:
|
||||
```
|
||||
{ ok:true, repoUrl:"https://git.nabomhalang.co.kr/hanarang/rails-abcd012345",
|
||||
rawUrlBase:"https://git.nabomhalang.co.kr/hanarang/rails-abcd012345/raw/branch/main",
|
||||
commit:"a1b2c3d", filesCount:7 }
|
||||
```
|
||||
|
||||
## 9.2 프리뷰 URL 추론
|
||||
|
||||
`derivePreviewUrl` 이 `producedFiles` 에서 `.html` 파일을 찾아 `${rawUrlBase}/${html파일경로}` 로 즉시 열 수 있는 공개 URL 을 계산한다. 결과는 `selfTestReport.deployUrl` 에 들어가고, 대시보드의 "url" 배지가 달린 FileRow 로 사용자에게 보여진다. 클릭하면 브라우저에서 바로 열린다.
|
||||
|
||||
## 9.3 왜 Gitea 인가
|
||||
|
||||
- `git.nabomhalang.co.kr` 은 우리 내부 SSOT 서버다 (Docker 로 Dev VM 에서 돌고 있음)
|
||||
- `gh` CLI 는 GitHub 전용이라 쓸 수 없고, 대신 `tea` CLI 또는 REST API 로 접근한다
|
||||
- 토큰: `.env` 의 `GITEA_TOKEN` 에 저장, 코드에서는 URL 에 `user:TOKEN@` 형태로만 사용
|
||||
|
||||
\newpage
|
||||
|
||||
# 10. 대시보드 — hanarang-dashboard
|
||||
|
||||
`hanarang-dashboard` 는 별도 repo 이며, rails 가 돌고 있는 모든 것을 시각화한다. Next.js 16 (Turbopack) + NestJS API + Socket.IO 실시간 이벤트로 만들어졌다.
|
||||
|
||||
## 10.1 페이지
|
||||
|
||||
| 경로 | 설명 |
|
||||
|---|---|
|
||||
| `/rails` | 활성 파이프라인 리스트 + SubTask 트리 시각화 |
|
||||
| `/rails/log` | 상태 전이 감사 로그 (SIEM 스타일) |
|
||||
| `/rails/escalations` | 에스컬레이션 큐 |
|
||||
| `/office` | 4 자매 대화 스트림 (사용자가 구경하는 용) |
|
||||
| `/sisters/[name]` | 자매 개별 프로필 + 통계 |
|
||||
|
||||
## 10.2 SubTask 상세 드로어
|
||||
|
||||
`/rails` 에서 노드를 클릭하면 우측 드로어가 열린다. 이 드로어에 들어가는 정보:
|
||||
|
||||
- **헤더**: 자매 아바타, role 배지, title, breadcrumb (부모 체인)
|
||||
- **상태/모델 그리드**: state, agent, model, duration, complexity, ID
|
||||
- **설명**: 태스크 description
|
||||
- **산출물 (Artifacts)**:
|
||||
- `.md` 로그 파일 → 클릭 시 모달로 내용 표시 (`FileViewerModal`)
|
||||
- 추출된 코드 파일 → 클릭 시 Gitea 프록시로 페치해서 표시
|
||||
- Deploy URL → 브라우저 외부 링크
|
||||
- **LLM 응답**: `react-markdown` 으로 렌더링 (front matter 는 분리)
|
||||
- **하위 노드 리스트**: children 요약
|
||||
- **이벤트 로그**: SubTaskEvent 전체
|
||||
|
||||
## 10.3 FileViewerModal
|
||||
|
||||
가장 최근에 추가된 기능. 대시보드에서 파일 내용을 보고 싶을 때 쓴다.
|
||||
|
||||
```
|
||||
┌────────────────────────────────────┐
|
||||
│ FILE implement/files/index.html │
|
||||
│ [복사] [닫기] │
|
||||
├────────────────────────────────────┤
|
||||
│ <!doctype html> │
|
||||
│ <html> │
|
||||
│ ... │
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
|
||||
두 가지 소스 타입을 받는다:
|
||||
|
||||
1. `{ type: 'llm', text }` — 이미 메모리에 있는 LLM 결과물 (로그 .md 용)
|
||||
2. `{ type: 'url', url }` — Gitea raw URL, 백엔드 `/api/rails/file-content` 프록시로 페치
|
||||
|
||||
프록시는 Gitea 호스트만 allowlist 한다 (`git.nabomhalang.co.kr`). 외부 URL 은 거부.
|
||||
|
||||
`.md` 파일은 `react-markdown` 으로, 아닌 것은 `<pre>` 로 표시. Front matter (`--- ... ---`) 는 상단 메타 박스로 분리한다.
|
||||
|
||||
\newpage
|
||||
|
||||
# 11. End-to-End 시나리오 — "todo 앱 만들어 줘"
|
||||
|
||||
처음 보는 사람이 가장 궁금해할 "한 번의 실행" 을 코드 흐름으로 따라가자.
|
||||
|
||||
## 11.1 Step 0 — 트리거
|
||||
|
||||
사용자가 Discord 에 다음과 같이 쓴다.
|
||||
|
||||
```
|
||||
/rails start project:todo-app requirements:"간단한 todo 웹앱 하나 만들어 줘"
|
||||
```
|
||||
|
||||
Discord 봇은 이걸 HTTP 요청으로 바꿔 Dev 서버 대시보드 백엔드로 보낸다.
|
||||
|
||||
```
|
||||
POST http://dev-vm/api/rails/pipelines/start
|
||||
{
|
||||
"project": "todo-app",
|
||||
"requirements": "간단한 todo 웹앱 하나 만들어 줘"
|
||||
}
|
||||
```
|
||||
|
||||
## 11.2 Step 1 — 오케스트레이터 진입
|
||||
|
||||
대시보드 백엔드 (`RailsService`) 가 rails orchestrator 에 포워딩.
|
||||
|
||||
```
|
||||
POST http://127.0.0.1:18800/pipelines/start
|
||||
```
|
||||
|
||||
rails 는:
|
||||
|
||||
1. ULID 를 발급해 `pipelines` 테이블에 새 row 를 만든다 (`currentState='idle'`)
|
||||
2. XState actor 를 생성해 `START` 이벤트 dispatch → `running` 상태로 전이
|
||||
3. `state_transitions` 에 `idle → running` 한 줄 기록
|
||||
4. 4 단계 루프를 시작한다
|
||||
|
||||
## 11.3 Step 2 — Plan (하랑이)
|
||||
|
||||
rails 는 harang LXC 의 `/invoke` 로 POST:
|
||||
|
||||
```
|
||||
{ stage:"plan", task:{ title:"todo-app", description:"..."}, priorStages:[] }
|
||||
```
|
||||
|
||||
harang sister-agent 는:
|
||||
|
||||
1. `harang-manager` SubTask row 생성, `sub_task_events` 에 `spawned`, `started` 이벤트
|
||||
2. complexity 계산 → 점수 35 → `simple` tier → principal 1 + junior 1 로 분해
|
||||
3. 각 하위 노드를 `Promise.all` 로 LLM 호출
|
||||
4. junior 가 반환한 계획을 manager 가 취합, ` ```md:plan.md` 코드 블록으로 감싼 응답을 만듦
|
||||
5. `maybeExtractFiles` 로 `plan/files/plan.md` 로 저장, producedFiles 에 기록
|
||||
6. `{stage:"plan", verdict:"PLAN_READY", payload:{ planDir, sprintId, selfTestReport:{producedFiles} }}` 리턴
|
||||
|
||||
rails 는 결과를 `priorStages[0]` 에 푸시한다.
|
||||
|
||||
## 11.4 Step 3 — Implement (나랑이)
|
||||
|
||||
```
|
||||
POST /invoke
|
||||
{ stage:"implement",
|
||||
task:{...},
|
||||
priorStages:[ { stage:"plan", text:"<plan.md 요약>" } ] }
|
||||
```
|
||||
|
||||
narang sister-agent 는:
|
||||
|
||||
1. complexity 60 → `moderate` → principal 1 + lead 2 (frontend/backend) + junior 4
|
||||
2. 병렬로 LLM 호출, junior 들이 각각 HTML / CSS / JS / server.js 를 생성
|
||||
3. 모든 산출물을 `implement/files/...` 로 저장
|
||||
4. `git-ops.commitAndPush(pipelineId, workdir)` 호출
|
||||
- Gitea 에 `rails-abcd012345` repo 생성
|
||||
- `git push` 성공
|
||||
- `repoUrl`, `rawUrlBase`, `commit` 리턴
|
||||
5. `derivePreviewUrl(producedFiles, rawUrlBase)` 로 `https://…/implement/files/frontend/index.html` 계산
|
||||
6. `{stage:"implement", verdict:"IMPL_DONE", payload:{ ..., selfTestReport:{ producedFiles, repoUrl, rawUrlBase, deployUrl } }}` 리턴
|
||||
|
||||
## 11.5 Step 4 — Review (다랑이)
|
||||
|
||||
```
|
||||
POST /invoke
|
||||
{ stage:"review",
|
||||
priorStages:[
|
||||
{ stage:"plan", text:"..." },
|
||||
{ stage:"implement", text:"repoUrl=...\nfilesCount=7\n..." }
|
||||
]}
|
||||
```
|
||||
|
||||
darang 은 rawUrlBase 로 Gitea 파일을 직접 페치해서 읽고, QA 체크리스트를 돌린다. 결과는 `verdict:"APPROVE" | "REQUEST_CHANGES" | "ABORT"`.
|
||||
|
||||
REQUEST_CHANGES 가 나오면 rails 는 implement 로 되돌려 재시도 (최대 3 회). 3 회 실패 시 `escalated` 상태로 전이하고 사용자에게 알림.
|
||||
|
||||
## 11.6 Step 5 — Deploy (이랑이)
|
||||
|
||||
erang 은 deploy URL 이 실제로 열리는지 verify, 필요하면 추가 설정 파일을 쓴다. 최종적으로 `{stage:"deploy", verdict:"DEPLOY_DONE", payload:{ deployArtifactPath, verificationResults }}`.
|
||||
|
||||
## 11.7 Step 6 — 완료
|
||||
|
||||
rails 는 `running → completed` 로 전이, 대시보드 Socket.IO 로 실시간 브로드캐스트. 사용자 Discord 에는 최종 deploy URL 이 포스트된다.
|
||||
|
||||
```
|
||||
✅ todo-app 완료
|
||||
repo: https://git.nabomhalang.co.kr/hanarang/rails-abcd012345
|
||||
deploy: https://git.nabomhalang.co.kr/hanarang/rails-abcd012345/raw/branch/main/implement/files/frontend/index.html
|
||||
duration: 4m 12s
|
||||
sub-tasks: 11 (완료 11, 실패 0)
|
||||
```
|
||||
|
||||
\newpage
|
||||
|
||||
# 12. HTTP API 명세
|
||||
|
||||
rails orchestrator 가 노출하는 엔드포인트. 대시보드 백엔드와 sister-agent 가 소비한다.
|
||||
|
||||
| 메서드 | 경로 | 설명 |
|
||||
|---|---|---|
|
||||
| GET | `/health` | 헬스체크 |
|
||||
| GET | `/pipelines?limit=N` | 파이프라인 리스트 |
|
||||
| GET | `/pipelines/:id` | 파이프라인 상세 (state, context, transitions) |
|
||||
| POST | `/pipelines/start` | 새 파이프라인 실행 |
|
||||
| POST | `/pipelines/:id/abort` | 파이프라인 강제 종료 |
|
||||
| GET | `/api/pipelines/:id/sub-tasks` | SubTask 트리 |
|
||||
| GET | `/api/sub-tasks/:id` | 서브태스크 상세 (parents/children/events) |
|
||||
| GET | `/api/transitions?pipelineId=...&limit=100` | 상태 전이 이력 |
|
||||
| GET | `/api/escalations?pipelineId=...&resolved=false` | 에스컬레이션 큐 |
|
||||
|
||||
대시보드 쪽 (`backend/src/rails/`) 은 이것들을 래핑해서 `/api/rails/*` 로 재노출하고, 인증/인가를 한 겹 더 얹는다.
|
||||
|
||||
\newpage
|
||||
|
||||
# 13. Sprint Contract — DoD 의 기계 검증
|
||||
|
||||
## 13.1 왜 필요한가
|
||||
|
||||
F2 실패 모드 ("build 통과 = 완료") 를 막기 위해서. 스프린트가 시작되기 전에 "이 스프린트는 무엇으로 끝난 것으로 보는가" 를 기계가 읽을 수 있는 형태로 고정한다.
|
||||
|
||||
## 13.2 구조
|
||||
|
||||
`.claude/state/contracts/<task-id>.sprint-contract.json`
|
||||
|
||||
```json
|
||||
{
|
||||
"taskId": "11.2",
|
||||
"sprintId": "SPRINT-003",
|
||||
"version": "v1",
|
||||
"checks": [
|
||||
{ "id": "files-exist", "type": "file-exists", "paths": ["src/contract/generator.ts"] },
|
||||
{ "id": "tests-pass", "type": "command-success", "cmd": "pnpm test src/contract" },
|
||||
{ "id": "schema-valid", "type": "artifact-schema", "path": "out/contract.json", "schema": "ContractSchema" }
|
||||
],
|
||||
"nonGoals": ["UI 변경"],
|
||||
"reviewerProfile": "static",
|
||||
"riskFlags": ["security-sensitive"]
|
||||
}
|
||||
```
|
||||
|
||||
## 13.3 체크 타입 (`src/contract/checks/`)
|
||||
|
||||
| 타입 | 의미 |
|
||||
|---|---|
|
||||
| `file-exists` | 경로 존재 여부 |
|
||||
| `command-success` | 쉘 명령 exit code 0 |
|
||||
| `http-status` | URL 응답 2xx |
|
||||
| `regex-in-file` | 파일 내용이 정규식 매칭 |
|
||||
| `artifact-schema` | JSON 산출물이 Zod 스키마 통과 |
|
||||
| `db-query` | DB 쿼리가 기대 행 수 리턴 |
|
||||
| `process-listening` | 포트 LISTEN 확인 |
|
||||
| `manual` | 수동 체크박스 (escape hatch, 최소화 권장) |
|
||||
|
||||
하나라도 FAIL 이 나오면 스프린트는 `cc:완료` 가 될 수 없다.
|
||||
|
||||
\newpage
|
||||
|
||||
# 14. 보안 모델
|
||||
|
||||
## 14.1 신뢰 경계
|
||||
|
||||
| 경계 | 정책 |
|
||||
|---|---|
|
||||
| 사용자 → 대시보드 | 세션 쿠키 인증 (NestJS) |
|
||||
| 대시보드 → rails | 내부망 전용 HTTP, 토큰 없음 (향후 추가 예정) |
|
||||
| rails → sister-agent | 내부망 HTTP, `AGENT_NAME` 환경변수로 정체성 고정 |
|
||||
| sister-agent → LLM | OpenClaw 런타임이 API 키 관리 |
|
||||
| rails → Gitea | `.env` 의 `GITEA_TOKEN`, URL 에만 주입 |
|
||||
|
||||
## 14.2 Gitea 프록시 allowlist
|
||||
|
||||
대시보드 백엔드 `/api/rails/file-content` 는 `URL.host === 'git.nabomhalang.co.kr'` 만 허용. 외부 URL 은 404 를 돌려준다. 이유: 악의적 링크로 백엔드에서 임의 HTTP 요청을 트리거하는 SSRF 공격 방지.
|
||||
|
||||
## 14.3 Zod 검증 경계
|
||||
|
||||
모든 외부 입력 (HTTP body, subprocess stdout, 파일 로드) 은 Zod 스키마를 통과한 뒤에만 내부 타입으로 들어온다. 경계 밖에서는 `any` 금지.
|
||||
|
||||
\newpage
|
||||
|
||||
# 15. 실패/복원력 (`src/resilience/`)
|
||||
|
||||
## 15.1 재시도 정책
|
||||
|
||||
Exponential backoff — `1s, 2s, 4s, 8s, 최대 30s`. 기본 3 회. 매 재시도는 `sub_task_events` 에 `retry` 이벤트로 기록된다.
|
||||
|
||||
## 15.2 타임아웃
|
||||
|
||||
자매 `/invoke` 응답 기본 600 초 (LLM 이 오래 걸릴 수 있어서). 초기에는 30 초로 잡았다가 `request-timed-out` 재현 → 600 초로 변경.
|
||||
|
||||
## 15.3 에스컬레이션
|
||||
|
||||
N 회 실패 시 `escalations` 테이블에 row 추가, Discord 에 사용자 멘션. 상태는 `escalated` 로 전이하고 파이프라인은 정지한다. 사용자가 `rails resume <id>` 를 호출하면 `escalated → running` 으로 복구.
|
||||
|
||||
\newpage
|
||||
|
||||
# 16. 설치/실행 가이드
|
||||
|
||||
## 16.1 사전 요구
|
||||
|
||||
- Node 22 + pnpm
|
||||
- MariaDB 10.11+
|
||||
- Gitea 인스턴스 (또는 환경변수 `GITEA_TOKEN` + `GITEA_API_URL` 재설정)
|
||||
- OpenClaw 런타임 (각 자매 LXC)
|
||||
|
||||
## 16.2 rails 서버 구동
|
||||
|
||||
```bash
|
||||
git clone https://git.nabomhalang.co.kr/hanarang/hanarang-rails.git
|
||||
cd hanarang-rails
|
||||
pnpm install
|
||||
cp .env.example .env
|
||||
# DATABASE_URL, GITEA_TOKEN 등 채우기
|
||||
pnpm prisma migrate deploy
|
||||
pnpm build
|
||||
pnpm rails serve # 18800 포트
|
||||
```
|
||||
|
||||
## 16.3 sister-agent 구동 (각 LXC)
|
||||
|
||||
```bash
|
||||
cd sister-agent
|
||||
pnpm install
|
||||
pnpm build
|
||||
AGENT_NAME=harang RAILS_API_URL=http://dev-vm:18800 \
|
||||
node dist/server.js
|
||||
```
|
||||
|
||||
## 16.4 대시보드 구동
|
||||
|
||||
별도 repo `hanarang-dashboard` 참조. `pnpm build && pm2 start ecosystem.config.js`.
|
||||
|
||||
## 16.5 스모크 테스트
|
||||
|
||||
```bash
|
||||
pnpm rails run hello-world --mock -r "Try a pipeline"
|
||||
pnpm rails status
|
||||
```
|
||||
|
||||
`--mock` 모드는 실제 LLM 호출 없이 결정론 파이프라인만 확인한다.
|
||||
|
||||
\newpage
|
||||
|
||||
# 17. 디렉토리 구조
|
||||
|
||||
```
|
||||
hanarang-rails/
|
||||
├── src/
|
||||
│ ├── orchestrator/ FSM 엔진
|
||||
│ ├── hierarchy/ 계층/복잡도/플래너
|
||||
│ ├── handoff/ 자매 간 메시지 스키마 + 트랜스포트
|
||||
│ ├── contract/ Sprint Contract + validator
|
||||
│ ├── qa/ QA 체크리스트 runtime
|
||||
│ ├── enforcement/ Skill 강제 진입 / bypass 감지
|
||||
│ ├── resilience/ 재시도/타임아웃/에스컬레이션
|
||||
│ ├── server/http.ts HTTP API 서버
|
||||
│ ├── cli/ citty 기반 rails CLI
|
||||
│ ├── config/ env + config loader
|
||||
│ └── bridge/ Discord 브릿지 (v0.2 예정)
|
||||
│
|
||||
├── sister-agent/
|
||||
│ └── src/
|
||||
│ ├── server.ts /invoke HTTP 서버
|
||||
│ ├── spawn.ts 재귀 트리 실행기
|
||||
│ ├── hierarchy.ts 역할 트리 builder
|
||||
│ ├── complexity.ts 스코어 계산
|
||||
│ ├── planner.ts 복잡도 → 분해 계획
|
||||
│ ├── roles.ts 역할 정의
|
||||
│ ├── prompts.ts 한국어 프롬프트 템플릿
|
||||
│ ├── llm.ts openclaw CLI wrapper
|
||||
│ ├── code-extractor.ts 코드 블록 파서
|
||||
│ ├── git-ops.ts Gitea API + git push
|
||||
│ └── rails-client.ts rails 에 이벤트 report
|
||||
│
|
||||
├── prisma/schema.prisma DB 스키마
|
||||
├── .plans/
|
||||
│ ├── OVERVIEW.md
|
||||
│ ├── failure-audit.md
|
||||
│ ├── design/ 설계 문서 9 종
|
||||
│ ├── sprints/ 스프린트 000–007 명세
|
||||
│ └── migration/
|
||||
├── docs/
|
||||
│ ├── GUIDE.md ★ 이 문서
|
||||
│ ├── migration-guide.md
|
||||
│ ├── operations.md
|
||||
│ └── discord-setup.md
|
||||
├── hooks/ OpenClaw pre/post-tool hooks
|
||||
├── qa-templates/ QA 체크리스트 6 종
|
||||
├── install.sh 설치 자동화
|
||||
└── rails.config.example.yaml
|
||||
```
|
||||
|
||||
\newpage
|
||||
|
||||
# 18. 로드맵
|
||||
|
||||
| 버전 | 상태 | 내용 |
|
||||
|---|---|---|
|
||||
| v0.1.0 | 완료 | Sprint 000–007, FSM/contract/QA/migration 코어 |
|
||||
| v0.1.1 | 완료 | 실 LLM 통합, 계층 실행, 파일 추출, Gitea auto-push |
|
||||
| v0.1.2 | 완료 | 대시보드 아티팩트 뷰, MD 파일 뷰어 모달 |
|
||||
| v0.2 | 진행 | Discord 브릿지 정식화, GatewayHttpTransport 분리 |
|
||||
| v0.3 | 계획 | Skill 강제 진입 실측, OpenClaw hook 프로덕션 적용 |
|
||||
| v0.4 | 계획 | 멀티 테넌시 (여러 사용자 동시 실행) |
|
||||
| v1.0 | 계획 | 외부 공개 + 문서화 완성 |
|
||||
|
||||
\newpage
|
||||
|
||||
# 19. 용어집
|
||||
|
||||
| 용어 | 정의 |
|
||||
|---|---|
|
||||
| **자매 (Sister)** | 4 개의 LLM 에이전트 중 하나 (harang/narang/darang/erang) |
|
||||
| **자기야** | 사용자 (나봄하랑) 에 대한 4 자매의 호칭 |
|
||||
| **OpenClaw** | 하나랑 생태계에서 사용하는 AI 런타임. Claude Code 기반이지만 별개 브랜드 |
|
||||
| **Rails** | 이 프로젝트. 결정론적 파이프라인 오케스트레이터 |
|
||||
| **Harness** | rails 의 전임자 `hanarang-harness`. 권고 기반이라 실패가 잦았음 |
|
||||
| **FSM** | Finite State Machine. XState 로 구현 |
|
||||
| **Sprint Contract** | 스프린트 시작 전에 쓰는 DoD 기계 검증 스펙 |
|
||||
| **DoD** | Definition of Done. 완료 조건 |
|
||||
| **SubTask** | 자매/역할별로 쪼개진 서브 태스크 |
|
||||
| **Stage** | 파이프라인의 주 단계 (plan/implement/review/deploy) |
|
||||
| **Role** | 자매 내부의 직급 (manager/principal/lead/junior) |
|
||||
| **Escalation** | 자동 복구 실패 시 사용자에게 넘기는 예외 상황 |
|
||||
| **priorStages** | 이전 단계 결과물 텍스트의 누적 배열 |
|
||||
| **producedFiles** | 자매가 이번 실행에서 만든 파일의 상대 경로 리스트 |
|
||||
| **SSOT** | Single Source of Truth. 여기서는 Dev VM 위의 Gitea + MariaDB |
|
||||
| **LXC** | 리눅스 컨테이너. Proxmox 에서 각 자매를 격리 실행 |
|
||||
|
||||
\newpage
|
||||
|
||||
# 20. 참고 자료
|
||||
|
||||
- 원본 실패 감사: `.plans/failure-audit.md`
|
||||
- 설계 문서: `.plans/design/state-machine.md`, `sprint-contract.md`, `hierarchy.md`, `deployment.md`, `handoff.md`, `retry-policy.md`, `qa-template.md`, `transports.md`, `triggers.md`
|
||||
- 스프린트 명세: `.plans/sprints/SPRINT-000` ~ `SPRINT-007`
|
||||
- 마이그레이션 가이드: `docs/migration-guide.md`
|
||||
- 운영 가이드: `docs/operations.md`
|
||||
- Discord 셋업: `docs/discord-setup.md`
|
||||
- 전임자 repo: `hanarang/openclaw-harness` (private archive)
|
||||
- 대시보드 repo: `hanarang/hanarang-dashboard`
|
||||
|
||||
\newpage
|
||||
|
||||
# 부록 A. 용례 비교 — 구 하네스 vs rails
|
||||
|
||||
## A.1 핸드오프
|
||||
|
||||
**구 하네스**
|
||||
|
||||
```
|
||||
harang → "이제 나랑이가 구현해 주세요" (Discord 멘션)
|
||||
narang → 잠시 뒤 멘션을 본다 (혹은 못 봄)
|
||||
→ 본인 판단으로 스폰, 직접 처리
|
||||
→ skill 을 안 탐 (F1)
|
||||
```
|
||||
|
||||
**rails**
|
||||
|
||||
```
|
||||
stage="plan" → FSM context.priorStages.push({ stage:"plan", text:... })
|
||||
XState transition(STAGE_DONE) → guard 검사 → next state="implement"
|
||||
runner 가 자동으로 POST /invoke (stage=implement) → narang 실행
|
||||
narang 은 선택권이 없다. 호출된 대로만 실행
|
||||
```
|
||||
|
||||
## A.2 DoD
|
||||
|
||||
**구 하네스**: `npm run build` 가 exit 0 → 완료 처리.
|
||||
|
||||
**rails**: Sprint Contract 의 `checks[]` 가 전부 pass 해야 `cc:완료`. `artifact-schema` 체크는 산출물 JSON 을 Zod 로 한 번 더 검증한다.
|
||||
|
||||
## A.3 QA
|
||||
|
||||
**구 하네스**: 사용자가 "다랑아 이거 QA 해 줘" 라고 멘션. 다랑이가 답장 없음 → 사용자가 중재.
|
||||
|
||||
**rails**: FSM 이 자동으로 `review` stage 로 전이. 다랑이는 반드시 호출되고, QA 템플릿의 체크리스트를 전부 채워야 `APPROVE` 를 낼 수 있다.
|
||||
|
||||
\newpage
|
||||
|
||||
# 부록 B. 자주 묻는 질문
|
||||
|
||||
**Q. 왜 Claude Code 가 아니라 OpenClaw 인가?**
|
||||
A. OpenClaw 는 하나랑이 내부에서 쓰는 커스텀 런타임이다. Claude Code 를 포크한 것이 아니라 별개의 구현이다. 4 자매는 OpenClaw 위에 올라가 있고, 이 rails 레포 자체는 Claude Code 세션에서 개발한다.
|
||||
|
||||
**Q. 왜 SQLite 가 아니라 MariaDB 를 쓰나?**
|
||||
A. 초기 설계에서는 SQLite 를 썼지만, Dev VM 에 MariaDB 가 이미 있고 대시보드가 같은 DB 를 공유하는 게 간단해서 MariaDB 로 옮겼다. Prisma 로 추상화되어 있어 다시 바꾸는 것도 어렵지 않다.
|
||||
|
||||
**Q. 병렬 실행은 어디까지 가능한가?**
|
||||
A. stage 는 순차 (plan → implement → …), stage 내부의 junior 스폰은 병렬. 기본 `default:8` 동시 실행, 나랑이는 빌드 자원 때문에 6 으로 제한. `concurrencyLimits.overrides` 로 자매별 조정 가능.
|
||||
|
||||
**Q. LLM 이 헛소리를 하면?**
|
||||
A. 세 겹의 방어가 있다. (1) 프롬프트 템플릿이 구조화 응답을 강제. (2) Zod 가 응답을 검증, 실패 시 재시도. (3) Sprint Contract 가 최종 산출물을 정적 검증.
|
||||
|
||||
**Q. 사용자 개입 없이 며칠 단위 장기 태스크가 가능한가?**
|
||||
A. 현재 v0.1.x 는 한 번의 파이프라인 = 한 번의 기획 → 배포 사이클이다. 더 긴 수명의 프로젝트는 여러 파이프라인을 엮는 방식으로 다룬다. v0.4 멀티 테넌시에서 검토 예정.
|
||||
|
||||
**Q. 테스트는 어떻게?**
|
||||
A. Vitest 105 테스트가 현재 통과. FSM, contract, QA, migration 핵심 경로를 커버한다. E2E 는 `--mock` 모드로 돌릴 수 있다.
|
||||
|
||||
\newpage
|
||||
|
||||
# 부록 C. 라이선스 및 기여
|
||||
|
||||
- 라이선스: MIT (`LICENSE`)
|
||||
- 저작권: 나봄하랑 / hanarang
|
||||
- 기여: PR 환영. `.plans/` 문서 규약을 따를 것.
|
||||
- 문의: Discord 또는 Gitea issue
|
||||
|
||||
> 하나랑의 4 자매가 사용자 중재 없이 달릴 수 있는 레일을 깐다 —
|
||||
> 그것이 이 프로젝트의 처음이자 끝의 목표다.
|
||||
BIN
docs/GUIDE.pdf
Normal file
BIN
docs/GUIDE.pdf
Normal file
Binary file not shown.
Reference in New Issue
Block a user