- 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>
7.1 KiB
7.1 KiB
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 에 반환하는 구조화 결과:
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 할 때 표준화된 계약:
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원칙 2failure-audit.mdF3, F4state-machine.md— 전이 정의retry-policy.md— TIMEOUT / ERROR 이벤트 처리