docs(plans): Sprint 000 — 전체 플랜 문서 세트 작성

- CLAUDE.md 를 .claude/rules/ 4파일로 분할 (project/stack/principles/workflow)
- Plans.md 루트 인덱스 (스프린트 목차 + 참조만)
- hooks/ pre/post-tool.sh 스켈레톤 (no-op, Sprint 002에서 구현)
- .plans/OVERVIEW.md — 목표/범위/성공기준 8개
- .plans/failure-audit.md — F1~F6 실패 감사 (증거 기반)
- .plans/design/ 6개 문서:
  * state-machine.md (XState v5 FSM 설계)
  * sprint-contract.md (Zod schema + validator)
  * skill-enforcement.md (4계층 방어)
  * handoff.md (상태 전이 기반 자매 통신)
  * retry-policy.md (backoff + escalation)
  * qa-template.md (체크리스트 runtime)
- .plans/sprints/ 8개 스프린트 명세 (SPRINT-000~007)
- .plans/migration/from-hanarang-harness.md (자산 매트릭스 + 단계별 가이드)

총 17개 문서, 약 2146 lines.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-10 12:36:49 +09:00
parent 042ee0da42
commit bac114d469
26 changed files with 2331 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
# 설계 원칙 (하드 룰)
## 1. 강제 > 권고
모든 파이프라인 전이는 코드로 강제한다. LLM 판단에 맡기지 않는다. "자매가 알아서 해주겠지"는 금지어다.
## 2. 결정론적 FSM
자매 간 핸드오프는 **XState state transition** 이다. 멘션 기반 핸드오프는 **사용자 알림 전용**이며, 실제 자매 간 통신은 상태 머신이 주도한다.
## 3. Sprint Contract = 불변 계약
모든 스프린트/작업은 시작 전에 `sprint-contract.json` 을 생성한다. DoD 는 Zod schema 로 표현되고 validator 가 pass/fail 을 판정한다. "build 통과 = 완료" 는 금지다.
## 4. Skill 진입 강제
OpenClaw 자매가 hanarang-rails 스킬을 **우회**하면 post-hook 이 이를 감지하고 작업을 revert 한다. skill 경로를 타지 않은 결과물은 invalid 다.
## 5. QA 체크리스트 의무
다랑이(QA)는 스프린트 타입별 체크리스트를 **전부 체크**해야 pass 를 내릴 수 있다. 체크 안 한 항목이 하나라도 있으면 자동 `REQUEST_CHANGES`.
## 6. 환경 검증 선행
실기동 검증 환경이 없는 경우 **스프린트를 시작하지 않는다**. "Docker 없음 → skip" 같은 escape hatch 는 contract 에서 사전 차단.
## 7. 재시도/에스컬레이션 policy
- 타임아웃/실패: 지수 백오프로 자동 재시도 (기본 3회)
- N회 실패: 사용자(나봄하랑) 에스컬레이션
- `thinking tier xhigh` 금지 (무한대기 유발 이력)

22
.claude/rules/project.md Normal file
View File

@@ -0,0 +1,22 @@
# 프로젝트 요약
**hanarang-rails**`hanarang-harness` 의 후계작으로, 4자매(하랑/나랑/다랑/이랑) AI 파이프라인을 **강제 기반 결정론 파이프라인**으로 재설계한 하네스다.
- 후계 대상: [`hanarang/openclaw-harness`](https://git.nabomhalang.co.kr/hanarang/openclaw-harness) (현재 private archive)
- 실패 감사: `.plans/failure-audit.md` 의 F1F6 참조
## 개입 지점 (사용자)
사용자(나봄하랑)는 다음 시점에서만 개입한다:
1. 최초 요청 ("X 프로젝트 기획해줘")
2. 에스컬레이션 (N회 실패 또는 Gap 2회 미해소)
3. 배포 최종 승인
이 외의 모든 단계는 자동. 사용자가 중재자로 끼어들 필요가 있으면 그것은 하네스의 실패다.
## 참고
- 실패 감사: `.plans/failure-audit.md`
- 상위 메모리: `~/.claude/projects/-home-erang/memory/MEMORY.md`
- OpenClaw 워크스페이스: `~/.openclaw/workspace/`
- SSOT: Dev 서버 `/home/dev/hanarang/`

24
.claude/rules/stack.md Normal file
View File

@@ -0,0 +1,24 @@
# 기술 스택
| 레이어 | 선택 |
|---|---|
| 런타임 | **Node 22 + TypeScript (strict)** |
| 패키지 | **pnpm** (npm/yarn 금지) |
| 상태 머신 | **XState v5** |
| 스키마 | **Zod** |
| 영속화 | **SQLite (better-sqlite3)** 단일 파일 |
| 프로세스 | **execa + AbortController** |
| CLI | **citty** |
| 로그 | **pino** (구조화 JSON) |
| 디스코드 | **discord.js v14** |
| 테스트 | **Vitest** |
## 코딩 룰
- **TypeScript strict 모드 고정** (`strict: true` + `noUncheckedIndexedAccess: true`)
- **Zod 검증 경계** — 모든 외부 입력(파일, 네트워크, subprocess stdout)은 Zod 로 파싱 후 사용
- **async/await 만 사용** — `.then()` 체이닝 금지
- **Result<T, E> 패턴** — throw 대신 `neverthrow` 또는 자체 Result 로 에러 표면화
- **로그는 pino** — `console.*` 금지
- **파일 경로는 `node:path` + `import.meta.url`** — `__dirname` 금지
- **환경변수는 Zod schema 로 검증된 env 객체 통해서만** — `process.env.X` 직접 참조 금지

29
.claude/rules/workflow.md Normal file
View File

@@ -0,0 +1,29 @@
# 워크플로우 / 커밋 룰
## 플랜 문서가 곧 하네스
> 사용자 자기야의 하드 룰: **모든 단계를 상세 .MD로 작성. root `Plans.md` 는 참조만.**
- `Plans.md` (루트) — 스프린트 목차 + 각 상세 문서로 링크만
- `.plans/OVERVIEW.md` — 프로젝트 전체 목표/범위/성공 기준
- `.plans/failure-audit.md` — 기존 하네스의 실패 모드 분석 (F1F6)
- `.plans/design/*.md` — 설계 문서 (state-machine, sprint-contract, handoff, retry-policy, qa-template, skill-enforcement)
- `.plans/sprints/SPRINT-NNN-*.md` — 스프린트 상세 명세
- `.plans/migration/from-hanarang-harness.md` — 마이그레이션 가이드
## 커밋/브랜치 룰
- 브랜치: `main` (기본) + `feature/sprint-NNN-*` (스프린트별)
- 커밋 메시지: `type(scope): 한국어 요약` (Conventional Commits 변형)
- Co-Authored-By 풋터 허용 (AI 공동작업 표기)
- 스프린트 완료 시 PR → 다랑이 QA 통과 후 merge
## 금지 사항
-`hanarang-harness` 의 Lobster 워크플로우 파일 복사 (결정성 부족 원인)
-`bridge.sh` curl 기반 디스코드 브릿지 (재구현: `discord.js`)
-`thinking tier xhigh` (무한대기)
- ❌ 멘션 기반 자매 간 라우팅 (상태 머신 사용)
-`console.*` 직접 호출
-`any` 타입 (Zod 경계 이후)
-`build` 단독으로 DoD 충족 판정

17
.claude/settings.json Normal file
View File

@@ -0,0 +1,17 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
"hooks": {
"PreToolUse": [
{
"matcher": "Write|Edit|Bash",
"hooks": [{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/hooks/pre-tool.sh" }]
}
],
"PostToolUse": [
{
"matcher": "Write|Edit",
"hooks": [{ "type": "command", "command": "$CLAUDE_PROJECT_DIR/hooks/post-tool.sh" }]
}
]
}
}

119
.plans/OVERVIEW.md Normal file
View File

@@ -0,0 +1,119 @@
# hanarang-rails — OVERVIEW
> 4자매가 달릴 결정론적 레일
## 목표 (Goal)
4자매 AI(하랑 / 나랑 / 다랑 / 이랑) 파이프라인을 **사용자 중재 없이 자동으로 완주**시킨다.
- 입력: 사용자 요청 한 줄 ("X 프로젝트 기획해줘")
- 출력: 배포 검증 완료 + 최종 승인 요청
- 사람 개입: 최초 요청 + 에스컬레이션 + 배포 승인 — 그 외 전부 자동
## 왜 재작성?
[`hanarang-harness`](https://git.nabomhalang.co.kr/hanarang/openclaw-harness) 는 **권고 기반 파이프라인**이었다. 자매들이 레일을 벗어나도 막을 수단이 없었고, 결과적으로 사용자가 계속 중재자로 개입해야 했다. 상세한 실패 모드는 [`failure-audit.md`](./failure-audit.md) 참조.
핵심 증상 한 줄: **"자매가 하네스 skill 을 안 타고 본인이 처리한다."**
## 범위 (Scope)
### In Scope
- 4자매 역할: 하랑(Planner) / 나랑(Generator) / 다랑(Evaluator/QA) / 이랑(Infra/Deploy)
- XState 기반 결정론적 오케스트레이터
- Sprint Contract (Zod schema + validator)
- Skill 강제 진입 (hook 기반 bypass 차단)
- 상태 전이 기반 자매 핸드오프
- 재시도 / 타임아웃 / 에스컬레이션
- QA 체크리스트 runtime
- 디스코드 알림 (사용자 알림 전용)
- SQLite 기반 상태 영속화
- 기존 hanarang-harness 자산 마이그레이션 (scaffold, install, agents md)
### Out of Scope
- OpenClaw 런타임 자체의 수정
- 자매 모델 자체 학습 / 파인튜닝
- 디스코드 봇 기능 확장 (파이프라인 외 기능)
- GitHub 연동 (Gitea 전용)
## 성공 기준 (Definition of Done)
1. **End-to-end 자동화**
- "X 프로젝트 기획해줘" 한 줄로 **사용자 개입 0회** 에서 배포 검증까지 도달한다
- 중간에 사용자가 멘션되는 경우는 에스컬레이션 뿐이다
2. **결정성 (Determinism)**
- 동일 입력 → 동일 파이프라인 전이 (상태 머신 테스트 100% pass)
- 같은 시나리오를 10번 반복 실행 시 **핸드오프 실패 0건**
3. **Skill 강제 진입**
- 자매가 hanarang-rails skill 을 우회하려 하면 hook 이 감지하고 차단한다
- 차단 이벤트가 로그에 구조화되어 남는다
4. **DoD 강제**
- `build 통과 = 완료` 시나리오가 거부된다
- 실기동 검증 결과가 structured result 로 contract 를 통과해야만 cc:완료 로 전이한다
5. **타임아웃 복원력**
- 자매 응답 없음 30초 → 자동 재시도 1회
- 3회 실패 → 사용자 에스컬레이션
- `request-timed-out` 로 파이프라인이 좀비가 되는 사례 0건
6. **QA 강제**
- 다랑이가 체크리스트를 전부 채우지 않으면 PASS 판정을 낼 수 없다
- QA 결과가 artifact 로 저장되고 추후 조회 가능
7. **관찰 가능성 (Observability)**
- 모든 상태 전이가 SQLite `state_transitions` 테이블에 기록된다
- `rails status` 명령으로 현재 상태 + 과거 전이 이력을 조회할 수 있다
- pino 로그가 pipeline-id 로 grouped
8. **마이그레이션 경로**
- 기존 hanarang-harness 사용자(=나봄하랑)가 수동 작업 없이 `rails migrate` 로 옮길 수 있다
- 기존 scaffold.sh, install.sh --role 동작이 하위 호환 유지되거나 대체재가 제공된다
## 비기능 요구사항 (NFR)
| 항목 | 기준 |
|---|---|
| 기동 시간 | `rails start` 후 2초 이내 ready |
| 메모리 | idle 시 < 100MB, 동시 4자매 spawn 시 < 500MB |
| 디스크 | SQLite DB 100 MB 이하 (90일 retention) |
| 로그 | 스프린트당 평균 1MB 이하 (구조화 압축 후) |
| 재시도 | exponential backoff (1s, 2s, 4s, 최대 30s) |
| 타임아웃 | 자매 응답 기본 30s, 설정 가능 |
## 레일 메타포 (왜 hanarang-rails?)
4자매는 레일 위의 열차다. 지금까지는 레일이 없어서 자매가 제멋대로 방향을 정했다. hanarang-rails 는:
- **레일** = XState FSM: 갈 수 있는 경로를 물리적으로 제한
- **신호등** = Sprint Contract: 다음 역으로 갈 조건
- **역** = 자매 작업 단계 (Plan / Impl / QA / Deploy)
- **차단봉** = Skill 강제 진입 hook
- **긴급 정차 버튼** = 에스컬레이션 policy
- **중앙 통제소** = SQLite orchestrator state
사용자(자기야)는 **출발 버튼**만 누르고, 긴급 상황에서만 호출된다.
## 타임라인 / 마일스톤
**스프린트 기반으로 진행. 날짜 예측은 하지 않는다.**
- M0: 스펙 확정 + Sprint 000 완료 (계획/감사/프로젝트 세팅)
- M1: Sprint 001003 완료 — "레일이 깔림" (스켈레톤 + 강제 + 계약)
- M2: Sprint 004006 완료 — "열차가 달림" (핸드오프 + 복원력 + QA)
- M3: Sprint 007 완료 — "이전이 끝남" (마이그레이션 + 실서비스 투입)
## 관련 문서
- [`failure-audit.md`](./failure-audit.md) — F1F6 실패 감사
- [`design/`](./design/) — 설계 문서 세트
- [`sprints/`](./sprints/) — 스프린트 명세
- [`migration/from-hanarang-harness.md`](./migration/from-hanarang-harness.md) — 마이그레이션
## 참고
- 후계 대상: [`hanarang/openclaw-harness`](https://git.nabomhalang.co.kr/hanarang/openclaw-harness)
- 4자매 인프라: 메모리 `project_hanarang.md`
- OpenClaw vs Claude Code: 4자매는 OpenClaw 런타임, 이 레포는 Claude Code 세션으로 개발

198
.plans/design/handoff.md Normal file
View File

@@ -0,0 +1,198 @@
# Design — Handoff (자매 간 핸드오프)
> **처방 대상**: F3 (QA 자동 라우팅 누락), F4 (핸드오프 멘션 불안정)
## 기존 방식의 한계
기존 hanarang-harness 는 자매 간 통신을 **디스코드 멘션**으로 구현했다:
```
하랑이: [구현해줘] @나랑이
나랑이: [완료] @하랑이
하랑이: [QA 필요할 듯] @다랑이
```
문제:
- 멘션 파싱 실패 → 다음 자매가 깨지 않음 (F3)
- 잘못된 자매 호출 → 파이프라인이 이상한 방향 (F4)
- 멘션 = 메시지 = LLM 재해석 → 결정성 없음
- 디스코드 rate limit / 네트워크 장애에 취약
## 새 방식 — 상태 전이가 곧 핸드오프
핸드오프는 **XState state transition** 이다. 디스코드 멘션은 **사용자 알림 전용**이다.
```
┌──────────────────────────────────────────────────┐
│ hanarang-rails orchestrator │
│ (XState FSM) │
└──────────────────────────────────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
spawn spawn spawn spawn
(execa) (execa) (execa) (execa)
│ │ │ │
▼ ▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐ ┌────────┐
│ 하랑이 │ │ 나랑이 │ │ 다랑이 │ │ 이랑이 │
│(spawn) │ │(spawn) │ │(spawn) │ │(spawn) │
└────────┘ └────────┘ └────────┘ └────────┘
│ │ │ │
│ structured │ structured │ structured │ structured
│ output │ output │ output │ output
▼ ▼ ▼ ▼
┌──────────────────────────────────────────────────┐
│ orchestrator receives │
│ Zod-validated HandoffMessage │
└──────────────────────────────────────────────────┘
[FSM transition]
next actor spawn
(병렬: 디스코드 bridge 가 상태 전이 이벤트를 구독 → 사용자 알림)
```
## HandoffMessage 스키마
각 자매가 orchestrator 에 반환하는 구조화 결과:
```ts
const HandoffMessage = z.discriminatedUnion('actor', [
z.object({
actor: z.literal('harang'),
verdict: z.enum(['PLAN_READY', 'ABORT']),
payload: z.object({
planDir: z.string(),
sprintId: z.string(),
contractId: z.string(),
}).optional(),
abortReason: z.string().optional(),
}),
z.object({
actor: z.literal('narang'),
verdict: z.enum(['IMPL_DONE', 'ERROR']),
payload: z.object({
branch: z.string(),
commits: z.array(z.string()),
workdir: z.string(),
selfTestReport: z.unknown(),
}).optional(),
errorReason: z.string().optional(),
}),
z.object({
actor: z.literal('darang'),
verdict: z.enum(['APPROVE', 'REQUEST_CHANGES', 'ABORT']),
payload: z.object({
checklistResults: z.array(z.object({
id: z.string(),
passed: z.boolean(),
note: z.string().optional(),
})),
issues: z.array(ReviewIssue),
artifactPath: z.string(),
}),
}),
z.object({
actor: z.literal('erang'),
verdict: z.enum(['DEPLOY_DONE', 'DEPLOY_FAILED']),
payload: z.object({
deployArtifactPath: z.string(),
projectType: z.string(),
verificationResults: z.unknown(),
}),
}),
])
```
## Spawn 계약
Orchestrator 가 자매를 spawn 할 때 표준화된 계약:
```ts
async function spawnSister(opts: {
sister: SisterName
contractId: string
workdir: string
timeoutMs: number
signal: AbortSignal
}): Promise<HandoffMessage> {
const { stdout } = await execa('openclaw', [
'spawn',
'--role', opts.sister,
'--contract', opts.contractId,
'--workdir', opts.workdir,
'--output', 'structured-json',
], {
timeout: opts.timeoutMs,
signal: opts.signal,
cwd: opts.workdir,
})
// 반드시 Zod 로 검증
return HandoffMessage.parse(JSON.parse(stdout))
}
```
- **stdout 이 structured JSON** 이 아니면 즉시 `ERROR` 로 처리
- 타임아웃 시 `AbortController` 로 프로세스 kill
- Zod 검증 실패 = 규격 위반 = `ERROR`
## 디스코드 브릿지 (알림 전용)
디스코드는 **사용자에게 진행 상황을 알리는 용도** 로만 사용. 자매 간 통신 아님.
```
orchestrator state transition
┌──────────────┐
│ event emitter│
└──────┬───────┘
┌────────────────┐
│ discord bridge │ (discord.js)
└──────┬─────────┘
포럼 포스트 업데이트 / 멘션
(사용자 = 자기야)
```
브릿지가 보내는 메시지 유형:
| 이벤트 | 메시지 예시 | 사용자 멘션 |
|---|---|---|
| `planning` 진입 | "🦊 하랑이가 SPRINT-001 기획 시작" | X |
| `implementing` 진입 | "⚙️ 나랑이가 구현 시작 (feature/sprint-001)" | X |
| `REQUEST_CHANGES` | "🔁 다랑이 re-review 요청 (라운드 2)" | X |
| `escalated` 진입 | "🚨 3회 실패. 자기야 확인 필요" | **O** |
| `done` 진입 | "✅ SPRINT-001 완료. 배포 승인해주세요" | **O** |
## 병렬 자매 호출 (나중에)
v1 은 **strict sequential**. 동시에 두 자매가 돌지 않음.
v2 후보:
- 나랑이가 worker 여러 개 spawn (내부 병렬)
- 다랑이가 static / runtime 리뷰 병렬
- orchestrator 자체는 여전히 single pipeline lock
## 실패 시나리오 & 복구
| 시나리오 | 감지 | 복구 |
|---|---|---|
| 자매 spawn 실패 (openclaw 커맨드 에러) | execa exit code ≠ 0 | ERROR event → retry policy |
| 자매 structured output 파싱 실패 | Zod validation 실패 | ERROR event → retry + log |
| 자매 타임아웃 (응답 없음) | AbortController fired | TIMEOUT event → retry policy |
| 자매 정상 종료했는데 verdict 이상 | Zod 통과했지만 invalid state transition | ERROR event → abort + log |
## 참고
- `principles.md` 원칙 2
- `failure-audit.md` F3, F4
- `state-machine.md` — 전이 정의
- `retry-policy.md` — TIMEOUT / ERROR 이벤트 처리

View File

@@ -0,0 +1,238 @@
# Design — QA Template System (다랑이 runtime)
> **처방 대상**: F3 (QA 자동 라우팅 누락), F2 (DoD 강제 실패 — QA 측면)
## 원칙
- 다랑이(Evaluator)는 **템플릿 체크리스트** 를 따라 검증한다.
- 체크리스트의 모든 필수 항목을 확인하기 전에 `APPROVE` 를 낼 수 없다.
- 체크 결과는 구조화된 artifact 로 저장되며 파이프라인이 기계적으로 읽는다.
- 사용자 메모리 원칙: **"QA는 항상 철저하게, 작업 단위 작아도 QA 체크리스트는 제한 없음"**
## 스프린트 타입별 템플릿
스프린트 `type` (scaffold / feature / refactor / bugfix / migration / infra) 에 따라 다른 템플릿이 로드됨.
### scaffold 템플릿
```yaml
template: scaffold-v1
required_checks:
- id: repo-structure
description: 표준 디렉토리 구조(src/, tests/, .plans/) 존재
kind: file_exists
- id: tsconfig-strict
description: tsconfig.json strict 모드
kind: regex_in_file
- id: package-manager-lockfile
description: pnpm-lock.yaml 존재 (npm/yarn lock 없음)
kind: file_exists
- id: gitignore-basics
description: .gitignore 에 node_modules, dist, *.env 등 포함
kind: regex_in_file
- id: readme-minimum
description: README 에 프로젝트명 + 요약 + 실행 방법
kind: manual
- id: license-present
description: LICENSE 파일 존재
kind: file_exists
```
### feature 템플릿
```yaml
template: feature-v1
required_checks:
- id: tests-added
description: 새 기능에 대한 테스트 1개 이상 존재
kind: manual
- id: tests-pass
description: pnpm test 통과
kind: command_success
- id: types-ok
description: pnpm tsc --noEmit 통과
kind: command_success
- id: no-console-log
description: console.* 호출 없음 (pino 사용)
kind: regex_absent
- id: no-any
description: any 타입 신규 도입 없음 (Zod 경계 밖)
kind: manual
- id: runtime-smoke
description: 실제 기동해서 smoke 테스트 통과
kind: command_success
- id: error-handling
description: 주요 에러 경로에 Result / neverthrow 패턴 적용
kind: manual
- id: docs-updated
description: README / .plans 에 변경 반영
kind: manual
```
### bugfix 템플릿
```yaml
template: bugfix-v1
required_checks:
- id: regression-test
description: 버그를 재현하는 테스트가 추가됨 (수정 전 fail → 수정 후 pass)
kind: manual
- id: root-cause-documented
description: .plans/sprints/SPRINT-NNN.md 에 root cause 기록
kind: regex_in_file
- id: no-scope-creep
description: 버그 외 리팩터/기능 추가 없음
kind: manual
- id: tests-pass
description: 전체 테스트 pass
kind: command_success
```
### migration 템플릿
```yaml
template: migration-v1
required_checks:
- id: migration-script
description: 마이그레이션 스크립트 / SQL 존재
kind: file_exists
- id: rollback-plan
description: 롤백 계획 문서화됨
kind: regex_in_file
- id: dry-run-tested
description: dry-run 검증 완료
kind: command_success
- id: data-loss-assessment
description: 데이터 손실 가능성 평가 완료
kind: manual
- id: backup-captured
description: 운영 DB 백업 확인
kind: manual
```
## 체크 종류 (kind)
| kind | 자동 검증 | 설명 |
|---|---|---|
| `file_exists` | ✅ | 경로에 파일 존재 |
| `regex_in_file` | ✅ | 파일 내 regex 매칭 |
| `regex_absent` | ✅ | 파일/전체에 regex 없음 |
| `command_success` | ✅ | 명령어 exit 0 |
| `http_status` | ✅ | HTTP 엔드포인트 응답 |
| `manual` | ❌ | 다랑이 LLM 이 코드를 읽고 판단 |
`manual` 체크는 다랑이가 실제 코드를 읽고 체크박스를 수동으로 채운다. 단, 각 체크는 **근거 링크** (파일:라인) 를 반드시 첨부해야 한다.
## 체크 결과 Artifact
```json
{
"schemaVersion": "v1",
"sprintId": "SPRINT-001",
"templateId": "scaffold-v1",
"reviewRound": 1,
"reviewer": "darang",
"startedAt": "2026-04-10T13:00:00Z",
"completedAt": "2026-04-10T13:08:32Z",
"checks": [
{
"id": "repo-structure",
"kind": "file_exists",
"passed": true,
"evidence": "src/, tests/, .plans/ 모두 존재",
"duration_ms": 12
},
{
"id": "tsconfig-strict",
"kind": "regex_in_file",
"passed": true,
"evidence": "tsconfig.json:5 `\"strict\": true`",
"duration_ms": 4
},
{
"id": "no-any",
"kind": "manual",
"passed": false,
"evidence": "src/handlers/request.ts:42 — `(req: any)` 발견",
"reviewerNote": "Zod schema 를 추가하고 req 타입 구체화 필요",
"severity": "major"
}
],
"verdict": "REQUEST_CHANGES",
"unpassedRequired": 1,
"summary": {
"total": 6,
"passed": 5,
"failed": 1,
"skipped": 0
}
}
```
## Verdict 판정 규칙
```
verdict =
| APPROVE if all required checks passed
| REQUEST_CHANGES if any required check failed (severity major+)
| APPROVE_WITH_NITS if only minor/nit issues remain
| ABORT if prerequisite check failed
```
- **`minor` 이슈만으로는 REQUEST_CHANGES 불가** (기존 하네스 원칙 계승)
- **`major` / `critical` 이 하나라도 있으면 REQUEST_CHANGES**
- severity 는 다랑이 LLM 의 판단이지만 체크 설명의 강도가 가이드
## 다랑이 runtime 흐름
```
orchestrator → spawn darang actor
┌───────────────────┐
│ load contract │ (Sprint Contract 에서 type 확인)
│ load template │ (type → qa-template)
└─────────┬─────────┘
┌───────────────────┐
│ automated checks │ (file_exists, command_success, etc.)
└─────────┬─────────┘
┌───────────────────┐
│ manual checks │ (다랑이 LLM 이 코드 읽고 판단)
└─────────┬─────────┘
┌───────────────────┐
│ verdict 집계 │
│ artifact 저장 │
└─────────┬─────────┘
structured HandoffMessage 반환
```
## 체크리스트 커스터마이즈
프로젝트별 추가 체크를 `qa-extra.yaml` 로 정의 가능:
```yaml
# .rails/qa-extra.yaml
extends: feature-v1
additional_checks:
- id: korean-ui-strings
description: 사용자 노출 문자열이 한국어인지 확인
kind: manual
severity: major
```
orchestrator 가 `base + extra` 를 merge 해서 다랑이에게 전달.
## 참고
- `principles.md` 원칙 5
- `failure-audit.md` F3, F2
- 사용자 메모리: `feedback_qa_thorough.md` (철저한 QA)
- `sprint-contract.md``manual_checklist` kind 와 연결

View File

@@ -0,0 +1,180 @@
# Design — Retry / Timeout / Escalation Policy
> **처방 대상**: F5 (중간 끊김 / 타임아웃 무한대기)
## 원칙
1. **fail-fast**: 감지할 수 있는 실패는 즉시 실패 처리. 대기하지 않는다.
2. **retry with backoff**: transient 실패는 자동 재시도 (지수 백오프).
3. **escalate after N**: N회 초과 실패는 사용자 에스컬레이션.
4. **no zombie**: 타임아웃된 프로세스는 반드시 kill.
5. **observable**: 모든 재시도 / 에스컬레이션은 SQLite 에 기록.
## 타임아웃 정책
| 수준 | 기본값 | 설명 |
|---|---|---|
| 자매 응답 (actor) | **30초** | orchestrator 가 자매 structured output 을 기다리는 최대 시간 |
| subprocess (execa) | **60초** | 내부 bash 스크립트 1건당 기본값 |
| contract 런타임 검증 커맨드 | **contract 에서 지정** | `runtimeValidation.commands[].timeoutMs` |
| 파이프라인 전체 | **60분** | escalated 로 전이되기 전 최대 소요 시간 |
| 디스코드 API | **10초** | 알림 전송 실패 시 로그만 남기고 넘어감 |
설정 가능. 환경 변수 `RAILS_TIMEOUT_ACTOR_MS` 등으로 override.
## Backoff 계산
지수 백오프 + jitter:
```ts
function backoffMs(retryCount: number): number {
const base = 1000 // 1s
const max = 30000 // 30s
const exp = Math.min(base * Math.pow(2, retryCount), max)
const jitter = Math.random() * 0.3 * exp // ±30%
return Math.floor(exp + jitter - (exp * 0.15))
}
// 예시:
// retry 0: ~1s
// retry 1: ~2s
// retry 2: ~4s
// retry 3: ~8s
// retry 4: ~16s
// retry 5+: ~30s (cap)
```
## Retry 카운터 관리
| 카운터 | 대상 | 리셋 시점 |
|---|---|---|
| `retryCount` | 현재 actor 의 재시도 | 새 state 진입 시 리셋 |
| `reviewRound` | 다랑이 재작업 루프 | 새 파이프라인 시작 시 리셋 |
| `pipelineRetries` | 파이프라인 전체 실패 횟수 | 수동 리셋 (`rails retry`) |
## Retryable vs Non-retryable 분류
```ts
interface ErrorClassification {
retryable: boolean
reason: 'timeout' | 'rate_limit' | 'network' | 'transient' |
'config' | 'permission' | 'invariant' | 'user_input_needed'
}
function classify(err: unknown): ErrorClassification {
if (err instanceof TimeoutError) return { retryable: true, reason: 'timeout' }
if (err instanceof NetworkError) return { retryable: true, reason: 'network' }
if (err instanceof RateLimitError) return { retryable: true, reason: 'rate_limit' }
if (err instanceof ZodError) return { retryable: false, reason: 'invariant' }
if (err instanceof PermissionError) return { retryable: false, reason: 'permission' }
if (err instanceof ConfigError) return { retryable: false, reason: 'config' }
// default: assume transient
return { retryable: true, reason: 'transient' }
}
```
**Non-retryable 은 즉시 escalation** — 재시도해도 고쳐질 가능성이 낮음.
## 에스컬레이션 트리거
다음 조건 중 하나라도 만족 시 `escalated` 상태로 전이:
1. `retryCount >= 3` (현재 actor 에서 3회 재시도 실패)
2. `reviewRound > 3` (다랑이 재작업 4라운드 진입)
3. 파이프라인 전체 경과 시간 > 60분
4. Non-retryable 에러 발생
5. Contract validator 가 `ABORT_PRECHECK` 반환 (환경 전제 미달)
6. Skill enforcement hook 이 bypass 감지
## 에스컬레이션 동작
```ts
async function escalate(opts: {
pipelineId: string
reason: string
context: PipelineContext
}): Promise<void> {
// 1. SQLite 에 escalation 이벤트 기록
await db.insert(escalations).values({
pipelineId: opts.pipelineId,
reason: opts.reason,
contextSnapshot: JSON.stringify(opts.context),
createdAt: new Date(),
})
// 2. 디스코드로 사용자 알림 (멘션 포함)
await bridge.notifyUser({
mentionUser: true,
level: 'critical',
title: `🚨 파이프라인 에스컬레이션: ${opts.pipelineId}`,
body: buildEscalationReport(opts),
actions: ['resume', 'abort', 'inspect'],
})
// 3. 파이프라인 FSM 을 escalated 상태로 전이
// (거기서 RESUME / ABORT 이벤트를 대기)
// 4. 현재 spawn 중인 actor 프로세스 cancel
opts.context.actors.forEach(a => a?.send({ type: 'ABORT' }))
}
```
## 에스컬레이션 메시지 내용
디스코드 메시지는 사용자가 **한 번에 판단할 수 있도록** 압축:
```
🚨 [SPRINT-001] 파이프라인 에스컬레이션
원인: 나랑이(implementing) 3회 재시도 실패
상세: pnpm install 타임아웃 (180s 초과)
(재시도 1, 2, 3 모두 180s 에서 kill)
마지막 성공 상태: planning → implementing
경과 시간: 42분
재시도 카운트: 3 / 3
review round: 0 / 3
마지막 로그 10줄:
> pnpm install
Progress: resolved 512, reused 412, downloaded 0, ...
[hanging at 180s]
가능한 조치:
▶️ `rails resume <pipeline-id>` — 재시도
⏹️ `rails abort <pipeline-id>` — 중단
🔍 `rails inspect <pipeline-id>` — 상세 조회
@나봄하랑
```
## 타임아웃 kill 보장
`execa` + `AbortController` 만으로는 자식의 자식 프로세스가 남을 수 있다. 대응:
- `execa(..., { killSignal: 'SIGTERM', forceKillAfterDelay: 5000 })` — SIGKILL 강제
- `detached: true` + `process.kill(-pid)` 로 프로세스 그룹 전체 kill
- orchestrator 종료 시 자식 PID 전수 kill (cleanup handler)
- `onExit` (sindresorhus) 로 SIGINT / SIGTERM 시 cleanup
## xhigh 금지
기존 커밋 `63c6d76 fix: thinking tier 되돌림 — xhigh는 무한 대기 유발` 기록:
- 자매 호출에 `xhigh` thinking tier 를 쓰지 않는다 (정책)
- contract 의 `nonGoals` 에 "xhigh thinking" 명시 가능
- Rails 는 `thinking_tier` 파라미터를 전달하지 않거나 `high` 까지만 허용
- `xhigh` 를 쓰면 runtime check 에서 경고 + 거부
## 관찰 가능성
- 모든 재시도는 `state_transitions` 테이블에 `event_type='RETRY'` 로 기록
- 모든 에스컬레이션은 `escalations` 테이블에 풀 context snapshot
- `rails status` 커맨드로 현재 파이프라인의 retry 카운트 / 경과 시간 조회
- pino 로그에 `pipelineId` + `actor` + `retryCount` 필드 항상 포함
## 참고
- `principles.md` 원칙 7
- `failure-audit.md` F5
- `state-machine.md``retrying`, `escalated` 상태 정의

View File

@@ -0,0 +1,154 @@
# Design — Skill Enforcement
> **처방 대상**: F1 (하네스 skill bypass)
## 문제
기존 hanarang-harness 에서 나랑이가 "worker 스폰해서 바로 시작한다" 라며 skill 진입을 건너뛰었다. skill 이 권고 수준이라 강제력이 없었다. 결과적으로 파이프라인 일관성이 무너졌다.
## 해결 전략 — 다층 방어
1. **Layer 1 — 시스템 프롬프트 강제문** (약한 방어)
- 자매 시스템 프롬프트에 "반드시 `/rails` 스킬을 거쳐야 한다" 명시
- LLM 지시 준수 기대치에 의존 — 약함
2. **Layer 2 — Pre-Tool Hook (중간 방어)**
- 자매가 Write / Edit / Bash 를 호출하기 직전
- 현재 세션이 `rails-skill-context` 를 세팅했는지 확인
- 세팅 안 됐으면 exit 2 로 차단하고 "먼저 /rails 를 호출하세요" 메시지
3. **Layer 3 — Post-Tool Hook (강한 방어)**
- 자매가 작업 결과를 커밋한 후
- `.rails/skill-trace.jsonl` 을 확인해 파이프라인이 skill 경로를 탔는지 감사
- 우회 감지 시 작업을 자동 revert + escalation
4. **Layer 4 — Contract Validator (최종 방어)**
- Sprint Contract 의 DoD 에 `skill-path-taken` 체크 포함
- contract validator 가 `.rails/skill-trace.jsonl` 을 읽어 검증
- 우회한 경우 `ABORT_PRECHECK` 반환
## 구현 — Layer 2/3 상세
### Pre-Tool Hook 로직
```bash
#!/usr/bin/env bash
# hooks/pre-tool.sh
set -euo pipefail
# stdin 에서 event JSON 읽기
EVENT=$(cat)
# 이 이벤트가 Write / Edit / Bash 인지 확인
TOOL=$(echo "$EVENT" | jq -r '.tool_name // empty')
case "$TOOL" in
Write|Edit|Bash) ;;
*) exit 0 ;;
esac
# 작업 디렉토리 추출
CWD="${CLAUDE_PROJECT_DIR:-$(pwd)}"
# skill context 파일 확인
SKILL_CTX="$CWD/.rails/skill-context.json"
if [ ! -f "$SKILL_CTX" ]; then
echo "[rails-enforce] Skill context missing. /rails 스킬을 먼저 호출하세요." >&2
exit 2 # 차단
fi
# skill context 가 유효한지 (최근 N초 이내) 확인
AGE=$(jq -r '.ageSeconds // 999' "$SKILL_CTX")
if [ "$AGE" -gt 300 ]; then
echo "[rails-enforce] Skill context stale (>${AGE}s). 세션을 재시작하세요." >&2
exit 2
fi
exit 0
```
### Post-Tool Hook 로직
```bash
#!/usr/bin/env bash
# hooks/post-tool.sh
set -euo pipefail
EVENT=$(cat)
CWD="${CLAUDE_PROJECT_DIR:-$(pwd)}"
TRACE="$CWD/.rails/skill-trace.jsonl"
# 이 도구 사용을 trace 에 append
mkdir -p "$(dirname "$TRACE")"
echo "$EVENT" | jq -c \
'{ ts: now, tool: .tool_name, cwd: env.PWD, session_id: env.CLAUDE_SESSION_ID }' \
>> "$TRACE"
exit 0
```
### Skill Context 구조
`/rails <subcommand>` 슬래시 커맨드 진입 시 skill 이 생성:
```json
{
"skillName": "rails",
"subcommand": "plan",
"sprintId": "SPRINT-001",
"contractId": "01HW0XYZ...",
"sessionId": "<session-id>",
"createdAt": "2026-04-10T12:00:00Z",
"ageSeconds": 5,
"allowedTools": ["Read", "Write", "Edit", "Bash", "Grep", "Glob"]
}
```
skill 의 entry 스크립트가 이 파일을 기록한다. 이 파일이 있으면 "skill 진입 완료" 증거.
### Skill Trace 파일
`.rails/skill-trace.jsonl` — 자매가 파이프라인 진행 중 호출한 도구 로그:
```jsonl
{"ts":1765789200,"tool":"Read","cwd":"/home/narang/projects/arang","session_id":"ab12"}
{"ts":1765789201,"tool":"Bash","cwd":"/home/narang/projects/arang","session_id":"ab12"}
{"ts":1765789202,"tool":"Write","cwd":"/home/narang/projects/arang","session_id":"ab12"}
```
Contract validator 는 이 파일에서 다음을 확인:
- trace 가 존재하는가?
- 첫 이벤트 이전에 `skill-context.json` 이 세팅됐는가?
- 세션 ID 가 일관된가? (중간에 다른 세션이 개입 안 했는가)
## 우회 사례 & 대응
| 우회 시도 | 감지 | 대응 |
|---|---|---|
| 자매가 skill 무시하고 직접 Write | Pre-hook (skill-context 없음) | exit 2 로 차단 + 에러 메시지 |
| 자매가 skill 호출 후 **딴 데서** 작업 | Post-hook (cwd mismatch) | trace 에 기록, validator 에서 FAIL |
| 자매가 skill 호출 후 stale 세션 재사용 | Pre-hook (ageSeconds > 300) | exit 2 + 재시작 안내 |
| 자매가 trace 파일 삭제 | Pre-hook (trace 파일 없음) | exit 2 |
| 자매가 trace 파일 변조 | Post-hook signature 불일치 | (향후) hash chain 으로 감지 |
## 예외 — Escape Hatch
긴급 상황에서 enforcement 를 일시 해제할 필요가 있을 수 있다:
- 환경 변수 `RAILS_ENFORCE=off` 세팅 시 hook 이 warn 만 남기고 통과
- 단, **반드시 로그에 기록**되고 다음 스프린트 시작 시 경고 표시
- production 에서는 기본 on
## 실패 시 복구
skill 우회가 감지된 경우:
1. 자매의 최근 commit 을 `git revert` 로 되돌림 (hanarang-rails orchestrator 권한)
2. 파이프라인 FSM 을 이전 상태로 rollback
3. 디스코드에 에스컬레이션 메시지 전송
4. 로그에 구조화 기록
## 참고
- `principles.md` 원칙 4
- `failure-audit.md` F1
- Claude Code hooks: `.claude/settings.json``PreToolUse` / `PostToolUse` matcher

View File

@@ -0,0 +1,224 @@
# Design — Sprint Contract
> **처방 대상**: F2 (DoD 강제 실패), F6 (환경 검증 누락)
## 컨셉
Sprint Contract 는 "이 스프린트/작업을 무엇으로 합격 판정할지" 를 **기계가 읽고 검증할 수 있는 형식**으로 고정한 불변 문서다.
- 형식: JSON (Zod schema 로 검증)
- 생성 시점: 스프린트/작업 시작 직전
- 수정 권한: 생성 후 **불변** (변경하려면 contract 버전을 올려야 함)
- 검증자: `validator.ts` 가 실행 결과 + contract → PASS/FAIL 판정
- 저장 위치: `.rails/contracts/<sprint-id>.sprint-contract.json`
## Schema (초안)
```ts
const SprintContract = z.object({
version: z.literal('v1'),
id: z.string().ulid(),
sprintId: z.string(), // e.g. SPRINT-001
createdAt: z.string().datetime(),
type: z.enum(['scaffold', 'feature', 'refactor', 'bugfix', 'migration', 'infra']),
// DoD (Definition of Done) - 체크 리스트
dod: z.object({
checks: z.array(z.object({
id: z.string(), // e.g. 'backend-build'
description: z.string(), // 사람이 읽는 설명
kind: z.enum([
'file_exists', // 특정 파일 존재
'command_success', // bash 커맨드 exit 0
'regex_in_file', // 파일 내 regex 매칭
'http_status', // HTTP 엔드포인트 200
'db_query', // DB 쿼리 결과
'process_listening', // 포트 리스닝
'artifact_schema', // JSON artifact 이 schema 통과
'manual_checklist', // QA 체크리스트 (다랑이)
]),
spec: z.unknown(), // kind 별 파라미터
blocking: z.boolean().default(true), // false 면 경고만
})),
}),
// 환경 전제 - 없으면 스프린트 시작 거부
environmentPrerequisites: z.array(z.object({
name: z.string(), // e.g. 'docker'
check: z.enum(['command_exists', 'port_open', 'env_var', 'file_exists', 'http_reachable']),
spec: z.unknown(),
reason: z.string(), // 왜 필요한지
})),
// 금지 사항 - 있으면 FAIL
nonGoals: z.array(z.string()),
// 실행 검증 커맨드
runtimeValidation: z.object({
commands: z.array(z.object({
name: z.string(),
command: z.string(),
cwd: z.string().optional(),
env: z.record(z.string()).optional(),
timeoutMs: z.number().default(60000),
expectExitCode: z.number().default(0),
})),
}),
// 리스크 플래그 - 다랑이가 주의 깊게 봐야 할 영역
riskFlags: z.array(z.enum([
'security-sensitive',
'data-migration',
'breaking-change',
'ux-regression',
'performance-critical',
'needs-spike',
])),
// 리뷰어 프로파일
reviewerProfile: z.enum(['static', 'runtime', 'browser']),
// 승인 게이트
approvalGates: z.object({
impl: z.boolean().default(true), // 나랑이 self-test
review: z.boolean().default(true), // 다랑이 QA
deploy: z.boolean().default(true), // 이랑이 pre-flight
}),
})
```
## 생성 흐름
1. 하랑이(Planner)가 스프린트 문서(`.plans/sprints/SPRINT-NNN-*.md`) 작성
2. `rails contract generate <sprint-id>` 실행
3. 스프린트 문서에서 "완료 기준" / "검증 커맨드" 섹션 파싱
4. 휴리스틱 + Zod schema 로 contract 초안 생성
5. 하랑이가 필요하면 수정 (단, 스프린트 시작 후 **불변**)
6. `rails contract freeze <contract-id>` 로 잠금
7. SQLite `contracts` 테이블에 저장, pipeline state 전이 키로 사용
## Validator 흐름
```ts
async function validate(contractPath: string, workdir: string): Promise<ValidationResult> {
const contract = SprintContract.parse(JSON.parse(await readFile(contractPath, 'utf8')))
const results: CheckResult[] = []
// 1. 환경 전제 먼저 (실패 시 short-circuit)
for (const prereq of contract.environmentPrerequisites) {
const r = await checkPrerequisite(prereq)
if (!r.ok) return { verdict: 'ABORT_PRECHECK', failed: r }
}
// 2. 런타임 검증 커맨드 실행
for (const cmd of contract.runtimeValidation.commands) {
results.push(await runCommand(cmd))
}
// 3. DoD 체크 수행
for (const check of contract.dod.checks) {
results.push(await runDodCheck(check, workdir))
}
// 4. 종합 판정
const blockingFails = results.filter(r => !r.pass && r.blocking)
return {
verdict: blockingFails.length === 0 ? 'PASS' : 'FAIL',
results,
failedChecks: blockingFails,
}
}
```
## DoD check 종류별 구현
| kind | 설명 | 예시 |
|---|---|---|
| `file_exists` | 경로에 파일 존재 | `src/app/page.tsx` |
| `command_success` | 명령어 exit 0 | `pnpm tsc --noEmit` |
| `regex_in_file` | 파일 내 regex 매칭 | `README.md`, `/## Getting Started/` |
| `http_status` | HTTP 200 OK | `curl -s -o /dev/null -w '%{http_code}' http://localhost:3000/api/health` |
| `db_query` | SQL 쿼리 결과 검증 | `SELECT COUNT(*) FROM settings` ≥ 1 |
| `process_listening` | 포트 LISTEN 상태 | `ss -ltn sport = :3000` |
| `artifact_schema` | JSON 파일이 Zod schema 통과 | `review-output.json` ⊆ ReviewOutput |
| `manual_checklist` | 사람/QA agent 체크박스 | 다랑이 체크리스트 (`qa-template.md`) |
## 예시 Contract (Sprint 001)
```json
{
"version": "v1",
"id": "01HW0XYZ...",
"sprintId": "SPRINT-001",
"type": "scaffold",
"createdAt": "2026-04-10T12:00:00Z",
"dod": {
"checks": [
{
"id": "ts-strict-tsconfig",
"description": "tsconfig.json 이 strict 모드",
"kind": "regex_in_file",
"spec": { "path": "tsconfig.json", "pattern": "\"strict\"\\s*:\\s*true" },
"blocking": true
},
{
"id": "typecheck",
"description": "pnpm tsc --noEmit 통과",
"kind": "command_success",
"spec": { "command": "pnpm tsc --noEmit" },
"blocking": true
},
{
"id": "vitest-runs",
"description": "vitest 기동 가능",
"kind": "command_success",
"spec": { "command": "pnpm vitest --version" },
"blocking": true
}
]
},
"environmentPrerequisites": [
{ "name": "node22", "check": "command_exists", "spec": { "command": "node" }, "reason": "Node 22 필수" },
{ "name": "pnpm", "check": "command_exists", "spec": { "command": "pnpm" }, "reason": "패키지 매니저" }
],
"nonGoals": ["XState 머신 실제 구현", "자매 spawn", "디스코드 연동"],
"runtimeValidation": {
"commands": [
{ "name": "install", "command": "pnpm install", "timeoutMs": 180000 },
{ "name": "typecheck", "command": "pnpm tsc --noEmit", "timeoutMs": 60000 }
]
},
"riskFlags": [],
"reviewerProfile": "static",
"approvalGates": { "impl": true, "review": true, "deploy": false }
}
```
## 불변성 강제
- `contracts` 테이블 `frozen_at` 컬럼 non-null 이면 write 거부
- contract 파일은 ro 퍼미션 (`chmod 0444`)
- 수정이 필요하면 새 버전의 contract 를 생성 (`v1.1`)
- 파이프라인은 frozen contract 만 참조 가능
## 실패 모드별 매핑
- **F2**: validator 가 `command_success` 로 실기동 검증 강제 → build 만으로 pass 불가
- **F6**: `environmentPrerequisites` 미달 → 스프린트 시작 거부 (pre-check 실패)
## CLI
```bash
rails contract generate <sprint-id> # 초안 생성
rails contract edit <contract-id> # 편집 (frozen 전만)
rails contract freeze <contract-id> # 잠금
rails contract validate <contract-id> # 수동 실행
rails contract show <contract-id> # pretty print
rails contract history <sprint-id> # v1, v1.1 ... 전체 이력
```
## 참고
- `principles.md` 원칙 3, 6
- `failure-audit.md` F2, F6
- `qa-template.md``manual_checklist` kind 의 구조

View File

@@ -0,0 +1,170 @@
# Design — State Machine (XState v5)
> **처방 대상**: F4 (핸드오프 불안정), F3 (QA 자동 라우팅 누락)
## 왜 XState?
- **결정론**: 동일 입력 → 동일 전이 (테스트 가능)
- **시각화**: Stately Inspector 로 런타임 상태를 관찰 가능
- **Actor 모델**: 자매별 격리된 actor, 메시지 패싱으로 통신
- **타입 안전**: TypeScript + XState v5 의 typegen 으로 상태/이벤트 완전 타입화
- **영속화 가능**: `createActor` 의 snapshot API 로 SQLite 저장/복원
## 최상위 머신 (Pipeline Machine)
```
┌─────────┐
│ idle │─── REQUEST ──────────┐
└─────────┘ │
┌──────────────┐
│ planning │ (actor: harang)
└──────┬───────┘
│ PLAN_READY
┌──────────────┐
│ implementing │ (actor: narang)
└──────┬───────┘
│ IMPL_DONE
┌──────────────┐
│ reviewing │ (actor: darang)
└──┬────────┬──┘
APPROVE │ │ REQUEST_CHANGES
│ │
▼ ▼
┌──────────┐ (→ implementing)
│ deploying│ [재작업 루프, 최대 3회]
└────┬─────┘
│ DEPLOY_DONE
┌──────────┐
│ done │
└──────────┘
(어느 상태에서든)
── TIMEOUT ──> retrying (자동 재시도)
── ERROR (N회 초과) ──> escalated (사용자 알림)
```
## 상태 정의
| 상태 | 담당 actor | 입장 액션 | 출구 이벤트 |
|---|---|---|---|
| `idle` | — | FSM ready | `REQUEST` |
| `planning` | harang (Planner) | contract 생성, planner spawn | `PLAN_READY` / `ERROR` |
| `implementing` | narang (Generator) | worker spawn, feature 브랜치 | `IMPL_DONE` / `ERROR` |
| `reviewing` | darang (Evaluator) | QA runtime 진입, checklist 로드 | `APPROVE` / `REQUEST_CHANGES` / `ERROR` |
| `deploying` | erang (Infra) | project type detect, deploy | `DEPLOY_DONE` / `ERROR` |
| `retrying` | — | backoff + re-enter prev state | `RETRY` |
| `escalated` | — | 디스코드 알림, 사용자 깨우기 | `RESUME` / `ABORT` |
| `done` | — | 결과 아카이브, 세션 종료 | — |
## 이벤트 스키마 (Zod)
```ts
const PipelineEvent = z.discriminatedUnion('type', [
z.object({ type: z.literal('REQUEST'), projectName: z.string(), requirements: z.string() }),
z.object({ type: z.literal('PLAN_READY'), planDir: z.string(), sprintId: z.string() }),
z.object({ type: z.literal('IMPL_DONE'), branch: z.string(), commits: z.array(z.string()) }),
z.object({ type: z.literal('APPROVE'), reviewArtifact: z.string() }),
z.object({ type: z.literal('REQUEST_CHANGES'), issues: z.array(ReviewIssue) }),
z.object({ type: z.literal('DEPLOY_DONE'), deployArtifact: z.string() }),
z.object({ type: z.literal('ERROR'), actor: SisterName, reason: z.string(), retryable: z.boolean() }),
z.object({ type: z.literal('TIMEOUT'), actor: SisterName, elapsedMs: z.number() }),
z.object({ type: z.literal('RETRY') }),
z.object({ type: z.literal('ABORT'), reason: z.string() }),
])
```
## 재작업 루프
- `reviewing``REQUEST_CHANGES``implementing`
- 이 루프는 최대 3회까지 허용
- 4회째 진입 시 자동으로 `escalated`
- 재작업 루프 카운트는 FSM context 에 저장 (`reviewRound`)
## 컨텍스트 (FSM context)
```ts
interface PipelineContext {
pipelineId: string // ULID
projectName: string
requirements: string
currentSprintId: string | null
reviewRound: number // 재작업 카운트
retryCount: number // 타임아웃 재시도 카운트
lastError: PipelineEvent | null
contractPath: string | null
actors: {
harang: ActorRef | null
narang: ActorRef | null
darang: ActorRef | null
erang: ActorRef | null
}
}
```
## 자매별 actor 머신 (서브 머신)
각 자매는 독립 actor 머신. 최상위 머신과는 이벤트로 통신.
예시 — Narang (Generator) actor:
```
┌────────┐
│ idle │─── START_IMPL ───┐
└────────┘ │
┌───────────────┐
│ pre-flight │ (환경 검증)
└───────┬───────┘
│ OK
┌───────────────┐
│ implementing │ (OpenClaw spawn)
└───────┬───────┘
│ DONE
┌───────────────┐
│ self-test │ (DoD 체크)
└───────┬───────┘
│ PASS
┌───────────────┐ (실패 시 → error)
│ done │
└───────────────┘
```
## 영속화 (SQLite)
- `pipelines` 테이블: `id`, `project_name`, `current_state`, `context_json`, `created_at`, `updated_at`
- `state_transitions` 테이블: `pipeline_id`, `from_state`, `to_state`, `event_type`, `event_payload`, `timestamp`
- `actor_spawns` 테이블: `pipeline_id`, `actor_name`, `spawned_at`, `pid`, `exit_code`
매 전이마다 `state_transitions` 에 append. context 는 `pipelines` 에 upsert.
## 크래시 복원
- 프로세스 재시작 시 `pipelines.current_state` + `context_json` 로 FSM 복원
- 진행 중이던 actor 는 죽었으므로, `reviewing` → 새 actor spawn
- 복원 이벤트 `RESUMED` 를 state_transitions 에 기록
## 테스트 전략
- **단위**: 각 상태에서의 전이 테이블을 Vitest 로 전수 검증
- **시나리오**: 성공 경로 / 재작업 1회 / 재작업 3회 escalation / 타임아웃 재시도 성공 / 타임아웃 3회 escalation
- **속성 기반**: fast-check 으로 랜덤 이벤트 시퀀스 → invariants 체크 (절대 `done` 에서 `implementing` 으로 못 간다 등)
- **시각화 회귀**: Stately Inspector 스냅샷 diff
## 열려있는 질문 (Sprint 001 에서 결정)
- [ ] 동시 병렬 파이프라인 허용? 아니면 single pipeline lock?
- [ ] 스프린트 내 작업 병렬화 (P 마크) 지원? 아니면 항상 직렬?
- [ ] 재시도 counter reset 정책 (새 이벤트마다? 아니면 영구?)
## 참고
- `principles.md` 원칙 2
- `failure-audit.md` F3, F4
- XState v5 docs: https://stately.ai/docs/xstate

213
.plans/failure-audit.md Normal file
View File

@@ -0,0 +1,213 @@
# Failure Audit — hanarang-harness (구)
> 증거 기반 실패 분석. 추측 아님. 2026-04-10 실제 대화 로그에서 추출.
## 분석 대상
- 대상 시스템: [`hanarang/openclaw-harness`](https://git.nabomhalang.co.kr/hanarang/openclaw-harness) (현재 private)
- 증거 1: 2026-04-10 새벽 아랑(Arang) 프로젝트 Sprint 001 디스코드 세션 로그
- 증거 2: `~/.openclaw/workspace/memory/``*timed-out*`, `ESCALATED`, 실패 기록
- 증거 3: hanarang-harness 커밋 히스토리 — `63c6d76 fix: thinking tier 되돌림 — xhigh는 무한 대기 유발` 등 fix 커밋 패턴
## 실패 모드 정리
### F1 — 하네스 Skill Bypass
**증상**
- 나랑이가 "worker 스폰해서 바로 시작한다" 라고 말하고 구현 진입
- `hanarang-harness` skill 의 구조화된 진입 경로(`plan-sprint.lobster``implement-sprint.lobster`)를 거치지 않음
- 자매 본인이 직접 처리 → 결과물의 품질이 skill 템플릿에 묶이지 않음
**증거**
```
나랑이: 프론트엔드+백엔드 풀세팅이니까 worker 스폰해서 바로 시작한다.
worker 스폰한다. Sprint 001 전체 구현 들어간다.
```
**근본 원인**
- Skill 은 "권고"일 뿐 강제 메커니즘이 없음
- OpenClaw system prompt 에 "반드시 skill 을 거쳐야 한다" 는 강제문이 약하거나 없음
- skill 바이패스를 감지하는 post-hook 이 없음
- skill 진입 여부가 artifact 에 기록되지 않아 사후 검증 불가
**영향**
- 파이프라인 일관성 상실
- 결과물이 스프린트 템플릿과 어긋남
- 리뷰어(다랑이)가 체크할 기준이 없어짐
**처방** → Sprint 002 (Skill Enforcement)
---
### F2 — DoD 자동 강제 실패
**증상**
- 나랑이가 "build 검증 통과" 만 하고 "완료" 보고
- 실행 검증, E2E, DB 마이그레이션, env 확인 등 없이 "커밋 푸시할까?" 물어봄
- 하랑이가 **수동으로** "build만 통과한 건 좋아. 근데 아직 완료 판정은 아니야" 라고 되돌림
**증거**
```
나랑이: Backend tsc + nest build 통과
Frontend next build 통과
Prisma generate 완료
커밋 푸시할까? @하랑이
하랑이: 아니야, 아직은 안 돼.
지금 상태는 build 검증까지야. 내가 준 Sprint 001 완료 기준은 실행 검증 포함이었어.
```
**근본 원인**
- 스프린트의 "완료 기준" 이 markdown 텍스트로만 존재 (기계가 검증 불가)
- Sprint Contract 객체가 없음 → validator 가 없음
- "완료" 판정이 자매의 주관에 맡겨짐
**영향**
- 매 스프린트마다 하랑이가 수동 감사
- 하랑이가 빠뜨리면 부실 완료가 통과됨
- 사용자가 "커밋 푸시는 기본이고 다랑이한테 QA" 라고 수동 개입해야 함
**처방** → Sprint 003 (Sprint Contract + Zod validator)
---
### F3 — QA 단계 자동 라우팅 누락
**증상**
- 나랑이가 build 완료 → 바로 푸시 시도
- QA(다랑이) 단계가 파이프라인에서 **선택적**으로 설계됨
- 사용자(나봄하랑)가 "다 완료됐으면 다랑이한테 보고해서 QA 피드백받고 다시 개발해" 라고 직접 명시해야 발동
**증거**
```
나봄하랑: 커밋 푸쉬는 기본이고, 다 완료됐으면 다랑이한테 보고해서 QA 피드백받고 다시 개발해
```
**근본 원인**
- `review-sprint.lobster` 는 존재하지만 **auto-trigger** 되지 않음
- 하랑이가 "@다랑이" 멘션을 명시적으로 안 걸면 다랑이가 깨지 않음
- 파이프라인이 LLM 의 분기 판단에 의존
**영향**
- QA 단계가 실질적으로 옵셔널
- 결과물 품질이 하랑이의 "이번엔 QA 필요할까?" 판단에 의존
- 사용자가 지속적으로 리마인드
**처방** → Sprint 004 (상태 전이 기반 핸드오프 — Impl 완료 → QA 강제 전이)
---
### F4 — 핸드오프 멘션 불안정
**증상**
- Gap 감지 루프에서 "APPROVE → 이랑이 / REQUEST_CHANGES → 나랑이" 분기가 LLM 판단에 맡겨짐
- 잘못된 자매 호출, 멘션 씹힘 사례 발생
- Lobster 워크플로우의 분기 로직이 언제나 예측 가능하지 않음
**증거**
- 사용자 증언: "멘션도 제대로 안되고", "잘못 자매를 호출"
- 커밋 `dd07c67 refactor: Lobster 하이브리드 구조로 재설계` — Lobster 자체가 여러 번 재설계됨
**근본 원인**
- Lobster 분기 문법이 LLM 해석에 의존
- 결정론적 state machine 이 없음
- 핸드오프 방식이 "디스코드 멘션" 이라서 멘션 파싱 / 알림 전달 / 자매 wake 라는 3단계를 거침 — 각 단계가 실패 지점
**영향**
- 같은 입력 → 다른 결과
- 디버깅이 사실상 불가능 (재현성 없음)
- 사용자가 파이프라인 신뢰 상실
**처방** → Sprint 001 (XState FSM 스켈레톤) + Sprint 004 (상태 전이 핸드오프)
---
### F5 — 중간 끊김 / 타임아웃 무한대기
**증상**
- `request-timed-out` 메모리 파일 여러 건 존재
- 과거 "thinking tier xhigh 는 무한대기 유발" 이력 (fix 커밋 존재)
- 재시도 정책이 없거나 약함
**증거**
```
~/.openclaw/workspace/memory/2026-04-04-request-timed-out-before-a-res.md
~/.openclaw/workspace/memory/2026-04-08-request-timed-out-before-a-res.md
커밋: 63c6d76 fix: thinking tier 되돌림 — xhigh는 무한 대기 유발
```
**근본 원인**
- 자매 호출에 명시적 timeout 이 없거나 기본값이 과도함
- timeout 발생 시 재시도 정책 부재
- 파이프라인이 타임아웃 자매를 기다리며 좀비 상태가 됨
**영향**
- 사용자가 수동으로 죽이고 재시작
- 중간에 끊기면 어디까지 진행했는지 복구 경로 없음
**처방** → Sprint 005 (Resilience policy — exponential backoff + auto retry + escalation)
---
### F6 — 환경 검증 누락
**증상**
- 나랑이가 "Docker/MariaDB 실기동 미검증 (서버에 Docker 없음)" 를 **리스크 항목**으로만 보고하고 넘어감
- "환경이 없어서 skip" 이 용인됨
**증거**
```
나랑이: 남은 리스크:
Docker/MariaDB 실기동 미검증 (서버에 Docker 없음)
WebSocket 실연결은 브라우저 필요
OpenClaw Gateway 실연결은 Gateway 있어야 함
Frontend next build 미검증
```
**근본 원인**
- 스프린트 시작 전 **환경 전제 검사(pre-flight)** 가 없음
- "환경 없음 = 검증 skip" 이 contract 에서 허용됨
- 자매가 검증 불가 항목을 "리스크" 로 포장해 통과시킴
**영향**
- 실제로 안 돌아가는 코드가 "완료" 로 표시됨
- 프로덕션 직전에 발견되어 롤백
- QA 가 검증할 수 있는 환경이 없는데도 파이프라인이 진행됨
**처방** → Sprint 003 (Sprint Contract 의 `environment_prerequisites` 필드) + Sprint 000 의 환경 감사
---
## 요약 매트릭스
| 코드 | 실패 | 처방 Sprint | 원칙 참조 |
|---|---|---|---|
| F1 | Skill bypass | Sprint 002 | 원칙 4 (Skill 진입 강제) |
| F2 | DoD 강제 실패 | Sprint 003 | 원칙 3 (Sprint Contract) |
| F3 | QA 자동 라우팅 누락 | Sprint 004 | 원칙 2 (결정론적 FSM) |
| F4 | 핸드오프 멘션 불안정 | Sprint 001 + 004 | 원칙 2 (결정론적 FSM) |
| F5 | 타임아웃 무한대기 | Sprint 005 | 원칙 7 (재시도/에스컬레이션) |
| F6 | 환경 검증 누락 | Sprint 003 + 000 | 원칙 6 (환경 검증 선행) |
## 안 깨진 것 (유지/계승)
hanarang-harness 의 모든 것이 실패는 아니었다. 다음은 유지/계승한다:
| 자산 | 상태 | 계승 방식 |
|---|---|---|
| `scaffold.sh` (.plans/ 스캐폴딩) | ✅ 잘 동작 | Rails 의 `rails init` 로 포팅 |
| `install.sh --role` (자매별 에이전트 격리) | ✅ 잘 동작 | `rails install --role <role>` 로 포팅 |
| 에이전트 md 파일 (planner/worker/reviewer/deploy-manager) | ✅ 내용 좋음 | 템플릿으로 계승, FSM actor 시스템 프롬프트 소스 |
| `doctor.sh` (환경 체크) | ✅ 유용 | `rails doctor` 로 포팅 + environment_prerequisites 로 확장 |
| `hanarang/*.md` (자매별 전문화 프롬프트) | ✅ 유지 | 그대로 계승 |
| 디스코드 포럼 포스트 자동 생성 | ⚠️ 재검토 | discord.js 로 재구현 |
| Hook 3종 (파이프라인 자동화) | ⚠️ 아이디어 계승 | hanarang-rails hook 체계로 재작성 |
## 교훈 (Lessons Learned)
1. **"권고" 는 강제가 아니다.** LLM 은 명시적 강제가 없으면 가장 짧은 경로를 택한다.
2. **인간이 중재자가 되면 파이프라인은 실패다.** 자동화의 목적은 개입 최소화.
3. **"완료" 의 정의는 기계가 판정해야 한다.** 자매의 주관 = 불일치 = 부실 완료.
4. **핸드오프는 데이터 구조이지 메시지 교환이 아니다.** 멘션은 UI, 핸드오프는 state transition.
5. **재시도 없는 타임아웃은 좀비를 만든다.** fail-fast + retry + escalate.
6. **환경이 없으면 스프린트를 시작하지 마라.** skip 용인 = 거짓 완료.

View File

@@ -0,0 +1,105 @@
# Migration — from hanarang-harness
> 기존 [`hanarang/openclaw-harness`](https://git.nabomhalang.co.kr/hanarang/openclaw-harness) (현재 private archive) 에서 hanarang-rails 로의 이관 가이드.
## 요약
| 항목 | 기존 (hanarang-harness) | 신규 (hanarang-rails) |
|---|---|---|
| 런타임 | bash + Node hooks + Lobster | Node 22 + TypeScript (strict) |
| 오케스트레이션 | Lobster 워크플로우 (.lobster 파일) | XState v5 머신 |
| 상태 | 파일 분산 (`state/`) | SQLite 단일 파일 |
| 핸드오프 | 디스코드 멘션 | FSM state transition |
| 디스코드 | bridge.sh (curl) | discord.js v14 (알림 전용) |
| DoD 검증 | 텍스트만 (기계 판정 불가) | Zod schema + validator |
| Skill 진입 | 권고 | hook 기반 강제 |
| 재시도 | 없음 / 약함 | exponential backoff + escalation |
| QA | 선택적 | 체크리스트 강제 |
## 자산 매트릭스
### ✅ 유지 / 포팅
| 자산 | 위치 | 신규 매핑 |
|---|---|---|
| `scaffold.sh` | 기존 `scripts/scaffold.sh` | `rails scaffold` 서브커맨드 |
| `install.sh --role` | 기존 `scripts/install.sh` | `rails install --role` 서브커맨드 |
| `doctor.sh` | 기존 `scripts/doctor.sh` | `rails doctor` + env prereq 통합 |
| 에이전트 md (planner/worker/reviewer/deploy-manager) | 기존 `agents/*.md` | `agents/` 로 복사, XState actor 의 system prompt 소스 |
| 자매별 특화 프롬프트 (`agents/hanarang/*`) | 기존 | 동일 경로 유지 |
| `.plans/` 디렉토리 구조 컨벤션 | 기존 | **계승**. 단 `.plans/rails/` 추가 (contract / qa artifact) |
| 디스코드 포럼 포스트 컨셉 | 기존 `bridge.sh` 일부 | discord.js 로 재구현 |
| Hook 3종 아이디어 | 기존 `hooks/*.js` | enforcement hook 으로 재설계 (Sprint 002) |
| 프로젝트 타입별 배포 분기 | `deploy-manager.md` 최근 업데이트 | `erang` actor 의 project-type-detector 로 포팅 |
### ⚠️ 재검토 후 부분 계승
| 자산 | 상태 | 이유 |
|---|---|---|
| Lobster 워크플로우 파일 (`workflows/*.lobster`) | **폐기** | 결정성 부족의 근본 원인. XState 로 전면 대체. 단, "4단계 Plan→Impl→Review→Deploy" 컨셉은 계승 |
| bridge.sh | **폐기** | curl 기반 브릿지는 장애 많음. discord.js 로 교체 |
| thinking_tier 파라미터 | **제한** | xhigh 금지, high 까지만 |
| route-task.sh | 재평가 | rails 에서는 FSM 이 라우팅 담당, 별도 필요 여부 검토 |
| GLM / GPT 모델 라우팅 룰 | 계승 | `rails models.yaml` 로 정리 |
| install.sh 의 에이전트 격리 로직 | 계승 | role 기반 격리 유지 |
### ❌ 폐기
| 자산 | 이유 |
|---|---|
| 워킹트리의 .lobster 파일 | 결정성 없음, LLM 의존 |
| 멘션 기반 자매 간 핸드오프 | F3 / F4 의 원인 |
| "build 통과 = 완료" 판정 | F2 의 원인 |
| "환경 없음 → skip" 용인 | F6 의 원인 |
| 무제한 timeout (또는 기본값 너무 김) | F5 의 원인 |
## 마이그레이션 단계 (운영자 관점)
### Step 0 — 읽기 (사전)
1. `.plans/failure-audit.md` 를 전체 읽는다 (F1~F6 이해)
2. `.plans/OVERVIEW.md` 의 성공 기준을 검토
3. 기존 hanarang-harness-archive 를 **참조만**. 코드는 복사하지 않는다 (인용 OK)
### Step 1 — 실행 환경 준비 (이랑이 역할)
1. `pnpm install` 로 rails 설치
2. `rails doctor` — Node 22, pnpm, SQLite, OpenClaw 커맨드 확인
3. `.env` 작성 (`DISCORD_TOKEN`, `DISCORD_GUILD_ID`, `GITEA_TOKEN`, …)
4. `rails migrate from-hanarang-harness /home/erang/hanarang-harness-archive` 실행 → 포팅 레포트
### Step 2 — 4자매 배포
1. 하랑이(LXC 104): `rails install --role planner`
2. 나랑이(LXC 105): `rails install --role generator`
3. 다랑이(LXC 106): `rails install --role evaluator`
4. 이랑이(LXC 107): `rails install --role infra` (본 머신에서도)
5. 각 자매의 `openclaw.json` 에 rails skill 등록
6. 각 자매에 `rails doctor --role <role>` 로 확인
### Step 3 — 첫 프로젝트 (아랑)
1. 아랑 프로젝트는 이미 `hanarang/arang` Gitea repo 존재
2. `cd /home/erang/projects/arang && rails bind`
3. `rails run "Sprint 002 — Live2D 아바타"` (기획 기반)
4. end-to-end 완주 확인
5. 실패 시 `rails status <id>` + 로그 분석
### Step 4 — 레거시 철거
1. 4자매 LXC 에서 기존 hanarang-harness 진입점 비활성화 (`.openclaw/skills/hanarang-harness/` 제거)
2. `bridge.sh` 프로세스 종료
3. `hanarang-harness-archive` 는 읽기 전용 상태로 보존 (절대 삭제 금지)
4. Gitea `openclaw-harness` repo 는 private 상태 유지 (이미 Sprint 000 에서 전환 완료)
## 회고 포인트
마이그레이션 후 SPRINT-007 완료 시점에서 다음 회고 문서 작성:
- `.plans/retro/hanarang-harness-retro.md` — 기존 하네스의 교훈
- `.plans/retro/hanarang-rails-v1.md` — rails v1 설계 결정 재평가
- 추후 v2 후보 기능 목록
## 롤백 경로
만약 rails 가 실패하면:
1. `hanarang-harness-archive` 를 checkout 해서 기존 bash 스크립트 복구
2. 4자매 LXC 에 기존 install.sh 재실행
3. `bridge.sh reset` 으로 디스코드 브릿지 부활
4. 실패 원인을 `.plans/retro/rails-failure.md` 에 기록하고 rails 개선
단, **하네스 자체가 실패** 라는 건 재설계 원칙이 잘못됐다는 뜻이니 rollback 전에 반드시 사용자(자기야) 승인.

View File

@@ -0,0 +1,63 @@
# SPRINT-000 — 세이프티 네트 + 실패 감사 + 프로젝트 세팅
> **목표**: 기존 hanarang-harness 를 안전하게 보존하고, 실패 모드를 감사하고, hanarang-rails 의 계획 문서 뼈대를 세운다. 코드는 단 한 줄도 작성하지 않는다.
## Scope
- 기존 자산 보존 (push, archive, repo 가시성 전환)
- 실패 감사 문서화 (F1F6)
- `.plans/` 계층 문서 세트 작성 (OVERVIEW, failure-audit, design/*, sprints/*, migration/*)
- Gitea 에 신규 repo 생성 + 초기 커밋
- Claude Code harness-setup init (CLAUDE.md, Plans.md, .claude/rules, hooks)
## Non-Goals
- package.json, src/, TypeScript 코드 — **Sprint 001 에서 시작**
- XState 머신 구현, orchestrator 구현
- 자매 spawn 로직
- 디스코드 브릿지 코드
## Tasks
| # | 내용 | DoD | Status |
|---|---|---|---|
| 0.1 | 기존 hanarang-harness dirty 파일 커밋 + 24커밋 push | `origin/main == HEAD` | cc:완료 |
| 0.2 | 중복 clone 삭제 + archive 이름 변경 | `hanarang-harness-archive` 존재, `openclaw-harness` 없음 | cc:완료 |
| 0.3 | Gitea `openclaw-harness` repo private 전환 | API 로 `private: true` 확인 | cc:완료 |
| 0.4 | Gitea `hanarang-rails` repo 생성 (public) | URL 접근 가능 | cc:완료 |
| 0.5 | 로컬 `hanarang-rails/` 스캐폴딩 (README, LICENSE, .gitignore) | 초기 커밋 push 완료 | cc:완료 |
| 0.6 | `harness-setup init` — CLAUDE.md (분할), Plans.md, .claude/rules/, hooks/ | 전부 생성 + CLAUDE.md ≤ 30 lines | cc:WIP |
| 0.7 | `.plans/OVERVIEW.md` 작성 | 문서 존재, 성공 기준 8개 명시 | cc:WIP |
| 0.8 | `.plans/failure-audit.md` 작성 (F1F6) | 문서 존재, 각 실패에 증거 인용 포함 | cc:WIP |
| 0.9 | `.plans/design/state-machine.md` 작성 | 문서 존재, 상태 다이어그램 포함 | cc:WIP |
| 0.10 | `.plans/design/sprint-contract.md` 작성 | Zod schema 초안 포함 | cc:WIP |
| 0.11 | `.plans/design/skill-enforcement.md` 작성 | Layer 14 명시 | cc:WIP |
| 0.12 | `.plans/design/handoff.md` 작성 | HandoffMessage 스키마 포함 | cc:WIP |
| 0.13 | `.plans/design/retry-policy.md` 작성 | backoff 수식 + 에스컬레이션 트리거 | cc:WIP |
| 0.14 | `.plans/design/qa-template.md` 작성 | 타입별 템플릿 4종 이상 | cc:WIP |
| 0.15 | `.plans/sprints/SPRINT-001~007.md` 초안 작성 | 각 파일 존재 + Scope/Tasks/DoD | cc:WIP |
| 0.16 | `.plans/migration/from-hanarang-harness.md` 초안 작성 | 유지/계승/폐기 자산 매트릭스 | cc:WIP |
| 0.17 | Plans.md 루트 인덱스 완성 (스프린트 목차 + 링크) | 모든 스프린트 링크 유효 | cc:WIP |
| 0.18 | Sprint 000 전체 commit + push | Gitea 에 반영 | cc:TODO |
## Definition of Done
Sprint 000 은 다음 조건을 **전부** 만족해야 완료:
1. 기존 자산 손실 0건 (git push 완료, dirty 변경 보존)
2. `.plans/` 하위 17개 문서 존재 + 최소 스켈레톤 내용
3. README, CLAUDE.md, Plans.md 루트 인덱스 유효
4. Gitea `hanarang-rails` 에 Sprint 000 전체가 push 되어있음
5. `harness-setup init` 이 생성한 hook/settings 파일이 정상
6. CLAUDE.md ≤ 30 lines (분할 완료)
## Risks
- 장문의 문서 작성 중 컨텍스트 한도 초과 → **해결**: 병렬 Write + phase 단위 commit
- 디자인 문서가 Sprint 001 에서 뒤집힐 가능성 → **허용**: Sprint 000 은 "초안" 품질. 001 에서 정제
## Exit Criteria
- Plans.md 상단에 "Current Sprint: SPRINT-001" 표시
- `git log --oneline` 에 Sprint 000 완료 커밋이 보임
- 사용자(나봄하랑) 가 계획 문서 확인 후 승인

View File

@@ -0,0 +1,91 @@
# SPRINT-001 — 스켈레톤: XState FSM + orchestrator + CLI
> **목표**: 결정론적 파이프라인의 뼈대를 세운다. XState 머신이 메모리에서 돌고, SQLite 에 상태가 저장되고, `rails status` CLI 로 조회 가능해야 한다.
## Type
`scaffold`
## Prerequisites
- Sprint 000 완료
- Node 22, pnpm 설치됨
- `.plans/design/state-machine.md` 확정
## Scope
- `package.json` / `tsconfig.json` (strict + noUncheckedIndexedAccess)
- Core 디렉토리 구조: `src/orchestrator/`, `src/contract/`, `src/cli/`, `tests/`
- XState v5 머신 정의 (`src/orchestrator/machine.ts`)
- Zod 이벤트/컨텍스트 스키마 (`src/orchestrator/events.ts`)
- SQLite 스키마 + migration (`src/orchestrator/store.ts`)
- citty CLI 뼈대 (`rails <subcommand>`)
- `rails start` — 빈 파이프라인 시작
- `rails status` — 현재 상태 표시
- Vitest 테스트: 머신 상태 전이 단위 테스트 (성공 경로 1건 + 에러 경로 1건)
## Non-Goals
- 실제 자매 spawn (Sprint 004)
- Sprint Contract validator (Sprint 003)
- Skill enforcement hook 로직 (Sprint 002)
- 재시도 policy 실구현 (Sprint 005)
- QA runtime (Sprint 006)
- 디스코드 브릿지 (Sprint 004 또는 별도)
## Tasks
| # | 내용 | DoD | Depends | Status |
|---|---|---|---|---|
| 1.1 | `package.json` + `pnpm-lock.yaml` + 의존성 설치 | `pnpm install` 성공 | — | cc:TODO |
| 1.2 | `tsconfig.json` strict + noUncheckedIndexedAccess | `pnpm tsc --noEmit` 통과 | 1.1 | cc:TODO |
| 1.3 | 디렉토리 구조 생성 (`src/`, `tests/`, `fixtures/`) | `ls src/` 기대대로 | 1.1 | cc:TODO |
| 1.4 | `src/orchestrator/events.ts` — Zod 이벤트 스키마 | 타입체크 통과 | 1.2 | cc:TODO |
| 1.5 | `src/orchestrator/context.ts` — FSM context 타입 | 타입체크 통과 | 1.4 | cc:TODO |
| 1.6 | `src/orchestrator/machine.ts` — XState v5 머신 (stub actor) | `createActor` 성공 | 1.5 | cc:TODO |
| 1.7 | `src/orchestrator/store.ts` — better-sqlite3 + 스키마 + prepared statements | 테이블 4개 생성됨 | 1.1 | cc:TODO |
| 1.8 | `src/orchestrator/persist.ts` — FSM snapshot ↔ SQLite 변환 | snapshot round-trip 테스트 통과 | 1.6, 1.7 | cc:TODO |
| 1.9 | `src/cli/index.ts` — citty 진입점 | `pnpm rails --help` 출력 | 1.1 | cc:TODO |
| 1.10 | `src/cli/start.ts``rails start <projectName>` | 파이프라인 생성, ULID 반환 | 1.9, 1.8 | cc:TODO |
| 1.11 | `src/cli/status.ts``rails status [<id>]` | 현재 상태 표 출력 | 1.9, 1.8 | cc:TODO |
| 1.12 | `tests/machine.test.ts` — 성공 경로 + 에러 경로 | 2개 이상 pass | 1.6 | cc:TODO |
| 1.13 | `tests/store.test.ts` — snapshot round-trip | 1개 이상 pass | 1.8 | cc:TODO |
| 1.14 | `src/logger.ts` — pino 구조화 로거 + pipelineId 필드 | 로그 JSON 형식 확인 | 1.1 | cc:TODO |
| 1.15 | `src/env.ts` — Zod 검증 env | 환경변수 검증 실패 시 throw | 1.1 | cc:TODO |
| 1.16 | README 실행 방법 섹션 업데이트 | `## 실행 방법` 존재 | — | cc:TODO |
## Definition of Done
### 자동 검증 (contract validator)
- `pnpm install` exit 0
- `pnpm tsc --noEmit` exit 0
- `pnpm test` 전체 통과 (머신 + store 최소 3개)
- `pnpm rails --help` exit 0 + 출력에 `start`, `status` 포함
- `pnpm rails start test-project` → ULID 출력
- `pnpm rails status <id>``idle` 또는 `planning` 상태 표시
- SQLite 파일 (`data/rails.db`) 생성 확인
- `package.json` lockfile 이 `pnpm-lock.yaml` (yarn/npm 아님)
### 수동 검증 (다랑이 manual)
- [ ] 아무 파일에도 `console.*` 호출 없음 (pino 사용)
- [ ] 아무 파일에도 `any` 타입 없음 (Zod 경계 제외)
- [ ] 모든 외부 입력에 Zod 검증 존재
- [ ] 에러 처리에 Result / neverthrow 또는 명시적 try/catch + 재던지기
- [ ] `.claude/rules/stack.md` 의 코딩 룰 전수 준수
## 환경 전제
- `node --version` ≥ 22
- `pnpm --version` ≥ 9
- `/home/erang/hanarang-rails/` 쓰기 권한
## Risks
- XState v5 + TypeScript strict + Zod 조합이 typegen 세팅 초기 삽질 가능 → **완화**: fixture 예제 따라하기
- better-sqlite3 네이티브 빌드 — Node 22 ABI 대응 필요 → **완화**: `@types/better-sqlite3` + build-from-source 옵션
- FSM snapshot round-trip 테스트가 XState 내부 구현 의존 → **완화**: public API (`getPersistedSnapshot` / `restore`) 만 사용
## Exit Criteria
- 모든 태스크 `cc:완료`
- `pnpm test` 전부 pass
- Plans.md 에서 SPRINT-001 Status 가 `cc:완료 [hash]`
- feature 브랜치 머지됨 (PR + 다랑이 QA)

View File

@@ -0,0 +1,59 @@
# SPRINT-002 — Skill 강제 진입 + Bypass 감지
> **목표**: F1 (자매 skill bypass) 해결. pre/post hook 이 skill 진입을 강제하고 우회를 감지한다.
## Type
`feature`
## Prerequisites
- Sprint 001 완료 (FSM + CLI 뼈대)
- `.plans/design/skill-enforcement.md` 확정
## Scope
- `.claude/rules/stack.md` 의 hooks 섹션을 enforcement 로 교체
- `hooks/pre-tool.sh` 실제 로직 구현 (skill-context 검증)
- `hooks/post-tool.sh` 실제 로직 구현 (skill-trace append)
- `src/enforcement/skill-context.ts` — context 파일 생성/읽기
- `src/enforcement/skill-trace.ts` — trace append / 조회
- `src/enforcement/guard.ts` — context 유효성 검증 로직
- `rails skill-context {create|show|clear}` CLI
- `rails skill-trace show <pipeline-id>` CLI
- 테스트: context 없을 때 pre-hook exit 2, 있을 때 exit 0
## Non-Goals
- Claude Code skill 자체 정의 (기존 claude-code-harness 사용)
- OpenClaw 쪽 bypass 감지 (일단 Claude Code 환경 먼저)
- Hash chain 변조 방지 (v2 이후)
## Tasks
| # | 내용 | DoD | Status |
|---|---|---|---|
| 2.1 | `src/enforcement/skill-context.ts` + Zod schema | 타입체크 통과, test pass | cc:TODO |
| 2.2 | `src/enforcement/skill-trace.ts` | append 동작 확인 | cc:TODO |
| 2.3 | `src/enforcement/guard.ts` — context 유효성 + 만료 체크 | 만료 / stale 케이스 테스트 | cc:TODO |
| 2.4 | `hooks/pre-tool.sh` 구현 (jq + context 파일 체크) | context 없으면 exit 2 | cc:TODO |
| 2.5 | `hooks/post-tool.sh` 구현 (trace append) | 이벤트가 jsonl 에 추가됨 | cc:TODO |
| 2.6 | `src/cli/skill-context.ts` (create/show/clear) | CLI 동작 | cc:TODO |
| 2.7 | `src/cli/skill-trace.ts` (show) | CLI 동작 | cc:TODO |
| 2.8 | `tests/enforcement.test.ts` — 시나리오 4종 | 전부 pass | cc:TODO |
| 2.9 | escape hatch (`RAILS_ENFORCE=off`) | 환경변수 테스트 pass | cc:TODO |
## Definition of Done
### 자동
- `hooks/pre-tool.sh` exit 2 when no context
- `hooks/post-tool.sh` appends valid JSON to `.rails/skill-trace.jsonl`
- `rails skill-context create``.rails/skill-context.json` 생성
- `tests/enforcement.test.ts` 전부 pass
- `RAILS_ENFORCE=off` 로 bypass 가능 (로그 남김)
### 수동
- [ ] bypass 감지 후 orchestrator 가 revert 경로를 알고 있음 (실제 revert 는 Sprint 005 에서)
- [ ] 디스코드 에스컬레이션 훅 포인트 존재
## Exit Criteria
F1 failure mode 가 재현 불가능해진다. `rails skill-trace show` 에 우회 이벤트가 보이면 즉시 알람.

View File

@@ -0,0 +1,65 @@
# SPRINT-003 — Sprint Contract + DoD Validator
> **목표**: F2 (DoD 강제 실패), F6 (환경 검증 누락) 해결. contract 가 기계 판정 가능하고 불변이며 validator 가 pass/fail 을 확정한다.
## Type
`feature`
## Prerequisites
- Sprint 001 완료 (FSM + store)
- `.plans/design/sprint-contract.md` 확정
## Scope
- `src/contract/schema.ts` — Zod schema 전체 (SprintContract + 하위 타입)
- `src/contract/checks/` — check kind 별 구현 (file_exists, command_success, regex_in_file, http_status, db_query, process_listening, artifact_schema, manual)
- `src/contract/validator.ts` — 전체 검증 파이프라인 (prerequisites → runtime → dod → verdict)
- `src/contract/generator.ts` — 스프린트 md → contract 초안
- `src/contract/store.ts` — SQLite `contracts` 테이블 + frozen_at
- `rails contract <subcommand>` CLI 전 서브커맨드 구현 (generate / edit / freeze / validate / show / history)
- Fixture: SPRINT-001 contract 샘플
- 테스트: 각 check kind 단위 테스트 + validator integration 테스트
## Non-Goals
- manual check 의 LLM 판단 로직 (Sprint 006)
- contract 의 diff/merge 도구
- 시각화 UI
## Tasks
| # | 내용 | DoD | Status |
|---|---|---|---|
| 3.1 | Zod schema 전체 (`SprintContract`) | 타입체크 pass | cc:TODO |
| 3.2 | `src/contract/checks/file_exists.ts` | unit test pass | cc:TODO |
| 3.3 | `src/contract/checks/command_success.ts` (execa + timeout) | timeout 테스트 포함 | cc:TODO |
| 3.4 | `src/contract/checks/regex_in_file.ts` | unit test | cc:TODO |
| 3.5 | `src/contract/checks/http_status.ts` | mock server 테스트 | cc:TODO |
| 3.6 | `src/contract/checks/db_query.ts` | fixture SQLite 테스트 | cc:TODO |
| 3.7 | `src/contract/checks/process_listening.ts` | port open/close 테스트 | cc:TODO |
| 3.8 | `src/contract/checks/artifact_schema.ts` | Zod 검증 위임 | cc:TODO |
| 3.9 | `src/contract/checks/manual.ts` — placeholder (Sprint 006 에서 활성) | 현재는 SKIP | cc:TODO |
| 3.10 | `src/contract/validator.ts` — 전체 파이프라인 | integration test | cc:TODO |
| 3.11 | `src/contract/generator.ts` — md 파싱 → draft contract | SPRINT-001 으로 generate 테스트 | cc:TODO |
| 3.12 | `src/contract/store.ts` — frozen_at 불변성 | write 거부 테스트 | cc:TODO |
| 3.13 | `rails contract generate/edit/freeze/validate/show/history` | 전 서브커맨드 동작 | cc:TODO |
| 3.14 | Environment prerequisite 체크 (ABORT_PRECHECK) | pre-check fail 테스트 | cc:TODO |
| 3.15 | SPRINT-001 샘플 contract 생성 + validate | PASS 결과 확인 | cc:TODO |
## Definition of Done
### 자동
- `rails contract generate SPRINT-001` → JSON 생성
- `rails contract freeze <id>``frozen_at` 세팅 + ro 퍼미션
- `rails contract validate <id>` → PASS / FAIL / ABORT_PRECHECK 중 하나 명확 출력
- frozen contract 에 write 시도 시 에러
- env prerequisite 미달 → `ABORT_PRECHECK`
- 단일 check 종류별 테스트 8개 이상 pass
### 수동
- [ ] "build 통과 = 완료" 시나리오를 수동 테스트. FAIL 판정 확인
- [ ] contract JSON 이 사람이 읽기 좋은 포맷
## Exit Criteria
F2 / F6 재현 불가. 어떤 스프린트도 contract 없이 시작될 수 없고, build 만으로 PASS 받을 수 없다.

View File

@@ -0,0 +1,68 @@
# SPRINT-004 — 4자매 핸드오프 엔진 + 디스코드 알림
> **목표**: F3 / F4 해결. 자매 간 통신을 상태 전이로 강제하고, 디스코드는 사용자 알림 전용으로 분리한다.
## Type
`feature`
## Prerequisites
- Sprint 001 (FSM)
- Sprint 003 (Contract)
- `.plans/design/handoff.md`, `state-machine.md` 확정
- Discord bot token / guild id (`.env`)
## Scope
- `src/handoff/message.ts``HandoffMessage` Zod schema
- `src/handoff/spawn.ts``spawnSister(opts)` — execa + AbortController + JSON 파싱 + Zod 검증
- `src/orchestrator/actors/harang.ts` — planner actor
- `src/orchestrator/actors/narang.ts` — generator actor
- `src/orchestrator/actors/darang.ts` — evaluator actor (Sprint 006 에서 QA runtime 상세 완성)
- `src/orchestrator/actors/erang.ts` — deploy actor
- `src/bridge/discord.ts` — discord.js v14 bridge (알림 전용)
- `src/bridge/events.ts` — FSM state transition → discord event mapping
- Spawn 계약 프로토콜 — OpenClaw 측에서 structured JSON 출력을 보장하는 방식 결정 (자매 system prompt 에 "반드시 JSON 형식으로 출력" 강제)
- `rails run <project>` 커맨드 — planning → implementing → reviewing → deploying → done 전체 동작
- Mock 자매 (`rails:mock` 모드) — 실제 OpenClaw 없이 로컬 테스트
## Non-Goals
- 재시도 로직 (Sprint 005)
- QA 체크리스트 실제 실행 (Sprint 006)
- 실배포 로직 (erang actor 는 mock)
## Tasks
| # | 내용 | DoD | Status |
|---|---|---|---|
| 4.1 | `HandoffMessage` discriminated union Zod schema | 4가지 actor 전부 커버 | cc:TODO |
| 4.2 | `spawnSister` — execa + timeout + structured output 파싱 | 시나리오 테스트 | cc:TODO |
| 4.3 | harang actor — plan 생성 호출 + stub 응답 | mock 시나리오 pass | cc:TODO |
| 4.4 | narang actor — impl 호출 + stub 응답 | mock 시나리오 pass | cc:TODO |
| 4.5 | darang actor — review 호출 (stub QA) | mock 시나리오 pass | cc:TODO |
| 4.6 | erang actor — deploy 호출 (stub) | mock 시나리오 pass | cc:TODO |
| 4.7 | XState 머신의 actor invoke 연결 | FSM test pass | cc:TODO |
| 4.8 | discord.js bridge 초기화 (login, guild 선택) | 봇 online | cc:TODO |
| 4.9 | state transition → discord message mapping | 포럼 포스트 동작 | cc:TODO |
| 4.10 | escalated / done 시 사용자 멘션 | 멘션 동작 확인 | cc:TODO |
| 4.11 | `rails run --mock <project>` 서브커맨드 | end-to-end mock pass | cc:TODO |
| 4.12 | OpenClaw spawn 커맨드 결정 + 문서화 | `docs/openclaw-integration.md` | cc:TODO |
| 4.13 | 자매 system prompt 에 structured output 강제 | 프롬프트 파일 존재 | cc:TODO |
## Definition of Done
### 자동
- `rails run --mock test-project``planning → implementing → reviewing → deploying → done` 전이 완주
- `state_transitions` 테이블에 전이 5건 이상
- discord bridge 가 전이마다 메시지 전송
- `HandoffMessage` Zod 위반 시 ERROR event 로 전이
- Timeout 시나리오는 일단 ERROR (Sprint 005 에서 retry 추가)
### 수동
- [ ] 실제 OpenClaw 자매 (LXC 104~107) 1회 연결 테스트 (환경 허용 시)
- [ ] 디스코드 포럼 포스트 한 번 수동 확인
- [ ] 멘션 스킵/실행 시나리오 수동 검증
## Exit Criteria
F3 / F4 재현 불가. 자매가 호출 안 되는 상황 없음. 멘션 기반 라우팅 제거 완료.

View File

@@ -0,0 +1,63 @@
# SPRINT-005 — Retry / Timeout / Escalation
> **목표**: F5 해결. 모든 transient 실패가 자동 재시도되고, 무한대기가 사라지고, N회 실패는 사용자 에스컬레이션.
## Type
`feature`
## Prerequisites
- Sprint 001 (FSM + store)
- Sprint 004 (actor spawn)
- `.plans/design/retry-policy.md` 확정
## Scope
- `src/resilience/backoff.ts` — exponential backoff + jitter
- `src/resilience/classifier.ts` — error → retryable 분류
- `src/resilience/retry.ts` — XState retrying 상태 로직
- `src/resilience/escalate.ts` — 에스컬레이션 이벤트 생성 + discord 알림
- `src/resilience/kill.ts` — 자식 프로세스 그룹 kill 보장
- FSM 에 `retrying`, `escalated` 상태 완전 구현
- Timeout 감지 — actor spawn 에 `AbortController` 연동 + SIGKILL 보장
- `rails resume <pipeline-id>` — escalated 에서 재개
- `rails abort <pipeline-id>` — 강제 중단 + cleanup
- Escalations 테이블 + 디스코드 메시지 포맷
## Non-Goals
- 자동 복구 (crash recovery) 는 별도 Sprint 에서 (006 이후)
- Hash chain 변조 방지
## Tasks
| # | 내용 | DoD | Status |
|---|---|---|---|
| 5.1 | backoff 구현 + 테스트 (ms 범위 검증) | unit test | cc:TODO |
| 5.2 | error classifier + 테스트 (retryable/non) | unit test | cc:TODO |
| 5.3 | XState retrying 상태 + counter | 3회 후 escalate 전이 | cc:TODO |
| 5.4 | actor spawn 에 AbortController + SIGKILL | kill 테스트 | cc:TODO |
| 5.5 | 파이프라인 전체 타임아웃 (60분 기본) | 시나리오 테스트 | cc:TODO |
| 5.6 | `src/resilience/escalate.ts` + 디스코드 알림 | escalation 메시지 전송 | cc:TODO |
| 5.7 | `escalations` 테이블 + snapshot 저장 | 레코드 검증 | cc:TODO |
| 5.8 | `rails resume` — escalated → 이전 상태 복귀 | resume 후 파이프라인 재개 | cc:TODO |
| 5.9 | `rails abort` — 강제 종료 + cleanup | 좀비 프로세스 0건 | cc:TODO |
| 5.10 | xhigh thinking tier 거부 로직 | tier 검증 테스트 | cc:TODO |
| 5.11 | Non-retryable 즉시 escalate 로직 | 시나리오 테스트 | cc:TODO |
## Definition of Done
### 자동
- Timeout 시나리오 3회 → `escalated` 전이
- Non-retryable 에러 1회 → 즉시 `escalated`
- `rails abort` 후 자식 프로세스 전수 kill (ps aux 확인)
- `rails resume` 후 파이프라인이 이전 상태에서 재개
- xhigh thinking tier 시도 → 명시적 거부
### 수동
- [ ] 실제 60분 초과 시나리오 1회 수동 검증 (단축된 timeout 으로)
- [ ] escalation 메시지가 사용자에게 명확하게 전달되는지
- [ ] `.openclaw/workspace/memory/request-timed-out-*` 와 같은 파일 생성 0건
## Exit Criteria
F5 재현 불가. "파이프라인이 멈췄는데 왜 멈췄는지 모르겠다" 상태가 나오지 않음.

View File

@@ -0,0 +1,69 @@
# SPRINT-006 — QA Template Runtime (다랑이 완성)
> **목표**: F3 (QA 자동 라우팅 누락) 최종 해결 + F2 (DoD 강제) 의 QA 측면 완성. 다랑이가 체크리스트를 실제로 실행하고 구조화된 verdict 를 낸다.
## Type
`feature`
## Prerequisites
- Sprint 003 (Contract schema)
- Sprint 004 (darang actor stub)
- `.plans/design/qa-template.md` 확정
## Scope
- `qa-templates/` 디렉토리 — YAML 템플릿 (scaffold, feature, refactor, bugfix, migration, infra)
- `src/qa/template.ts` — YAML 템플릿 로드 + Zod schema
- `src/qa/runtime.ts` — 자동 check + manual check 실행 엔진
- `src/qa/artifact.ts` — QA artifact 생성 + 저장
- `src/qa/verdict.ts` — verdict 판정 규칙
- `src/qa/llm-manual.ts` — manual check 에 LLM 위임 (자매가 코드 읽고 판단)
- `src/orchestrator/actors/darang.ts` 완성 (stub → 실제 QA 수행)
- `rails qa run <sprint-id>` CLI
- `rails qa show <artifact-id>` CLI
- 프로젝트별 `qa-extra.yaml` merge 지원
- Fixture: 샘플 QA artifact 1개 (SPRINT-001 용)
## Non-Goals
- QA UI 대시보드
- 다랑이 LLM 자체 fine-tuning
- Browser-based QA (v2)
## Tasks
| # | 내용 | DoD | Status |
|---|---|---|---|
| 6.1 | qa-templates/scaffold-v1.yaml | Zod 통과 | cc:TODO |
| 6.2 | qa-templates/feature-v1.yaml | Zod 통과 | cc:TODO |
| 6.3 | qa-templates/bugfix-v1.yaml | Zod 통과 | cc:TODO |
| 6.4 | qa-templates/migration-v1.yaml | Zod 통과 | cc:TODO |
| 6.5 | qa-templates/refactor-v1.yaml | Zod 통과 | cc:TODO |
| 6.6 | qa-templates/infra-v1.yaml | Zod 통과 | cc:TODO |
| 6.7 | `src/qa/template.ts` — loader + extends/merge | unit test | cc:TODO |
| 6.8 | `src/qa/runtime.ts` — 자동 + manual 실행 | integration test | cc:TODO |
| 6.9 | `src/qa/artifact.ts` — JSON 저장 schema | Zod 검증 | cc:TODO |
| 6.10 | `src/qa/verdict.ts` — APPROVE/REQUEST_CHANGES/ABORT 규칙 | unit test | cc:TODO |
| 6.11 | `src/qa/llm-manual.ts` — OpenClaw 자매 호출 wrapper | mock test | cc:TODO |
| 6.12 | darang actor 완성 — template 로드 + runtime 실행 | e2e mock pass | cc:TODO |
| 6.13 | `rails qa run/show` CLI | 동작 | cc:TODO |
| 6.14 | `qa-extra.yaml` merge 테스트 | 시나리오 pass | cc:TODO |
| 6.15 | Sprint 001 samples 로 실제 QA 실행 | APPROVE 결과 | cc:TODO |
## Definition of Done
### 자동
- `rails qa run SPRINT-001` → artifact 생성 + verdict 반환
- 필수 체크 1개라도 fail → REQUEST_CHANGES
- `manual_checklist` kind 이 LLM 호출로 결과 생성
- artifact JSON 이 Zod schema 에 검증됨
- extends 템플릿 merge 동작
### 수동
- [ ] 실제 다랑이 프롬프트로 Sprint 001 자체를 QA 돌려봄
- [ ] QA 근거 링크 (파일:라인) 포함 확인
- [ ] 메모리 규칙 `feedback_qa_thorough.md` 에 부합 (항목 수 제한 없음)
## Exit Criteria
다랑이가 수동 개입 없이 체크리스트를 완주한다. QA 가 "대충 통과" 되는 사례 0건.

View File

@@ -0,0 +1,67 @@
# SPRINT-007 — hanarang-harness 마이그레이션 + 실서비스 투입
> **목표**: 기존 hanarang-harness 사용자(=나봄하랑, 4자매)가 hanarang-rails 로 옮겨 실사용. 기존 프로젝트(아랑 등)에 바로 적용.
## Type
`migration`
## Prerequisites
- Sprint 001 ~ 006 완료
- `.plans/migration/from-hanarang-harness.md` 확정
- 기존 hanarang-harness-archive 에 대한 read-only 접근
## Scope
- `rails migrate from-hanarang-harness <path>` CLI — 기존 repo 를 읽어 hanarang-rails 세팅으로 변환
- scaffold.sh 기능 포팅 → `rails scaffold` (프로젝트별 `.plans/` 구조 생성)
- install.sh --role 기능 포팅 → `rails install --role <role>`
- doctor.sh 기능 확장 → `rails doctor` + environment_prerequisites 체크
- hanarang-harness 의 agent md 파일 → `rails agents import`
- 4자매 LXC (104~107) 에 rails 설치 스크립트
- 아랑(Arang) 프로젝트를 rails 로 운영 (첫 실사용 프로젝트)
- 기존 bridge.sh 제거 + discord.js bridge 로 교체
- 문서: `docs/migration-guide.md`, `docs/operations.md`
## Non-Goals
- 기존 hanarang-harness repo 영구 삭제 (archive 유지)
- 모든 기존 프로젝트 일괄 마이그레이션 (아랑만 first)
- 백업/롤백 자동화 (수동)
## Tasks
| # | 내용 | DoD | Status |
|---|---|---|---|
| 7.1 | `rails migrate from-hanarang-harness` 스캐너 | source 분석 레포트 | cc:TODO |
| 7.2 | `.plans/` 구조 변환 로직 | hanarang-harness .plans/ → rails .plans/ | cc:TODO |
| 7.3 | agent md 파일 포팅 (`rails agents import`) | 4자매 프롬프트 유효 | cc:TODO |
| 7.4 | `rails scaffold <project>` — .plans/ 구조 생성 | scaffold test | cc:TODO |
| 7.5 | `rails install --role <role>` — 4자매 LXC 설치 | install test | cc:TODO |
| 7.6 | `rails doctor` — env prereq + 4자매 gateway | erang 실기동 확인 | cc:TODO |
| 7.7 | discord.js bridge 배포 (기존 bridge.sh 대체) | 디스코드 알림 동작 | cc:TODO |
| 7.8 | 4자매 LXC (104~107) 에 rails 배포 | 전 자매 `rails --version` 동일 | cc:TODO |
| 7.9 | 아랑(Arang) 프로젝트에 rails 적용 | 첫 실제 Sprint 완주 | cc:TODO |
| 7.10 | 운영 문서 작성 (`docs/operations.md`) | 트러블슈팅 가이드 포함 | cc:TODO |
| 7.11 | hanarang-harness → hanarang-rails 체인지로그 | `CHANGELOG.md` 작성 | cc:TODO |
| 7.12 | 회고 문서 (`.plans/retro/hanarang-harness-retro.md`) | 교훈 정리 | cc:TODO |
## Definition of Done
### 자동
- `rails migrate from-hanarang-harness ../hanarang-harness-archive` 성공
- `rails install --role planner` → 4자매 LXC 에 rails 설치 확인
- `rails doctor` 가 4자매 gateway 전부 OK
- 아랑 프로젝트 Sprint 001 을 rails 로 실행 → end-to-end 완주 (사용자 개입 최소)
### 수동
- [ ] 디스코드 포럼 포스트가 정상 생성되는지
- [ ] 4자매 멘션 알림이 정상 전달되는지
- [ ] 에스컬레이션 상황에서 사용자가 1번에 이해할 수 있는지
- [ ] 기존 hanarang-harness 의 실패 모드 F1~F6 재현 시도 → 모두 재현 불가
## Exit Criteria
- 나봄하랑이 실제로 rails 를 사용 중
- 아랑 프로젝트 (또는 후속 프로젝트) 의 최근 1주일 파이프라인 전수 성공률 ≥ 90%
- 에스컬레이션 발생 시 원인과 복구 경로가 명확
- 기존 hanarang-harness 로 복귀할 이유 없음

13
CLAUDE.md Normal file
View File

@@ -0,0 +1,13 @@
# CLAUDE.md — hanarang-rails
> Claude Code / OpenClaw 세션에서 이 저장소를 다룰 때 반드시 따라야 할 규칙.
> 상세는 아래 문서 참조. 이 파일은 얇게 유지한다.
- @.claude/rules/project.md — 프로젝트 요약 + 개입 지점
- @.claude/rules/stack.md — 기술 스택 + 코딩 룰
- @.claude/rules/principles.md — 7가지 하드 설계 원칙
- @.claude/rules/workflow.md — 플랜/커밋/금지 사항
## 한 문장 요약
**강제 기반 결정론 파이프라인.** 자매들이 달릴 레일을 코드로 깐다. 권고는 금지, FSM 만 믿는다.

40
Plans.md Normal file
View File

@@ -0,0 +1,40 @@
# Plans.md — hanarang-rails
> 루트 인덱스만. 상세는 `.plans/sprints/*.md` 참조.
> 포맷: v2 (Task / 내용 / DoD / Depends / Status)
## 📖 관련 문서
- [`.plans/OVERVIEW.md`](.plans/OVERVIEW.md) — 전체 목표 / 범위 / 성공 기준
- [`.plans/failure-audit.md`](.plans/failure-audit.md) — F1F6 실패 감사
- [`.plans/design/`](.plans/design/) — 설계 문서 세트
- [`.plans/sprints/`](.plans/sprints/) — 스프린트별 상세 명세
- [`.plans/migration/`](.plans/migration/) — 마이그레이션 가이드
## Sprint 목차
| # | Sprint | 상세 | Status |
|---|---|---|---|
| 0 | 세이프티 네트 + 실패 감사 + 프로젝트 세팅 | [SPRINT-000](.plans/sprints/SPRINT-000-safety-and-audit.md) | cc:WIP |
| 1 | 스켈레톤: XState FSM + orchestrator + .plans/ 스캐폴딩 | [SPRINT-001](.plans/sprints/SPRINT-001-skeleton.md) | cc:TODO |
| 2 | Skill 강제 진입 hook + bypass 감지 + 차단 | [SPRINT-002](.plans/sprints/SPRINT-002-enforcement.md) | cc:TODO |
| 3 | Sprint Contract + DoD validator (Zod) | [SPRINT-003](.plans/sprints/SPRINT-003-contract.md) | cc:TODO |
| 4 | 4자매 핸드오프 엔진 (상태 전이 기반) | [SPRINT-004](.plans/sprints/SPRINT-004-handoff.md) | cc:TODO |
| 5 | 재시도 / 타임아웃 / 에스컬레이션 policy | [SPRINT-005](.plans/sprints/SPRINT-005-resilience.md) | cc:TODO |
| 6 | QA 체크리스트 템플릿 + 다랑이 runtime | [SPRINT-006](.plans/sprints/SPRINT-006-qa.md) | cc:TODO |
| 7 | 기존 프로젝트 마이그레이션 (아랑 등) | [SPRINT-007](.plans/sprints/SPRINT-007-migration.md) | cc:TODO |
## 현재 스프린트
**Sprint 000 — 세이프티 네트 + 실패 감사 + 프로젝트 세팅** (`cc:WIP`)
계획 단계 문서 작성 중. Sprint 000 태스크는 `.plans/sprints/SPRINT-000-safety-and-audit.md` 참조.
## 마커 범례
| 마커 | 의미 |
|---|---|
| `cc:TODO` | 미착수 |
| `cc:WIP` | 작업 중 |
| `cc:blocked` | 의존 대기 |
| `cc:완료 [hash]` | 완료 (commit hash) |

8
hooks/post-tool.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env bash
# hanarang-rails post-tool hook (thin shim)
# 현재 no-op — Sprint 002 에서 skill bypass 감지 + revert 로직 주입 예정.
# 입력: stdin 으로 tool use result JSON
# 출력: exit 0 = proceed
set -euo pipefail
exit 0

8
hooks/pre-tool.sh Executable file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env bash
# hanarang-rails pre-tool hook (thin shim)
# 현재 no-op — Sprint 002 에서 skill-enforcement 로직 주입 예정.
# 입력: stdin 으로 tool use event JSON
# 출력: exit 0 = proceed, exit 2 = block
set -euo pipefail
exit 0