자기야 결정: 옵션 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 은 그대로 작동.
자기야 발견: 다랑이가 디스코드에 "리뷰 통과! 이랑이 받아" 라고 말했는데
실제로는 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 에 따라 "리뷰 통과! 이랑이 받아" / "결함 발견 — 나랑아
다시 봐줄래?" / "이건 접근 자체가 잘못된 것 같아. 중단." 중 하나로 답함.
이랑이는 "배포 검증 완료" / "배포 실패 — 자기야 봐줘" 둘 중 하나.
자기야 검증: 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 테스트 그대로 통과.
자기야 묶음 요청 (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 테스트 그대로 통과.
자기야 발견: 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 모두 정상.
자기야 발견: 다랑이/이랑이 (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 자체에 명시.
자기야 발견: 다랑이의 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 는 그대로 보존 — 거기선 분해가 맞으니까.
자기야 발견: 다랑이가 또 '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 미만.
자기야 요청: 안쪽 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).
자기야 발견: 다랑이 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.
자기야 발견: 다랑이가 코드에 문제가 있다고 답해도 파이프라인이 그대로 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"
발견: 첫 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 확인.
자기야 요청: 지금은 하랑이가 모든 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 테스트: 다음 단계에서 실제 디스코드 호출로 최종 검증
## 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 생성
이전 커밋 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 개 삭제), 나머지 그대로 통과.
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 테스트 통과
## 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 통과
- docs/GUIDE.md: 20 장 + 3 부록, 배경/원칙/아키텍처/데이터 모델/FSM/계층/
sister-agent/LLM/파일 추출/Git push/대시보드/E2E 시나리오/API/Contract/
보안/복원력/설치/디렉토리/로드맵/용어집 망라
- docs/GUIDE.pdf: weasyprint 로 렌더, 목차 + 페이지 헤더/풋터 포함
- README.md: 가이드 링크 추가, 상태 섹션 v0.1.2 까지 갱신, 아키텍처 그림
과 설계 원칙 정리
- 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
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 퍼미션)
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 파일시스템에 저장함.
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 (깔끔한 레이어)
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 를 찔러 파이프라인을 기동할 수 있음.
기존 { value, context } 수동 스냅샷은 XState v5 의 restoreSnapshot 이
status/children 등을 기대해서 런타임 에러 발생.
actor.getPersistedSnapshot() 를 써서 완전한 snapshot JSON 을 저장/복원.
getPipelineState 는 snapshot.context 에서 PipelineContext 를 추출.
Dev 서버에서 첫 실행 중 발견.
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>