64 Commits

Author SHA1 Message Date
c58bc311a6 feat(notify): junior 가 LLM 응답에 직접 discord 한 줄 멘트 emit (옵션 C)
자기야 결정: 옵션 C — junior 가 자기 work 끝에 ```discord-line``` 블록을
출력하고, sister-agent 가 그걸 추출해서 stage-end Discord notify 메시지로
사용. 추가 LLM 호출 0, 매번 다른 메시지, 자매 본인이 자기가 한 일을 직접
보고하는 느낌.

## 변경

### prompts.ts
- buildPrompt 의 footer 에 role==='junior' 분기 추가
- 새 helper discordLineFooter(ctx): 자매별 페르소나 + stage 별 예시 포함
- 출력 형식 강제: ```discord-line\n<한 줄>\n```
- 50 자 이내, 이모지 1-2 개, 실패시 ✗/⚠️/🛑 명시 지시

### discord-notify.ts
- StageMessageContext.customLine 필드 추가
- extractDiscordLines(text): 모든 ```discord-line``` 블록 추출 +
  본문에서 strip → { lines, cleaned }
- renderStageEnd 가 customLine 우선, 없으면 hardcoded 풀로 fallback

### spawn.ts
- aggregated 만들기 전에 extractDiscordLines() 호출
- cleaned 텍스트만 buildSuccessResult / priorStages 에 전달 (chat noise
  가 다음 stage 의 LLM context 로 새지 않게)
- 첫 번째 추출된 line 을 customLine 으로 renderStageEnd 에 전달

## 효과

이전: 다랑이가 항상 "🔍 리뷰 통과! 이랑이 받아" 같은 4 variant pool 에서
픽 → 식상 + 작업 내용 무관

이후: 다랑이가 직접 LLM 응답 끝에 emit
  - "🔍 체크리스트 다 ✓. addTodo/toggleTodo/deleteTodo 모두 동작. 이랑이 받아."
  - "⚠️ addTodo 가 빈 입력 처리 못 함. 나랑아 다시 봐줘."
LLM 이 자기가 본 코드의 실제 결함을 한 줄로 요약. 같은 stage 라도 매번
다른 메시지. fallback 은 그대로 작동.
2026-04-11 16:24:56 +09:00
ece52f9dd5 fix(notify): stage-end Discord 메시지가 실제 verdict 와 일치하게
자기야 발견: 다랑이가 디스코드에 "리뷰 통과! 이랑이 받아" 라고 말했는데
실제로는 REQUEST_CHANGES 가 발사돼서 나랑이로 다시 돌아감. 페르소나 메시지
와 실제 흐름이 어긋나는 버그.

원인:
spawn.ts 의 흐름이
  1. manager LLM 호출
  2. children LLM 호출
  3. **Discord stage-end notify 발사**  ← 이 시점엔 verdict 모름
  4. buildSuccessResult() → 여기서 verdict 결정

renderStageEnd 가 verdict 를 안 받아서 항상 "통과" 풀에서 픽함. forced
가드 (RAILS_FORCE_REVIEW_VERDICT) 검증 시에도, 정상 LLM 이 REQUEST_CHANGES
를 내놓을 때도 동일하게 잘못된 메시지가 나감.

수정:
- discord-notify.ts:
  - StageMessageContext 에 verdict 필드 추가
  - END_POOLS 를 verdict 별로 분리:
    * harang/ok, harang/abort
    * narang/ok, narang/error
    * darang/approve, darang/request_changes, darang/abort
    * erang/ok, erang/failed
  - endPoolKey() 함수가 (agentName, verdict) → 풀 키 결정
  - renderStageEnd 가 verdict 를 받아 적절한 풀에서 픽

- spawn.ts:
  - notifyDiscord stage-end 호출을 buildSuccessResult() **이후** 로 이동
  - result.verdict 를 renderStageEnd 에 전달
  - notify 결과도 명시 로깅 (notify.end + verdict)

이제 다랑이는 verdict 에 따라 "리뷰 통과! 이랑이 받아" / "결함 발견 — 나랑아
다시 봐줄래?" / "이건 접근 자체가 잘못된 것 같아. 중단." 중 하나로 답함.
이랑이는 "배포 검증 완료" / "배포 실패 — 자기야 봐줘" 둘 중 하나.
2026-04-11 16:12:55 +09:00
fb90abc8e3 fix(runner): review-loop 소진으로 escalated 도달 시 recordEscalation/notify 누락
자기야 검증: forced REQUEST_CHANGES 로 escalation 까지 갔는데 디스코드 알림이
안 떨어졌음. 로그 분석 결과 recordEscalation() 호출 자체가 0회.

원인: runner.ts 의 main loop 가 recordEscalation 을 ERROR 이벤트의
non-retryable 분기에서만 호출. 그런데 review-loop 소진 + replan 소진으로
FSM 이 자체적으로 escalated 로 전이한 경우는 ERROR 가 아니라 REQUEST_CHANGES
이벤트 경로라서 그 분기를 안 탐. 결과: escalation row 안 만들어지고 notifier
도 안 호출됨.

수정:
- escalationRecorded flag 추가, ERROR non-retryable 분기에서 true 로 세팅
- lastActiveStage 를 매 iteration 마다 트래킹
- main loop 종료 후 result.state === "escalated" && !escalationRecorded 면
  post-loop 에서 recordEscalation() + emit({type:"escalated"}) 호출
- stage 는 lastActiveStage (review-loop 소진의 경우 보통 "review"),
  attempts 는 context.replanCount, reason 은 context.lastError

검증 예정: 다음 forced 테스트에서 디스코드 채널에 escalation SOS 알림 떨어짐.
113 테스트 그대로 통과.
2026-04-11 15:35:08 +09:00
fc646894d9 feat: review DoD 체크리스트 + 자매 페르소나 톤 + 에스컬레이션 디스코드 알림
자기야 묶음 요청 (1+2+3):
  1) 에스컬레이션 발생 시 디스코드 SOS 알림
  2) review prompt 를 DoD 체크리스트로 강제
  3) 디스코드 stage 알림 톤 자매별 페르소나로 다양화

## #2: review DoD 체크리스트화

prompts.ts 의 reviewHintForRole 강화:
- manager: 자유 prose 금지, 정해진 출력 형식 강제
    ## DoD 체크리스트
    - [✓|✗] <항목> — <근거>
    ## 최종 결정
    APPROVE | REQUEST_CHANGES | ABORT
    ## 결정 근거
    <한 문단>
  - "✓" 가 아닌 "통과/OK" 같은 단어 금지 (parser 가 못 잡음)
  - minor/스타일 결함은 REQUEST_CHANGES 사유로 카운트 안 함
  - 사용자가 의도한 모순 (e.g. "함수 비워줘") 은 새 DoD 로 인식
- principal: [critical|major] <어디> — <무엇> — <왜> — <고침> 형식
- lead: ✓/✗ <기능명>: 동작 OK/실패 — <근거> 형식
- junior: 동일 verdict 시작 + 한 문단

## #3: 디스코드 페르소나 톤

discord-notify.ts:
- START_POOLS / END_POOLS 를 자매별 4 variant 풀로 확장
  - 하랑 차분/단정, 나랑 활달, 다랑 꼼꼼, 이랑 차분/믿음직
- 안정적 픽: pipeline title 해시로 결정 → 같은 task 같은 line, 다른 task
  로테이션 → robotic 느낌 제거
- 새 함수 renderEscalation(): SOS 메시지 포맷 (멘션 + 사유 + 대시보드 링크)

## #1: 에스컬레이션 디스코드 알림

흐름:
  rails orchestrator 가 escalated 시점 도달
  → recordEscalation() 의 EscalationNotifier 인터페이스 호출
  → DiscordEscalationNotifier 구현체가 sister-agent (하랑이) 의 /notify
    엔드포인트에 POST
  → sister-agent 가 local openclaw CLI 로 디스코드 채널에 메시지 발송
  → 자기야 멘션 + 사유 + pipelineId + 복구 명령

신설:
- src/handoff/escalation-notifier.ts: DiscordEscalationNotifier 클래스
- sister-agent/src/server.ts: POST /notify 엔드포인트 (channelId + message)
- src/server/http.ts ServerOpts 에 escalationConfig 옵션 추가. 시작
  엔드포인트가 notifyChannelId 를 받으면 per-pipeline notifier 인스턴스
  생성 → runPipeline 의 notifier opt 로 주입
- src/cli/serve.ts: RAILS_NOTIFY_SISTER_URL + RAILS_NOTIFY_USER_ID 환경
  변수 읽어 startHttpServer 에 escalationConfig 전달

운영 시 활성화: Dev VM 의 .env 에 다음 두 줄 추가
  RAILS_NOTIFY_SISTER_URL=http://10.10.10.112:18801
  RAILS_NOTIFY_USER_ID=452664876691881984

113 테스트 그대로 통과.
2026-04-11 05:11:53 +09:00
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
6c6a0a50dc fix(prompt): plan/implement manager hint 도 가짜 팀 narration 금지
자기야 발견: 다랑이/이랑이 (review/deploy) 의 manager 가 가짜 팀 분배를
출력하던 건 이전 커밋에서 고쳤는데, 하랑이/나랑이 (plan/implement) 의
manager 도 똑같이 "수석 1명은... 선임 2명은... 신입 2명은..." 를 풀어서
narration 하고 있었음. 분해는 sister-agent 의 planner.ts 가 complexity
score 로 결정론적으로 결정하지 LLM 의 prose 가 결정하지 않는데, manager
LLM 이 prose 로 가짜 team plan 을 출력해서:

  - 토큰 낭비 (실제로 spawn 에 영향 0)
  - 사용자 혼란 ("진짜 그렇게 팀이 구성됐나?" 오해)
  - 다음 stage 의 priorStages 노이즈 증가

수정 — plan/implement manager hint 를 stage 별로 짧게 재작성:

plan/manager:
  1) MVP 범위 한 줄
  2) 명시적 비범위 한 줄
  3) 통과 기준 한 줄
  명령형/단정형 강제. "수석/선임/신입" 단어 사용 금지.

implement/manager:
  1) 기술 스택 / 런타임 한 줄
  2) 파일 구조 1~2 줄
  3) 핵심 구현 결정 한 줄
  같은 제약.

추가로 principal/lead/junior 의 plan/implement hint 에도 "팀 narration 금지"
한 줄을 박아 일관성 유지. junior 의 implement code 출력에도 코드 블록 안에
가짜 팀 prose 를 섞지 말라는 가드 추가.

실제 분해는 코드 (planner.ts) 가 한다는 사실을 prompt 자체에 명시.
2026-04-11 02:54:02 +09:00
6c43cccca5 fix(prompt): review/deploy stage 의 role hint 를 stage-aware 하게 분리
자기야 발견: 다랑이의 review 답변이 "수석 1명은 ...전체 원문 다시 제출,
선임 1명은 CRUD 재정리, 신입 A는 ..., 신입 B는 ..., 나는 마지막에 승인"
형태로 나옴 — 즉 verdict 가 아니라 가상 팀 분배 plan 을 출력.

원인: prompts.ts 의 roleOutputHint 가 stage 와 무관하게 동일했음.
- manager hint: "이 작업을 어떻게 분해할지, 어떤 팀(수석/선임/신입) 을 배치할지"
- principal hint: "구체적으로 어떤 리스크... 어떻게 분해되어야 하는지"
- lead hint: "신입에게 어떻게 나눠줄지, 검증 포인트"

이게 plan/implement 단계에서는 맞는데 review/deploy 에서는 작동 모델이
완전히 다름. 다랑이 manager 가 review 단계에서 "팀 분배" 지시를 받으니
review verdict 대신 가짜 팀 plan 을 내놓고 있었음. 이랑이 deploy 도 동일.

수정: stage-specific role hint 분리.

reviewHintForRole(role):
- manager: 절대 분해/분배 금지. 첫 줄 APPROVE/REQUEST_CHANGES/ABORT, 다음
  bullet 1~3개 근거. "내가 마지막에 본다" 같은 미래 약속 금지.
- principal: critical 결함 1~3개. 형식: [심각도] 어디 — 무엇 — 왜 — 어떻게 고쳐야
- lead: 사용자 요구사항의 각 핵심 기능별 통과 여부 한 줄. ✓ <기능>: ... 또는 ✗
- junior: 첫 줄 verdict, 한 문단 이유. 코드 다시 짜지 마.

deployHintForRole(role):
- manager: 종합 한 문단 + 마지막 줄 DEPLOY_DONE/DEPLOY_FAILED.
- principal: 배포 환경 리스크 (브라우저/CSP/CDN/의존성) 1~3개 또는 "리스크 없음".
- lead: "□ <확인 절차>" 체크리스트.
- junior: 기존 그대로.

plan/implement 의 hint 는 그대로 보존 — 거기선 분해가 맞으니까.
2026-04-11 02:29:36 +09:00
fe9ec0d9db fix(prompt): priorStages text 가 prompt 에 박힐 때 2500자로 잘리던 마지막 잘림 지점
자기야 발견: 다랑이가 또 'editInput = document.createElement(...)' 에서 잘렸
다고 함. narang junior 의 LLM 출력 원본 (junior-01-*.md) 은 5KB 로 끝까지
정상이고, spawn.ts 의 aggregated/summary 도 64KB 로 OK 였는데, 실제 다랑이
LLM 에 박히는 prompt 텍스트 단계에서 또 잘리고 있었음.

원인: prompts.ts 의 buildPrompt 가 priorStages text 를 prompt 에 박을 때
slice(0, 2500) 로 자르고 있었음. 5KB 짜리 HTML 의 절반 가까이에서 정확히
잘림. prevStageOutput slice(0, 2000) 도 같은 패턴.

수정:
- priorStages text slice: 2500 → 64_000 (upstream 과 일치)
- prevStageOutput slice: 2000 → 32_000
- prompt 헤더에 "잘리지 않은 원본" 명시 추가 — LLM 이 "이게 잘렸나?" 의심
  하지 않게 (다랑이가 잘렸다고 오해할 가능성도 같이 줄임)

LLM context window (200k tokens+) 안에서 안전. priorStages 가 4 stage
누적되어도 256KB 정도면 50k tokens 미만.
2026-04-11 02:21:52 +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
ae2d4b1d3e fix(truncation): priorStages slice 한도가 너무 짧아 review 가 잘린 코드 받던 버그
자기야 발견: 다랑이 review 결과에 "코드가 끊긴다" 가 반복 등장. 다랑이의
실제 review 메시지를 확인해 보니 narang 이 만든 todo-debug-mode.html 의
<script> 가 `const form = document.getElementById(` 에서 잘려서 반복 round
마다 같은 잘림 지점을 지적했음.

원인 추적:
- narang junior 의 LLM 출력 원본 (junior-01-*.md) 은 깔끔하게 </html> 로
  종료. LLM 자체는 멀쩡.
- spawn.ts buildSuccessResult 의 summary slice(0, 2000) 가 1차로 자름
- 그 위 aggregated slice(0, 6000) 가 0차로 자름
- runner extractStageText 의 review issues slice(0, 2000) 도 추가 한도
- LLM-emit reason 도 spawn.ts parseReviewVerdict 에서 1000/800 자로 잘림

todo HTML 한 파일이 5KB 정도 되니 6000 자 한도에서 3분의 1 잘려나감 →
darang 은 결과적으로 절반짜리 코드를 받음 → 영원히 REQUEST_CHANGES.

수정 (모두 LLM context 200k+ 안에서 안전한 한도):
- spawn.ts aggregated: 6_000 → 64_000
- spawn.ts buildSuccessResult.summary: 2_000 → 64_000
- spawn.ts parseReviewVerdict reason: 1_000 → 16_000
- spawn.ts parseDeployVerdict reason: 800/1000 → 16_000
- runner.ts extractStageText review issues: 2_000 → 32_000

검증: 다음 실행에서 darang 이 동일한 잘림 지점을 지적하지 않으면 OK.
2026-04-11 01:16:49 +09:00
6627ad709f fix(verdict): review/deploy stage 가 LLM 출력을 무시하고 verdict 하드코딩하던 버그
자기야 발견: 다랑이가 코드에 문제가 있다고 답해도 파이프라인이 그대로 deploy
로 넘어가는 현상. 원인은 sister-agent 의 buildSuccessResult 가 review/deploy
stage 의 verdict 를 LLM 출력과 무관하게 "APPROVE" / "DEPLOY_DONE" 로 하드
코딩하고 있었음. FSM 의 review-loop 와 escalation 경로는 정상이었지만
sister 가 한 번도 REQUEST_CHANGES 를 emit 하지 않아 loop 가 죽어 있었음.

수정:
- parseReviewVerdict(text): LLM 출력에서 ABORT / REQUEST_CHANGES / APPROVE
  키워드 탐색. 마커가 없으면 conservative 하게 REQUEST_CHANGES 로 분류해
  silent approval 방지.
- parseDeployVerdict(text): DEPLOY_FAILED / DEPLOY_DONE 마지막 마커 추출.
  마커 없으면 DEPLOY_FAILED 로 bias.
- buildSuccessResult: review/deploy 케이스가 위 파서 결과를 사용. issues
  배열에 LLM reason 을 major severity 로 첨부 → runner 가 같은 텍스트를
  다음 implement round 에 priorStages 로 전달함.

검증: pipeline 01KNW1MJWGMZHXDE7178SP8FZB
  planning → implementing → reviewing
    → REQUEST_CHANGES (round 1) → implementing
    → reviewing → REQUEST_CHANGES (round 2) → implementing
    → reviewing → REQUEST_CHANGES (round 3) → implementing
    → reviewing → REQUEST_CHANGES (max exceeded) → escalated
  context: reviewRound=3, lastError="Max review rounds exceeded"
2026-04-11 01:07:43 +09:00
13b2c00048 fix(notify): openclaw CLI cold-start 7-10s 대비해 timeout 25s + 디버그 로그
발견: 첫 E2E 테스트에서 4 자매 중 하랑이만 notify 성공, 나머지 3 자매는
"openclaw timeout" 으로 실패. 직접 측정해 보니 narang LXC 의
`openclaw message send` 가 7.258s (gateway connect + auth cold start).
기존 8s timeout 이 빡빡해서 가끔 들어오고 가끔 죽었음.

수정:
- discord-notify.ts: timeoutMs 기본값 8000 → 25000ms. fire-and-forget
  호출이라 메인 파이프라인 latency 영향 없음.
- spawn.ts: notify.start 호출 결과를 명시적 로그로 남김 (성공/실패/error
  각각 가시화). 다음 디버깅을 빠르게.
- server.ts: invoke.start 로그에 notifyChannelId 표시.

검증: 파이프라인 01KNW0ADA5CBFBVGCRV7VN1YTF 에서 4 자매 모두 notify.start
ok:true 확인.
2026-04-11 00:37:36 +09:00
809d5b94c4 feat(notify): 각 자매가 본인 봇 identity 로 디스코드 stage 업데이트 직접 포스트
자기야 요청: 지금은 하랑이가 모든 stage 를 대신 말해서 어색함. 각 자매가
자기 작업할 때 본인 목소리로 짧게 디스코드에 보고해야 함.

핵심 발견: OpenClaw 가 sessions.json (`agent:main:discord:channel:<id>`) 의
키에 활성 채널 ID 를 박아 놓음. updatedAt 으로 정렬하면 가장 최근에 자기야
가 말한 채널을 자동 추출 가능. bash tool 환경변수에는 채널 ID 가 안 들어
있어서 이 우회가 필요했음.

## rails 쪽 (notifyChannelId 전파)

- handoff/message.ts: InvokeRequest schema 에 notifyChannelId optional 추가
- src/orchestrator/runner.ts: RunOptions 에 notifyChannelId 받아 InvokeRequest
  에 그대로 propagate
- src/server/http.ts: StartRequest schema + /pipelines/start 와
  /pipelines/start-async 둘 다 notifyChannelId 받아 runPipeline 에 전달
- sister-agent/src/types.ts: InvokeRequest 에도 같은 필드 추가

## sister-agent 쪽 (각자 본인 봇으로 포스트)

- sister-agent/src/discord-notify.ts: 신설. local openclaw CLI 를 spawn 으로
  호출해 본인 봇 identity 로 메시지 발송 (best-effort, 실패해도 파이프라인
  안 막음). 자매별 페르소나 메시지 템플릿 (renderStageStart/End) 포함
- sister-agent/src/spawn.ts: executeInvocation 시작과 manager 완료 시점에
  notifyDiscord 호출. notifyChannelId 가 없으면 no-op

## skill wrapper

- ~/.openclaw/skills/hanarang-rails/scripts/rails-start-and-watch.sh:
  sessions.json 에서 가장 최근 discord 채널 ID 자동 추출 →
  /pipelines/start-async 본문에 notifyChannelId 포함. 중간 stage echo 제거 —
  이제 각 자매가 본인 봇으로 직접 포스트하므로 하랑이는 시작 banner 와 최종
  보고만 출력
- SKILL.md "스크립트 출력 처리" 섹션 새 흐름에 맞게 업데이트

## 검증

- 사전 검증: nara LXC 에서 `openclaw message send --channel discord --target
  channel:<id>` 호출이 정상 작동 (Message ID 받아옴), 자기야가 디스코드에서
  나랑이 봇 메시지 확인
- E2E 테스트: 다음 단계에서 실제 디스코드 호출로 최종 검증
2026-04-11 00:02:29 +09:00
4a77f43a32 fix(sister-agent): trivial tier ghost pipeline + openclaw model override 거부
## Bug 1: trivial tier 가 산출물 없는 유령 파이프라인 생성

planner.ts 의 trivial 케이스가 strategy="direct" + spawn=[] 였음. 그런데
spawn.ts 의 maybeExtractFiles() 는 role !== "junior" 면 파일을 저장하지
않고, manager 의 프롬프트는 "본인이 직접 코드를 짜지 않는다" 로 박혀 있어,
trivial 태스크는 아무도 코드를 안 쓰는 상태로 done 처리됨.

수정: trivial tier 도 single-junior 로 강제. junior 1 명이 무조건 코드를
생성하게 함. 이전 "direct" 전략은 의도적으로 사용 안 함.

재현: "간단한 todo 앱 만들어" → score 10 → tier=trivial → ghost pipeline
검증: smoke-todo-v3 파이프라인이 implement/files/frontend/sprints/SPRINT-AUTO/
      index.html (115 줄) 을 실제로 생성함

## Bug 2: openclaw 어댑터가 모델 override 로 OpenClaw 거부됨

LLM 어댑터 리팩터 시 spawn.ts 에서 callLlm 에 model: ROLES[role].primaryModel
을 명시하게 했는데, 그 모델 이름 (gpt-5.4 / glm-5-turbo 등) 이 OpenClaw
agent="main" 의 allowlist 에 없어서 "Model override not allowed" 로 거부됨.

수정: openclaw 어댑터는 --model 플래그를 더 이상 넘기지 않음. OpenClaw 의
자체 라우팅에 모델 선택을 위임. 다른 어댑터 (openai/anthropic/ollama) 는
그대로 req.model 을 honor 함.

검증: smoke-todo-v3 의 narang junior 가 LLM 호출 성공, 실제 HTML 생성
2026-04-10 23:41:12 +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
c2e89ec5da docs: 완전 가이드 추가 — 처음 보는 사람용 전 구간 해설
- docs/GUIDE.md: 20 장 + 3 부록, 배경/원칙/아키텍처/데이터 모델/FSM/계층/
  sister-agent/LLM/파일 추출/Git push/대시보드/E2E 시나리오/API/Contract/
  보안/복원력/설치/디렉토리/로드맵/용어집 망라
- docs/GUIDE.pdf: weasyprint 로 렌더, 목차 + 페이지 헤더/풋터 포함
- README.md: 가이드 링크 추가, 상태 섹션 v0.1.2 까지 갱신, 아키텍처 그림
  과 설계 원칙 정리
2026-04-10 21:21:02 +09:00
be8715a7f0 fix(pipeline): track produced file paths accurately for deploy URL
- RunContext.producedFiles[] accumulates repo-relative paths of extracted
  code files as juniors write them
- implement selfTestReport now includes producedFiles[]
- runner extractStageText forwards producedFiles= in priorStages
- derivePreviewUrl uses producedFiles list first (exact match), then falls
  back to rawUrlBase/index.html heuristic
- Fixes bug where deployUrl pointed to files/index.html but actual repo
  path was implement/files/frontend/sprints/SPRINT-AUTO/index.html
2026-04-10 20:48:37 +09:00
17b2f2b232 feat(pipeline): E+F+G — file artifacts, git push, deploy URL
E - Dashboard drawer 산출물 섹션
- completed event payload 의 file / extractedFiles / deployUrl / repoUrl 추출
- 파일 배지 + 언어별 색상 (html/js/ts/css/json/md/url)
- URL 은 클릭 가능한 링크

F - Gitea auto-commit + push (narang/implement stage)
- sister-agent/src/git-ops.ts — ensureGiteaRepo + commitAndPush
  Gitea API POST /api/v1/orgs/{org}/repos 로 repo 자동 생성
  git init + add + commit + push (HTTPS + token in URL)
  repo 이름: rails-<last-10-of-pipeline-id>
  사용자: rails-agent <rails@hanarang.local>
- spawn.ts: implement stage 종료 시점에 commitAndPush 호출
  gitResult.repoUrl/rawUrlBase/commit/filesCount 를 manager completed event 에 포함
- buildSuccessResult: implement HandoffMessage.selfTestReport 에 git 메타 포함

G - Deploy stage preview URL
- runner.ts extractStageText: implement 단계에서 rawUrlBase/repoUrl/filesCount 파싱해서 priorStages text 에 포함
- spawn.ts derivePreviewUrl(): priorStages 의 implement 텍스트에서 rawUrlBase 추출 + HTML 파일 경로 힌트 조합
- deploy manager completed event 에 deployUrl 포함
- 결국 dashboard drawer 에 클릭 가능한 preview URL 이 뜸

Env 설정:
- sister-agent/.env 에 GITEA_TOKEN, GITEA_BASE_URL, GITEA_ORG, GIT_USER_*
- start-sister-agent.sh 가 .env 를 source
- 4자매 LXC 전부 배포 (700 퍼미션)
2026-04-10 20:35:04 +09:00
1c25fb3b5b feat(pipeline): C stage-chaining + D code block extraction
C - Stage 간 결과 전달:
- InvokeRequest schema + sister-agent types: priorStages[] field
- runner.ts: accumulates stage outputs as pipeline progresses
  extractStageText() pulls summary from each HandoffMessage
  each subsequent invoke gets priorStages[{stage, text}, ...]
- prompts.ts: priorStages rendered as "# 앞 단계(들)의 결과물" section
  manager/principal/lead/junior 모두 볼 수 있음
- spawn.ts: forwards ctx.req.priorStages into doWork() at every level

이제 하랑이 plan 결과가 나랑이 implement 의 프롬프트에 포함되고,
나랑이 결과가 다랑이 review 에, 다랑이 결과가 이랑이 deploy 에 전달됨.
실제 파이프라인으로 이어짐.

D - 코드 블록 추출 + 파일 저장:
- sister-agent/src/code-extractor.ts — 마크다운 코드 블록 파서
  form 지원: ```lang / ```lang:path / ```lang path=... / ```src/file.ext
  path sanitize (.., leading /, absolute 차단)
  LANG_TO_EXT 25+ 매핑
- spawn.ts: maybeExtractFiles() — implement stage junior 에만 적용
  {workspaceDir}/files/{relpath} 로 저장
  sub_task_events.completed.extractedFiles 에 path 목록 포함
- prompts.ts: implement junior 프롬프트에 파일 경로 명시 형식 강제
  "```html:src/index.html" 예시 포함

이제 하랑이가 계획한 내용 기반으로 나랑이가 실제로 코드 파일을 LXC 파일시스템에 저장함.
2026-04-10 20:16:47 +09:00
6a8599e0b3 feat(sister-agent): parallelize + write output files
Parallelization:
- 3중 nested for loop → Promise.all 재귀 tree walker
- 같은 레벨 sub-task 들 전부 동시 실행
- complex tier 15 LLM calls 가 직렬 → tree depth 기반 wall-clock
  manager(1) + max(principals) + max(leads per principal) + max(juniors per lead)
  ≈ 4 calls worth instead of 15

File output:
- SISTER_WORKSPACE_DIR (기본 ~/rails-projects)
- 각 노드의 LLM 출력을 {pipeline}/{stage}/{role}-{idx}-{id}.md 로 저장
- Front matter 에 pipeline/stage/agent/role/subTaskId/createdAt
- file 경로를 sub_task_events 의 completed payload 에 포함

Refactor:
- 402 → 382 lines
- 3중 loop → 재귀 runSpawnNode() 1개 함수
- executeInvocation → runPlanChildren → runSpawnNode (깔끔한 레이어)
2026-04-10 19:56:24 +09:00
a7d5a2bdec fix(config): default agent timeoutMs 30s→600s for LLM workloads 2026-04-10 18:49:03 +09:00
9f238f570d merge: real LLM execution for sister-agent 2026-04-10 18:46:37 +09:00
98540af98c fix(runner): default invoke timeout 30s→600s, retries 3→1 for LLM workloads 2026-04-10 18:46:30 +09:00
579137e4bf feat(rails): GET /api/transitions + /api/escalations for SIEM dashboard 2026-04-10 18:22:58 +09:00
9aeef223c6 feat(rails): GET /api/sub-tasks/:id — node detail with parents/events 2026-04-10 17:54:55 +09:00
e2d71ca47d fix(server): remove deprecated mock-only check in /pipelines/start
transport builder 가 이미 env + config 기반으로 mock/http 결정하므로
서버 레이어의 mock==false 거부는 불필요. 제거.
2026-04-10 17:17:42 +09:00
3e354d21c1 merge: Stage 1+2 — hierarchical sub-agent team 2026-04-10 17:11:23 +09:00
a88323716d feat(stage-1-2): hierarchical sub-agent team — sister-agent + SubTask tracking
Stage 1 + 2 통합 구현 — 사용자 피드백 반영:
  manager / principal / lead / junior 계층 구조 추가.
  부장이 복잡도를 판단해서 팀을 동적으로 꾸린다.

Design:
- .plans/design/hierarchy.md — 전체 설계 문서 (점수/tier/plan/escalation)

Prisma schema:
- SubTask (계층 트리 + 역할/모델/state/complexity)
- SubTaskEvent (JSONL 스타일 이벤트 로그)

rails (core):
- src/hierarchy/roles.ts — 4역할 기본 config + 모델 매핑
  manager/principal: gpt-5.4 / glm-5.1
  lead: gpt-codex-5.3 / glm-5
  junior: glm-5-turbo / gpt-5
- src/hierarchy/complexity.ts — 규칙 기반 스코어러 (7 factors, 0-100)
- src/hierarchy/planner.ts — tier → DecompositionPlan (trivial/simple/moderate/complex/massive)
  + concurrency budget 강제
- src/hierarchy/store.ts — Prisma CRUD + tree builder
- src/handoff/http-transport.ts — 실제 HTTP transport (MockTransport 대체)
- src/handoff/build.ts — config + env 기반 transport 빌더
  (env RAILS_TRANSPORT_MODE + RAILS_AGENT_{STAGE}_HOST 오버라이드)
- src/server/http.ts — sub-task 엔드포인트 4개 추가
    POST /api/sub-tasks
    PATCH /api/sub-tasks/:id
    POST /api/sub-tasks/:id/events
    GET /api/pipelines/:id/sub-tasks (tree view)

sister-agent (new sub-project):
- sister-agent/ — 각 LXC 에 배포될 Node.js 데몬
- src/types.ts, roles.ts, complexity.ts, planner.ts
- src/spawn.ts — executeInvocation: 복잡도 점수 → plan → spawn 트리
  simulation mode (현재는 work 를 delay 로 시뮬레이트, LLM 연동은 후속)
- src/rails-client.ts — sub-task / event push
- src/server.ts — POST /invoke 엔드포인트 (port 18801)

LXC 리소스 실측 기반 기본 concurrency:
  104/106/107: 8 concurrent sub-agents
  105 (narang, build 중): 6

검증: tsc --noEmit ✓ | vitest 105/105 ✓ | rails build ✓ | sister-agent build ✓

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-10 17:11:16 +09:00
c8d6ceb337 merge: HTTP API for orchestration 2026-04-10 16:10:44 +09:00
1f518c0c54 feat(server): HTTP API for orchestration (/pipelines/*, /health)
rails serve 가 이제 실제 HTTP 서버를 띄움:
- GET  /health
- GET  /pipelines?limit=N
- POST /pipelines        — 파이프라인 생성만
- POST /pipelines/start  — 생성 + E2E 실행 (현재 mock 만)
- GET  /pipelines/:id    — 상태 + 타임라인
- POST /pipelines/:id/abort — 강제 종료

node built-in http 만 사용 (추가 의존성 없음).
Zod 로 요청 바디 검증. Graceful shutdown (SIGINT/SIGTERM).

하랑이(오케스트레이터) 가 이 API 를 찔러 파이프라인을 기동할 수 있음.
2026-04-10 16:10:39 +09:00
3a226ada95 merge: hotfix — FSM snapshot restore (XState v5) 2026-04-10 16:04:48 +09:00
7a8f2c1af0 fix(persist): use XState v5 getPersistedSnapshot for round-trip
기존 { value, context } 수동 스냅샷은 XState v5 의 restoreSnapshot 이
status/children 등을 기대해서 런타임 에러 발생.

actor.getPersistedSnapshot() 를 써서 완전한 snapshot JSON 을 저장/복원.
getPipelineState 는 snapshot.context 에서 PipelineContext 를 추출.

Dev 서버에서 첫 실행 중 발견.
2026-04-10 16:04:41 +09:00
f6c1768c60 docs: Sprint 007 완료 — v0.1.0 전 스프린트 완료 v0.1.0 2026-04-10 15:54:44 +09:00
8786efc81c merge: Sprint 007 — Migration + docs + v0.1.0 (#7) 2026-04-10 15:54:14 +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
1d37fee3ad docs: Sprint 006 완료 마크 + 마지막 스프린트 007로 갱신 2026-04-10 15:47:35 +09:00
256f334706 merge: Sprint 006 — QA template runtime (#6) 2026-04-10 15:47:15 +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
83fb627d2f docs: Sprint 005 완료 마크 + 현재 스프린트 006으로 갱신 2026-04-10 15:41:23 +09:00
39d5f26c40 merge: Sprint 005 — Resilience (#5) 2026-04-10 15:41:03 +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
b630779909 docs: Sprint 004 완료 마크 + 현재 스프린트 005로 갱신 2026-04-10 15:35:00 +09:00
38c579f177 merge: Sprint 004 — Handoff engine + Transport abstraction (#4) 2026-04-10 15:34:41 +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
eb63428174 docs: Sprint 003 완료 마크 + 현재 스프린트 004로 갱신 2026-04-10 15:22:09 +09:00
8f0691aafb merge: Sprint 003 — Sprint Contract + DoD Validator (#3) 2026-04-10 15:21:49 +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
205084cc60 docs: Sprint 002 완료 마크 + 현재 스프린트 003으로 갱신 2026-04-10 13:57:43 +09:00