- docs/GUIDE.md: 20 장 + 3 부록, 배경/원칙/아키텍처/데이터 모델/FSM/계층/ sister-agent/LLM/파일 추출/Git push/대시보드/E2E 시나리오/API/Contract/ 보안/복원력/설치/디렉토리/로드맵/용어집 망라 - docs/GUIDE.pdf: weasyprint 로 렌더, 목차 + 페이지 헤더/풋터 포함 - README.md: 가이드 링크 추가, 상태 섹션 v0.1.2 까지 갱신, 아키텍처 그림 과 설계 원칙 정리
907 lines
40 KiB
Markdown
907 lines
40 KiB
Markdown
---
|
||
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 자매가 사용자 중재 없이 달릴 수 있는 레일을 깐다 —
|
||
> 그것이 이 프로젝트의 처음이자 끝의 목표다.
|