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:
24
.claude/rules/principles.md
Normal file
24
.claude/rules/principles.md
Normal 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
22
.claude/rules/project.md
Normal 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` 의 F1–F6 참조
|
||||
|
||||
## 개입 지점 (사용자)
|
||||
|
||||
사용자(나봄하랑)는 다음 시점에서만 개입한다:
|
||||
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
24
.claude/rules/stack.md
Normal 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
29
.claude/rules/workflow.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# 워크플로우 / 커밋 룰
|
||||
|
||||
## 플랜 문서가 곧 하네스
|
||||
|
||||
> 사용자 자기야의 하드 룰: **모든 단계를 상세 .MD로 작성. root `Plans.md` 는 참조만.**
|
||||
|
||||
- `Plans.md` (루트) — 스프린트 목차 + 각 상세 문서로 링크만
|
||||
- `.plans/OVERVIEW.md` — 프로젝트 전체 목표/범위/성공 기준
|
||||
- `.plans/failure-audit.md` — 기존 하네스의 실패 모드 분석 (F1–F6)
|
||||
- `.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
17
.claude/settings.json
Normal 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
119
.plans/OVERVIEW.md
Normal 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 001–003 완료 — "레일이 깔림" (스켈레톤 + 강제 + 계약)
|
||||
- M2: Sprint 004–006 완료 — "열차가 달림" (핸드오프 + 복원력 + QA)
|
||||
- M3: Sprint 007 완료 — "이전이 끝남" (마이그레이션 + 실서비스 투입)
|
||||
|
||||
## 관련 문서
|
||||
|
||||
- [`failure-audit.md`](./failure-audit.md) — F1–F6 실패 감사
|
||||
- [`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
198
.plans/design/handoff.md
Normal 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 이벤트 처리
|
||||
238
.plans/design/qa-template.md
Normal file
238
.plans/design/qa-template.md
Normal 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 와 연결
|
||||
180
.plans/design/retry-policy.md
Normal file
180
.plans/design/retry-policy.md
Normal 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` 상태 정의
|
||||
154
.plans/design/skill-enforcement.md
Normal file
154
.plans/design/skill-enforcement.md
Normal 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
|
||||
224
.plans/design/sprint-contract.md
Normal file
224
.plans/design/sprint-contract.md
Normal 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 의 구조
|
||||
170
.plans/design/state-machine.md
Normal file
170
.plans/design/state-machine.md
Normal 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
213
.plans/failure-audit.md
Normal 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 용인 = 거짓 완료.
|
||||
105
.plans/migration/from-hanarang-harness.md
Normal file
105
.plans/migration/from-hanarang-harness.md
Normal 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 전에 반드시 사용자(자기야) 승인.
|
||||
63
.plans/sprints/SPRINT-000-safety-and-audit.md
Normal file
63
.plans/sprints/SPRINT-000-safety-and-audit.md
Normal file
@@ -0,0 +1,63 @@
|
||||
# SPRINT-000 — 세이프티 네트 + 실패 감사 + 프로젝트 세팅
|
||||
|
||||
> **목표**: 기존 hanarang-harness 를 안전하게 보존하고, 실패 모드를 감사하고, hanarang-rails 의 계획 문서 뼈대를 세운다. 코드는 단 한 줄도 작성하지 않는다.
|
||||
|
||||
## Scope
|
||||
|
||||
- 기존 자산 보존 (push, archive, repo 가시성 전환)
|
||||
- 실패 감사 문서화 (F1–F6)
|
||||
- `.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` 작성 (F1–F6) | 문서 존재, 각 실패에 증거 인용 포함 | 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 1–4 명시 | 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 완료 커밋이 보임
|
||||
- 사용자(나봄하랑) 가 계획 문서 확인 후 승인
|
||||
91
.plans/sprints/SPRINT-001-skeleton.md
Normal file
91
.plans/sprints/SPRINT-001-skeleton.md
Normal 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)
|
||||
59
.plans/sprints/SPRINT-002-enforcement.md
Normal file
59
.plans/sprints/SPRINT-002-enforcement.md
Normal 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` 에 우회 이벤트가 보이면 즉시 알람.
|
||||
65
.plans/sprints/SPRINT-003-contract.md
Normal file
65
.plans/sprints/SPRINT-003-contract.md
Normal 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 받을 수 없다.
|
||||
68
.plans/sprints/SPRINT-004-handoff.md
Normal file
68
.plans/sprints/SPRINT-004-handoff.md
Normal 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 재현 불가. 자매가 호출 안 되는 상황 없음. 멘션 기반 라우팅 제거 완료.
|
||||
63
.plans/sprints/SPRINT-005-resilience.md
Normal file
63
.plans/sprints/SPRINT-005-resilience.md
Normal 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 재현 불가. "파이프라인이 멈췄는데 왜 멈췄는지 모르겠다" 상태가 나오지 않음.
|
||||
69
.plans/sprints/SPRINT-006-qa.md
Normal file
69
.plans/sprints/SPRINT-006-qa.md
Normal 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건.
|
||||
67
.plans/sprints/SPRINT-007-migration.md
Normal file
67
.plans/sprints/SPRINT-007-migration.md
Normal 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
13
CLAUDE.md
Normal 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
40
Plans.md
Normal 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) — F1–F6 실패 감사
|
||||
- [`.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
8
hooks/post-tool.sh
Executable 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
8
hooks/pre-tool.sh
Executable 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
|
||||
Reference in New Issue
Block a user