12 Commits

Author SHA1 Message Date
185320f1b9 fix(fsm): retry/replan budget 축소 + 테스트 검증 가드 추가
자기야 발견: maxReplans=2, maxReviewRounds=3 budget 으로 forced REQUEST_CHANGES
검증을 돌렸는데, FSM 자체는 정상 작동했지만 (replanCount=1 정확히 카운트, plan→
review×3→implement loop 정확히 발동) 총 12 review attempts × LLM 호출 30-60s =
실 운영에서 escalation 까지 15-30분 걸림. 사용자가 "무한 루프" 라고 느낄 정도.

수정:
- src/orchestrator/context.ts:
  maxReviewRounds: 3 → 2
  maxReplans: 2 → 1
  → 총 budget = (1+1)*(1+2) = 6 review attempts (이전 12 의 절반)
  → 실 운영 wall-clock 약 3-6 분 안에 escalation 도달
- src/orchestrator/machine.ts: 동일하게 default context 갱신
- tests/machine.test.ts: 새 budget 에 맞춰 round 수 조정
  - "after max review rounds" 테스트: 4 round → 3 round
  - "escalates after both budgets exhausted" 테스트: 12 round → 6 round
- sister-agent/src/spawn.ts:
  RAILS_FORCE_REVIEW_VERDICT 환경변수 가드 추가 (test-only).
  APPROVE / REQUEST_CHANGES / ABORT 중 하나 설정하면 LLM 우회하고 즉시 verdict
  반환. darang sister 에 박아서 retry FSM E2E 검증 가능.
  운영 시점엔 env 미설정 → no-op, LLM 응답 정상 사용.

테스트: 113 통과 (변경 없음).

검증 흔적: pipeline 01KNWB8WYRR11PGY8DYMNQ1BZD 가 forced REQUEST_CHANGES 로
이전 budget (12 round) 의 60% 까지 진행 후 수동 abort. transitions 18 개에
replanCount=1, reviewRound=3 정확히 보존됨 — persistence/FSM 모두 정상.
2026-04-11 04:12:42 +09:00
294abdbc25 feat(fsm): re-planning loop — review 다 실패하면 plan 단계로 되돌아감
자기야 요청: 안쪽 review-loop 다 써도 그냥 escalation 이 아니라, 한 단계
위에서 plan 부터 다시 짜야 함. 첫 plan 자체가 잘못된 접근일 수도 있으니까.

새 FSM:
  reviewing → REQUEST_CHANGES
    ├─ canReviewAgain (reviewRound < maxReviewRounds) → implementing
    ├─ canReplan (replanCount < maxReplans)           → planning
    │     • incrementReplanCount, resetReviewRound
    │     • lastError = "Re-planning after exhausted review rounds"
    │     • 다음 plan 단계가 priorStages 로 review issues 를 보고 새 접근
    └─ both exhausted                                  → escalated

기본값: maxReplans=2 → 총 budget = (1+2)*(1+3) = 12 round (3 plans × 4 reviews
each). 그 이상 가면 사용자 개입.

추가:
- src/orchestrator/context.ts: replanCount, maxReplans 필드 (default 0, 2)
- src/orchestrator/machine.ts: canReplan guard, incrementReplanCount action,
  reviewing.REQUEST_CHANGES 의 transition 분기
- tests/machine.test.ts: 기존 "escalates after max review rounds" 를 새
  re-plan 동작에 맞게 수정 + 두 개 새 케이스 추가
    1. maxReplans+maxReviewRounds 둘 다 소진 후 escalated
    2. re-plan 후 새 plan 으로 APPROVE → done 흐름

113 테스트 모두 통과 (105 → 113).
2026-04-11 01:47:40 +09:00
e4f8e6ef53 revert(bridge): discord.js 봇 전면 제거 — OpenClaw 가 봇 소유자
이전 커밋 08ea92f 은 잘못된 접근이었음. OpenClaw 가 이미 Discord 게이트웨이를
띄우고 있고 자매들의 봇 identity 는 거기 하나로 통일되어야 함. rails 가
discord.js 로 별도 봇을 등록하면 두 봇이 같은 채널에 공존하는 기이한 구조가
된다.

올바른 경로는 OpenClaw skill 의 user-invocable frontmatter 로 슬래시 커맨드를
노출하는 것이고, 이건 별도 커밋으로 ~/.openclaw/skills/hanarang-rails/ 에
반영됨.

삭제:
- src/bridge/ 전체 (discord-client, discord-commands, discord-notifier, index)
- tests/discord-notifier.test.ts
- package.json 의 discord.js 의존성
- src/cli/serve.ts 의 bridge 부트스트랩
- .env.example 의 DISCORD_* 블록

보존:
- runner.ts 의 PipelineLifecycleEvent + onEvent 훅 — 유닛 테스트/대시보드
  WebSocket 등에 재사용 가능
- POST /pipelines/start-async 엔드포인트 — skill 의 polling wrapper 가 이걸 씀
- runner opts.pipelineId 지원 — async start 가 의존함
- http.ts ServerOpts 의 onPipelineEvent/notifier 파라미터 — 추상화는 유지,
  serve.ts 가 주입을 안 할 뿐

테스트: 118 → 111 (discord-notifier 7 개 삭제), 나머지 그대로 통과.
2026-04-10 22:35:24 +09:00
08ea92f540 feat(bridge): Discord 봇 — 슬래시 커맨드 트리거 + 실시간 알림
v0.1.4 — 옵션 3 (outbound + inbound). DISCORD_TOKEN 이 설정되지 않으면
bridge 는 no-op 이라 기존 배포는 영향 없음.

## Outbound (rails → Discord)

- runner.ts: PipelineLifecycleEvent emitter 추가
    started / stage-done / stage-failed / completed / failed / escalated
- DiscordNotifier: 이벤트 → Discord 메시지 렌더링
    thread mode (슬래시 커맨드 트리거) vs channel mode (CLI/HTTP 트리거)
- EscalationNotifier 인터페이스도 구현 — escalate.ts 에서 사용자에게 알림
- runPipeline opts 에 onEvent + notifier 주입

## Inbound (Discord → rails)

- /rails start project:<name> requirements:<text> — 파이프라인 기동
    → defer reply → POST /pipelines/start-async → thread 생성 → 실시간 업데이트
- /rails status <id> — 상태 조회 (ephemeral)
- /rails abort <id> — 강제 종료 (ephemeral)

## Async start 엔드포인트

- POST /pipelines/start-async: pipelineId 즉시 리턴 후 background 에서
  runPipeline 실행. Discord 의 3초 ACK 타임아웃을 회피.
- runPipeline 에 opts.pipelineId 지원: async 엔드포인트가 미리 만든
  row 위에 파이프라인을 그대로 얹을 수 있게.

## 부트스트랩

- rails serve 가 DISCORD_TOKEN/GUILD_ID/NOTIFY_CHANNEL_ID 세 개가 모두
  있으면 DiscordBridge 를 자동 시작. 없으면 "skip" 로그 남기고 무시.
- discord.js ^14.26 의존성 추가.

## 문서/테스트

- .env.example: Discord 섹션 전면 재작성 (동작 설명 포함)
- tests/discord-notifier.test.ts (7 tests): fake DiscordClientWrapper 로
  라우팅/렌더링/바인딩 해제 로직 검증
- 총 111 → 118 테스트 통과
2026-04-10 22:13:26 +09:00
8c123cb03a feat(v0.1.3): LLM 어댑터 + in-process 모드 + docker-compose — 외부인 배포 친화
## LLM 공급자 어댑터 (B)

- sister-agent/src/llm/ 에 LlmAdapter 인터페이스 신설. 어댑터 5종:
    openclaw (기존), openai, anthropic, ollama, mock
- 선택은 LLM_PROVIDER 환경변수로. 기본값 mock.
- 모델명은 LLM_MODEL_{MANAGER,PRINCIPAL,LEAD,JUNIOR} 로 외부화.
  OpenClaw 내부 네이밍이 기본값이지만 env 로 얼마든지 갈아끼움.
- openai 어댑터는 OPENAI_BASE_URL 로 OpenRouter / Azure / 로컬 llama.cpp
  서버까지 커버.

## In-process 단일 프로세스 모드 (C)

- sister-agent/src/core.ts 로 executeInvocation 을 library-export
- rails 에 InProcessTransport 추가. 동적 import 로 sister-agent core 를
  로드해 같은 Node 프로세스에서 함수 호출로 실행.
- DirectRailsClient 로 HTTP 루프백 없이 DB 에 직접 쓰기 — 단일
  프로세스에서도 observability 동일.
- RailsConfig 에 transport: "in-process" 추가.
- buildTransports 가 RAILS_TRANSPORT 와 per-stage override 를 지원하도록
  확장. 레거시 RAILS_TRANSPORT_MODE + RAILS_AGENT_*_HOST 도 그대로 호환.

## Git push 외부화 + allowlist env 화

- spawn.ts 의 ENABLE_GIT_PUSH 를 "GITEA_TOKEN 있으면 auto on" 으로 변경.
  기존 Dev 토폴로지는 sister LXC 들에 이미 토큰이 있어서 행동 변화 없음.
- rails.service.ts 의 파일 프록시 allowlist 를 GIT_RAW_ALLOWED_HOSTS
  env 로 외부화. 기본값은 기존 Gitea 호스트 유지.

## Docker / 배포

- Dockerfile 추가. 단일 이미지로 rails + sister-agent 둘 다 빌드.
- docker-compose.yml (기본): mariadb + rails 한 컨테이너 = in-process.
  docker compose up 한 줄로 로컬 E2E 가능.
- docker-compose.full.yml: rails + 4 개 독립 sister 컨테이너 = 분산.

## 설정 샘플 + 문서

- .env.example 완전 재작성: 필수/LLM/토폴로지/Gitea/Discord 5 섹션
- rails.config.local.yaml: in-process 샘플
- rails.config.distributed.yaml: http 분산 샘플
- docs/LOCAL-SETUP.md: 30분 퀵스타트 (Docker + 네이티브 두 경로)
- README 에 "5분 퀵스타트" + 토폴로지 표 + LLM 공급자 목록 추가

## 테스트

- tests/transport-build.test.ts (6 테스트): 기본값 / in-process /
  per-stage override / http 엔드포인트 누락 / env 기반 wiring / 레거시
  env 호환
- 전체 테스트 105 → 111 통과
2026-04-10 21:51:53 +09:00
2cadb3e0df feat(sprint-007): 마이그레이션 도구 + 운영 문서 + v0.1.0 릴리즈 준비
Sprint 007 전체 구현 — 마지막 스프린트. 프로젝트 완성:

CLI:
- rails doctor — 환경 헬스체크 (Node/pnpm/git/env/프로젝트 파일)
- rails scaffold [dir] — 신규 프로젝트 .plans/ 구조 생성
- rails migrate from-hanarang-harness <path> — 레거시 아카이브 스캐너
  agents/scripts/workflows 분류 (portable vs deprecated)
  xhigh 참조 경고 등 위험 패턴 감지

Docs (신규 3종):
- docs/migration-guide.md — 레거시 하네스 → rails 단계별 이전 가이드
- docs/operations.md — PM2, health check, 트러블슈팅, DB 유지보수
- docs/discord-setup.md — 봇 생성, DiscordPoster 구현 예시,
  marker 프로토콜 완전 명세

README 대폭 업데이트:
- v0.1.0 상태 선언
- 빠른 시작 가이드
- CLI 13 서브커맨드 목록
- 문서 링크

Tests (4 신규, 105 total pass):
- 마이그레이션 스캐너 (agents/scripts/workflows 감지)
- node_modules/.git 제외
- 빈 아카이브 처리
- scaffold 디렉토리 구조 검증

검증: tsc --noEmit ✓ | vitest 105/105 ✓ | build ✓
       rails doctor → 정상 출력 ✓
       rails --help → 13 subcommands ✓

마감 상태:
- F1~F6 모든 실패 모드 코어에서 해결
- 7 스프린트 완료 (000: 계획, 001~006: 코어, 007: 릴리즈)
- 105 테스트, 19 문서 (.plans/) + 3 운영 문서 (docs/)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:53:54 +09:00
da85b92a6b feat(sprint-006): QA template runtime — 다랑이 체크리스트 실행
Sprint 006 전체 구현 — F3/F2 의 QA 측면 완성:

Schema:
- src/qa/schema.ts — QaTemplate, QaChecklistResult, QaArtifact Zod 스키마
- Zod + DodCheck 재사용

Templates (6종 YAML):
- qa-templates/scaffold-v1.yaml — README/LICENSE/gitignore/lockfile/strict
- qa-templates/feature-v1.yaml — tests/typecheck/no-console/no-any/tests-added
- qa-templates/bugfix-v1.yaml — regression-test/root-cause/no-scope-creep
- qa-templates/refactor-v1.yaml — tests/typecheck/no-behavior-change
- qa-templates/migration-v1.yaml — rollback/dry-run/data-loss/backup (critical)
- qa-templates/infra-v1.yaml — config-validated/secrets/rollback

Core:
- src/qa/template.ts — YAML loader, extends 체인 resolution, 프로젝트별 extras
- src/qa/verdict.ts — verdict 규칙 (critical/major → REQUEST_CHANGES,
  minor/recommendation 만 → APPROVE_WITH_NITS, 절대 REQUEST_CHANGES 안 됨)
- src/qa/runtime.ts — runQaTemplate: Contract check handlers 재사용
  manual 체크는 resolver 주입 가능 (없으면 SKIP 기본값)

CLI:
- rails qa run <type> [-s sprint-id] [-w workdir]
- rails qa show <artifact-id>
- rails qa templates  (목록)

Tests (19 신규, 101 total pass):
- computeVerdict 7가지 시나리오 (APPROVE / APPROVE_WITH_NITS / REQUEST_CHANGES / ABORT)
- minor-only 는 절대 REQUEST_CHANGES 안 된다는 rule 명시 테스트
- Template loader + listTemplates + extends merge
- runtime: file_exists pass/fail + manual resolver 주입 + artifact 저장
- scaffold-v1 실파일 로드 확인

검증: tsc --noEmit ✓ | vitest 101/101 ✓ | build ✓
       rails qa templates → 6개 전부 출력 ✓

사용자 메모리 feedback_qa_thorough.md 준수:
  - 체크 항목 수 제한 없음
  - 각 템플릿이 타입별로 세분화됨

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:46:58 +09:00
30e782a32d feat(sprint-005): Resilience — retry + backoff + escalation + xhigh 금지
Sprint 005 전체 구현 — F5 (중간 끊김/타임아웃 무한대기) 해결:

Resilience core:
- src/resilience/backoff.ts — exponential backoff + jitter (base 1s, cap 30s)
  + cancellable sleep
- src/resilience/classifier.ts — error → {retryable, reason}
  retryable: timeout, network, rate_limit, transient
  non-retryable: permission, config, invariant(ZodError)
  휴리스틱: ETIMEDOUT/ECONNREFUSED/429 등 메시지 패턴 감지
  + assertAllowedThinkingTier('xhigh' 금지)
- src/resilience/retry.ts — withRetry 래퍼
  non-retryable은 즉시 중단, max retries 초과시 classification 반환
- src/resilience/kill.ts — child process SIGTERM→SIGKILL grace 처리
  + 전역 cleanup handler (SIGINT/SIGTERM)
- src/resilience/escalate.ts — recordEscalation + EscalationNotifier
  + listEscalations / resolveEscalation

Prisma:
- Escalation 모델 추가 (pipelineId, reason, errorCategory, attempts, contextSnapshot)
- Pipeline.escalations 역참조

Runner 통합:
- transport.invoke 를 withRetry 로 래핑
- 실패시 classification 기반으로 자동 escalation 기록 (non-retryable만)
- RunOptions 에 maxRetries / notifier 추가

CLI:
- rails resume <pipeline-id> — escalated → idle 전이
- rails abort <pipeline-id> [-r reason] — 강제 종료

Tests (25 신규, 82 total pass):
- backoff: 기본값/지수/캡/jitter 범위/abort
- classifier: 6 error 클래스 + 3 휴리스틱 + xhigh 금지
- withRetry: 성공/재시도 후 성공/non-retryable 즉시 중단/max 초과/abort

검증: tsc --noEmit ✓ | vitest 82/82 ✓ | build ✓ | CLI ✓

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:40:50 +09:00
fcd2e56129 feat(sprint-004): 4-agent handoff engine — Transport 추상화 + runner + Discord marker
Sprint 004 핵심 구현 — F3 (QA 자동 라우팅 누락) + F4 (핸드오프 불안정) 해결:

Config (범용):
- src/config/schema.ts — Zod RailsConfig (pipeline/agents/discord)
- src/config/loader.ts — YAML + 환경변수 interpolation (${VAR})
- rails.config.example.yaml — 샘플 설정

Handoff:
- src/handoff/message.ts — HandoffMessage discriminated union (plan/implement/review/deploy)
- src/handoff/transport.ts — SisterTransport 인터페이스
- src/handoff/mock-transport.ts — 시나리오 override 가능한 mock
- src/handoff/discord-transport.ts — encodeInvokeMarker / decodeResultMarker
  (HTML 주석 + json 블록 — 자매는 LLM 우회 파서로 처리)
  DiscordPoster 인터페이스 주입으로 discord.js 와 독립 테스트 가능

Orchestrator:
- src/orchestrator/runner.ts — runPipeline E2E
  state → stage 매핑 → transport.invoke → HandoffMessage → FSM 이벤트
  타임아웃/에러는 ERROR 이벤트로 변환해 FSM 에 위임

CLI:
- rails run <project> [-r requirements] [-c config.yaml] [--mock]

Tests (16 신규, 57 total pass):
- HandoffMessage discriminated union 검증
- MockTransport 기본/오버라이드 시나리오
- Discord marker encode/decode round-trip
- DiscordTransport with fake poster
- Config loader YAML + 환경변수 interpolation

검증: tsc --noEmit ✓ | vitest 57/57 ✓ | build ✓ | rails run --help ✓

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:34:28 +09:00
58d6c262d5 feat(sprint-003): Sprint Contract + DoD Validator
Sprint 003 전체 구현 — F2 (DoD 강제 실패) + F6 (환경 검증 누락) 해결:

Core:
- src/contract/schema.ts — SprintContract Zod schema 전체
  (DodCheck, EnvPrereq, ValidationResult, CheckResult)
- src/contract/validator.ts — 3단계 검증 파이프라인
  1. 환경 prerequisites (실패 시 ABORT_PRECHECK)
  2. Runtime validation commands
  3. DoD checks → PASS/FAIL 집계
- src/contract/prerequisite.ts — 5가지 prereq kind
  (command_exists, port_open, env_var, file_exists, http_reachable)
- src/contract/generator.ts — 스프린트 md 파싱 → draft contract
- src/contract/store.ts — 파일 + Prisma contract 저장, freeze/loadContract

Check handlers (9종):
- file_exists, command_success, regex_in_file, regex_absent
- http_status (native fetch), process_listening (TCP probe)
- artifact_schema (Zod registry), db_query (Prisma raw)
- manual (Sprint 006 스텁)

CLI:
- rails contract generate <sprint-md> -s <sprint-id>
- rails contract freeze <id>
- rails contract validate <id>
- rails contract show <id>

Tests (19 신규, 41 total pass):
- 각 check kind 단위 테스트
- http_status: node http 서버 mock
- validator integration: PASS / FAIL / ABORT_PRECHECK
- generator + store round-trip

검증: tsc --noEmit ✓ | vitest 41/41 ✓ | build ✓ | CLI help ✓

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 15:21:35 +09:00
ae86d95155 feat(sprint-002): Skill 강제 진입 + Bypass 감지
Sprint 002 전체 구현 — F1 (자매 skill bypass) 해결:

Enforcement core:
- src/enforcement/skill-context.ts — 스킬 컨텍스트 생성/읽기/삭제/만료 체크
- src/enforcement/skill-trace.ts — 도구 사용 추적 (JSONL append)
- src/enforcement/guard.ts — pre-tool 가드 (context 유무 + 만료 + escape hatch)

Hooks (실제 로직):
- hooks/pre-tool.sh — Write/Edit/Bash 게이트 (context 없으면 exit 2)
- hooks/post-tool.sh — 도구 사용 trace 자동 기록

CLI:
- rails skill-context {create|show|clear}
- rails skill-trace {show|blocked}

Tests (13 신규, 22 total pass):
- skill-context: CRUD + 만료 감지
- skill-trace: append + read + blocked count
- guard: no-context 차단, valid 허용, expired 차단, RAILS_ENFORCE=off escape hatch

검증: tsc --noEmit ✓ | vitest 22/22 ✓ | build ✓ | rails --help ✓

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 13:56:58 +09:00
0af4bbc685 feat(sprint-001): XState FSM + Prisma + CLI 뼈대 — 결정론적 파이프라인 코어
Sprint 001 전체 구현:

Foundation:
- package.json (pnpm + Node 22 + TypeScript strict)
- tsconfig.json (strict + noUncheckedIndexedAccess)
- .env.example (DATABASE_URL, DISCORD_TOKEN, etc.)
- vitest.config.ts

Core:
- src/env.ts — Zod 환경변수 검증
- src/logger.ts — pino 구조화 로거
- src/orchestrator/events.ts — Zod discriminated union 이벤트 스키마
- src/orchestrator/context.ts — PipelineContext 타입 + 팩토리
- src/orchestrator/machine.ts — XState v5 결정론적 FSM
  States: idle → planning → implementing → reviewing → deploying → done
  + retrying (exponential backoff 준비) + escalated + aborted
- src/orchestrator/persist.ts — Prisma 기반 상태 영속화
- prisma/schema.prisma — MariaDB 스키마 (pipelines, state_transitions, actor_spawns, contracts)

CLI (citty):
- rails start <project> — 파이프라인 생성
- rails status [id] — 상태 조회 + 타임라인
- rails serve — 오케스트레이터 서버 (Sprint 004 에서 완성)

Tests (9/9 pass):
- happy path (idle → done)
- REQUEST_CHANGES 재작업 루프 + max review round escalation
- retryable/non-retryable 에러 분기
- RESUME / ABORT
- context 추적

검증: pnpm tsc --noEmit ✓ | pnpm vitest run 9/9 ✓ | pnpm build ✓ | rails --help ✓

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 13:46:13 +09:00