diff --git a/.claude/rules/principles.md b/.claude/rules/principles.md new file mode 100644 index 0000000..725a95d --- /dev/null +++ b/.claude/rules/principles.md @@ -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` 금지 (무한대기 유발 이력) diff --git a/.claude/rules/project.md b/.claude/rules/project.md new file mode 100644 index 0000000..f4c95e8 --- /dev/null +++ b/.claude/rules/project.md @@ -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/` diff --git a/.claude/rules/stack.md b/.claude/rules/stack.md new file mode 100644 index 0000000..2541c08 --- /dev/null +++ b/.claude/rules/stack.md @@ -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 패턴** — throw 대신 `neverthrow` 또는 자체 Result 로 에러 표면화 +- **로그는 pino** — `console.*` 금지 +- **파일 경로는 `node:path` + `import.meta.url`** — `__dirname` 금지 +- **환경변수는 Zod schema 로 검증된 env 객체 통해서만** — `process.env.X` 직접 참조 금지 diff --git a/.claude/rules/workflow.md b/.claude/rules/workflow.md new file mode 100644 index 0000000..badd74c --- /dev/null +++ b/.claude/rules/workflow.md @@ -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 충족 판정 diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..c4ba35d --- /dev/null +++ b/.claude/settings.json @@ -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" }] + } + ] + } +} diff --git a/.plans/OVERVIEW.md b/.plans/OVERVIEW.md new file mode 100644 index 0000000..a44b4ac --- /dev/null +++ b/.plans/OVERVIEW.md @@ -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 세션으로 개발 diff --git a/.plans/design/handoff.md b/.plans/design/handoff.md new file mode 100644 index 0000000..dcf1353 --- /dev/null +++ b/.plans/design/handoff.md @@ -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 { + 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 이벤트 처리 diff --git a/.plans/design/qa-template.md b/.plans/design/qa-template.md new file mode 100644 index 0000000..7d58247 --- /dev/null +++ b/.plans/design/qa-template.md @@ -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 와 연결 diff --git a/.plans/design/retry-policy.md b/.plans/design/retry-policy.md new file mode 100644 index 0000000..c74ef52 --- /dev/null +++ b/.plans/design/retry-policy.md @@ -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 { + // 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 ` — 재시도 + ⏹️ `rails abort ` — 중단 + 🔍 `rails inspect ` — 상세 조회 + +@나봄하랑 +``` + +## 타임아웃 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` 상태 정의 diff --git a/.plans/design/skill-enforcement.md b/.plans/design/skill-enforcement.md new file mode 100644 index 0000000..a29a9a2 --- /dev/null +++ b/.plans/design/skill-enforcement.md @@ -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 ` 슬래시 커맨드 진입 시 skill 이 생성: + +```json +{ + "skillName": "rails", + "subcommand": "plan", + "sprintId": "SPRINT-001", + "contractId": "01HW0XYZ...", + "sessionId": "", + "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 diff --git a/.plans/design/sprint-contract.md b/.plans/design/sprint-contract.md new file mode 100644 index 0000000..0243837 --- /dev/null +++ b/.plans/design/sprint-contract.md @@ -0,0 +1,224 @@ +# Design — Sprint Contract + +> **처방 대상**: F2 (DoD 강제 실패), F6 (환경 검증 누락) + +## 컨셉 + +Sprint Contract 는 "이 스프린트/작업을 무엇으로 합격 판정할지" 를 **기계가 읽고 검증할 수 있는 형식**으로 고정한 불변 문서다. + +- 형식: JSON (Zod schema 로 검증) +- 생성 시점: 스프린트/작업 시작 직전 +- 수정 권한: 생성 후 **불변** (변경하려면 contract 버전을 올려야 함) +- 검증자: `validator.ts` 가 실행 결과 + contract → PASS/FAIL 판정 +- 저장 위치: `.rails/contracts/.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 ` 실행 +3. 스프린트 문서에서 "완료 기준" / "검증 커맨드" 섹션 파싱 +4. 휴리스틱 + Zod schema 로 contract 초안 생성 +5. 하랑이가 필요하면 수정 (단, 스프린트 시작 후 **불변**) +6. `rails contract freeze ` 로 잠금 +7. SQLite `contracts` 테이블에 저장, pipeline state 전이 키로 사용 + +## Validator 흐름 + +```ts +async function validate(contractPath: string, workdir: string): Promise { + 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 # 초안 생성 +rails contract edit # 편집 (frozen 전만) +rails contract freeze # 잠금 +rails contract validate # 수동 실행 +rails contract show # pretty print +rails contract history # v1, v1.1 ... 전체 이력 +``` + +## 참고 + +- `principles.md` 원칙 3, 6 +- `failure-audit.md` F2, F6 +- `qa-template.md` — `manual_checklist` kind 의 구조 diff --git a/.plans/design/state-machine.md b/.plans/design/state-machine.md new file mode 100644 index 0000000..8958429 --- /dev/null +++ b/.plans/design/state-machine.md @@ -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 diff --git a/.plans/failure-audit.md b/.plans/failure-audit.md new file mode 100644 index 0000000..8943fd0 --- /dev/null +++ b/.plans/failure-audit.md @@ -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 ` 로 포팅 | +| 에이전트 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 용인 = 거짓 완료. diff --git a/.plans/migration/from-hanarang-harness.md b/.plans/migration/from-hanarang-harness.md new file mode 100644 index 0000000..a3304da --- /dev/null +++ b/.plans/migration/from-hanarang-harness.md @@ -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 ` 로 확인 + +### 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 ` + 로그 분석 + +### 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 전에 반드시 사용자(자기야) 승인. diff --git a/.plans/sprints/SPRINT-000-safety-and-audit.md b/.plans/sprints/SPRINT-000-safety-and-audit.md new file mode 100644 index 0000000..69e3ef5 --- /dev/null +++ b/.plans/sprints/SPRINT-000-safety-and-audit.md @@ -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 완료 커밋이 보임 +- 사용자(나봄하랑) 가 계획 문서 확인 후 승인 diff --git a/.plans/sprints/SPRINT-001-skeleton.md b/.plans/sprints/SPRINT-001-skeleton.md new file mode 100644 index 0000000..2bd7d22 --- /dev/null +++ b/.plans/sprints/SPRINT-001-skeleton.md @@ -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 `) +- `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 ` | 파이프라인 생성, ULID 반환 | 1.9, 1.8 | cc:TODO | +| 1.11 | `src/cli/status.ts` — `rails status []` | 현재 상태 표 출력 | 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 ` → `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) diff --git a/.plans/sprints/SPRINT-002-enforcement.md b/.plans/sprints/SPRINT-002-enforcement.md new file mode 100644 index 0000000..8fd627a --- /dev/null +++ b/.plans/sprints/SPRINT-002-enforcement.md @@ -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 ` 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` 에 우회 이벤트가 보이면 즉시 알람. diff --git a/.plans/sprints/SPRINT-003-contract.md b/.plans/sprints/SPRINT-003-contract.md new file mode 100644 index 0000000..8d154d7 --- /dev/null +++ b/.plans/sprints/SPRINT-003-contract.md @@ -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 ` 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 ` → `frozen_at` 세팅 + ro 퍼미션 +- `rails contract validate ` → 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 받을 수 없다. diff --git a/.plans/sprints/SPRINT-004-handoff.md b/.plans/sprints/SPRINT-004-handoff.md new file mode 100644 index 0000000..af3dd9e --- /dev/null +++ b/.plans/sprints/SPRINT-004-handoff.md @@ -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 ` 커맨드 — 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 ` 서브커맨드 | 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 재현 불가. 자매가 호출 안 되는 상황 없음. 멘션 기반 라우팅 제거 완료. diff --git a/.plans/sprints/SPRINT-005-resilience.md b/.plans/sprints/SPRINT-005-resilience.md new file mode 100644 index 0000000..7119bcd --- /dev/null +++ b/.plans/sprints/SPRINT-005-resilience.md @@ -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 ` — escalated 에서 재개 +- `rails abort ` — 강제 중단 + 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 재현 불가. "파이프라인이 멈췄는데 왜 멈췄는지 모르겠다" 상태가 나오지 않음. diff --git a/.plans/sprints/SPRINT-006-qa.md b/.plans/sprints/SPRINT-006-qa.md new file mode 100644 index 0000000..c118856 --- /dev/null +++ b/.plans/sprints/SPRINT-006-qa.md @@ -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 ` CLI +- `rails qa show ` 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건. diff --git a/.plans/sprints/SPRINT-007-migration.md b/.plans/sprints/SPRINT-007-migration.md new file mode 100644 index 0000000..70b59fa --- /dev/null +++ b/.plans/sprints/SPRINT-007-migration.md @@ -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 ` CLI — 기존 repo 를 읽어 hanarang-rails 세팅으로 변환 +- scaffold.sh 기능 포팅 → `rails scaffold` (프로젝트별 `.plans/` 구조 생성) +- install.sh --role 기능 포팅 → `rails install --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 ` — .plans/ 구조 생성 | scaffold test | cc:TODO | +| 7.5 | `rails install --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 로 복귀할 이유 없음 diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c9cec5c --- /dev/null +++ b/CLAUDE.md @@ -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 만 믿는다. diff --git a/Plans.md b/Plans.md new file mode 100644 index 0000000..d1fa978 --- /dev/null +++ b/Plans.md @@ -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) | diff --git a/hooks/post-tool.sh b/hooks/post-tool.sh new file mode 100755 index 0000000..15b51f8 --- /dev/null +++ b/hooks/post-tool.sh @@ -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 diff --git a/hooks/pre-tool.sh b/hooks/pre-tool.sh new file mode 100755 index 0000000..e6cc25d --- /dev/null +++ b/hooks/pre-tool.sh @@ -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