Skip to content

Feat: 질문 중계 — 에이전트가 다른 역할에게 묻고 답을 받아 이어 간다 (상담 실행·프로젝트 스위치·내 질문 API, 마이그레이션 018, CLI 0.1.5) - #32

Merged
oyeonghun merged 14 commits into
devfrom
exp/question-relay
Oct 8, 2026

Conversation

@oyeonghun

Copy link
Copy Markdown
Collaborator

실험 브랜치(exp/question-relay)를 운영에 올린다. **프로젝트 스위치(기본 켜짐)**로 프로젝트마다 끌 수 있다.

무엇

  • 에이전트가 다른 역할이 정할 결정을 AskUserQuestion으로 묻는다(권한 도구 permission_prompt — AskUserQuestion만 처리, 나머지 거부).
  • 답할 역할의 노트북에서 상담 실행(읽기 도구만)이 코드를 읽어 초안. 전부 정해져 있으면 바로 답(agent_answered), 아니면 사람(대상 역할 담당·대표)이 답한다.
  • 실행 안에서 3분 기다리고, 넘으면 태스크를 내려놓는다(BLOCKED·QUESTION) → 답이 오면 READY로 재개.
  • 만료 3일 → ESCALATED(읽을 때 + 주기 정리).
  • projects.question_relay(기본 켜짐, 시작 전에만 PATCH /projects/:id/settings). 끄면 질문 409, Executor는 권한 도구를 붙이지 않고 "가정하고 GOTCHA".
  • GET /me/questions(알림 배지용), 룸 피드에 질문 줄(묻는 쪽·답할 쪽 룸 모두).
  • 다른 역할 소관 계약은 DECIDED가 아니라 GOTCHA "가정함"(실험 E6).
  • 질문 라우터 인터페이스와 오프라인 평가 장치(scripts/eval-question-router.ts).

근거

헤드리스 실험 E1~E6, 진짜 Claude E2E(중계·상담·두 Executor) — 상세는 CLAUDE.md "질문 중계".

확인

전체 678 통과(E2E 3개는 RUN_CLAUDE_E2E일 때만). 마이그레이션 018(agent_questions, projects.question_relay). CLI 0.1.5.

🤖 Generated with Claude Code

oyeonghun and others added 14 commits October 6, 2026 09:11
실행 중인 에이전트가 다른 역할 소관 결정을 AskUserQuestion으로 물으면 브릿지의 권한 도구가 서버에 올리고,
대상 역할 담당(또는 대표)이 답할 때까지 기다렸다가 답을 돌려준다. 근거 실험(E1~E6)은 scratchpad 기록 참고.

- 017 agent_questions: 질문(AskUserQuestion 형식 그대로)·대상 역할·상태(pending/answered/expired)·답
- API: POST /tasks/:id/questions·GET /tasks/:id/questions/:qid(에이전트), GET /projects/:id/questions·POST /questions/:id/answer(사람)
  - 대상 역할은 서버가 정한다(지금은 반대 역할 — routed_by), 답의 키는 질문 문장 그대로(아니면 422), 만료는 읽을 때 판정
  - 이벤트 QUESTION_ASKED·ANSWERED·EXPIRED, 실시간 토픽 questions
- 브릿지: 권한 도구 permission_prompt(AskUserQuestion만 처리, 나머지·고장은 거부), 대기 중 MCP 진행 알림
- Executor: --permission-prompt-tool, 프롬프트에 "다른 역할 결정은 AskUserQuestion으로 물어라"
- QUESTION_TIMEOUT_MS(기본 10분)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- domain/question/router.ts: QuestionRouter 인터페이스, 기본 규칙 라우터(반대 역할), LLM 계열 공통 프롬프트·응답 해석
  (DB·설정 의존 없음 — 오프라인 평가 장치가 그대로 쓴다)
- router-registry.ts: setQuestionRouter로 교체, 실패·30초 초과면 규칙 라우터로 대체하고 fallback을 기록
- SELF(묻는 쪽 자기 소관)면 넘기지 않고 self_owned — 브릿지가 "스스로 정하고 DECIDED로 남겨라"로 돌려준다
- agent_questions.routing(confidence·reason·latencyMs·fallback), QUESTION_ASKED에도 같은 값 — 라우터별 비교 지표
- 라우터는 트랜잭션 밖에서 부른다(LLM이면 수 초)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- scripts/eval-question-router.ts: 정확도·SELF 탐지·잘못 막음·지연·비용, 결과는 scripts/data/router-runs/(커밋 안 함)
- scripts/lib/question-routers.ts: claudeCliRouter(이 PC 구독, 도구 없음, 빈 폴더), httpRouter(Jev 등 외부 모델 자리)
- scripts/data/question-routing.json: 실험에서 나온 실제 질문 11 + 자기 소관 사례 9, 애매한 문항은 정답 둘 인정
- 첫 결과(v1 데이터셋): 규칙 70%·SELF 0%, Claude Opus·Sonnet 각 2회 100%·SELF 100%(문항당 약 6초)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 묶음, 형식 문서·빈 틀

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- 질문 상태 agent_answered(상담 실행이 코드·명세에서 찾은 답, 사람 미확인) · 초안(draft: answers·decided·basis)
- 사람의 답: 대기 중이면 human, 에이전트 답과 같으면 agent_confirmed, 다르면 human_override(고치는 태스크는 미구현 — notice)
- POST /tasks/:id/questions/:qid/detach: 태스크 BLOCKED(QUESTION)·담당 비움 → 답이 다 오면 READY, 기간(기본 3일) 지나면 ESCALATED
- GET /agents/me/questions(상담할 질문·읽을 레포), POST /questions/:id/draft(대상 역할 에이전트만, 한 번)
- 브리핑 answeredQuestions — 재개한 실행이 받은 답을 읽는다
- 이벤트 QUESTION_DRAFTED·TASK_BLOCKED_ON_QUESTION, 질문 생성·초안·답 뒤 tasksChanged(상대 Executor를 깨운다)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- executor/consult.ts: 읽기 전용 상담 실행(--tools Read,Grep,Glob, 빈 MCP), 초안 해석(decided 형식이 틀리면 미정으로)
- Executor 루프: 이 역할이 답할 질문이 있으면 상담을 하나씩 띄워 초안을 올린다. 진행 중인 작업공간이 같은 레포면 그걸 읽는다.
  실패한 질문은 다시 시도하지 않는다(담당자가 답한다)
- 브릿지: 실행 안에서 NOMOS_QUESTION_INLINE_WAIT_MS(기본 3분)만 기다리고 detach — "멈춰라, 나중에 다시 시작된다, 커밋하지 마라".
  에이전트 답(agent_answered)도 바로 쓴다
- 프롬프트: 재개한 실행에 "물어서 받은 답" 절(에이전트 답은 사람 미확인 표시)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… → 재개 (RUN_CLAUDE_E2E=1, ②는 E2E_BE_EMPTY=1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… E2E

- bridge/credentials.ts nomosHome(): 기본 ~/.nomos, NOMOS_HOME으로 바꾼다. 자격 증명·작업공간·레포·repos.json이 따른다.
  홈 자체를 바꾸면 Claude Code 로그인까지 바뀌어 안 된다.
- tests/e2e-executors.test.ts: 빌드된 CLI start 두 개(FE·BE)로 C안 전체 — BE Executor가 스스로 상담, FE Executor가 멈춘 태스크를
  답이 온 뒤 스스로 다시 집어 간다(RUN_CLAUDE_E2E=1, npm run build 먼저)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
정리 단계가 중계 대기열을 한 번만 비웠다. 테스트가 남긴 백그라운드 PM 작업은 계획 행·예산 확인 뒤에 대기열에 올라오므로,
부하가 있으면 비운 뒤에 올라와 가져갈 워커 없이 PM_TIMEOUT_MS(기본 10분)를 기다렸다 — 전체 실행 6회 중 3회 실패(60초 제한에서도).
백그라운드 작업이 다 끝날 때까지 계속 비운다. 단독 20/20, 전체 3/3 통과(616개 전부).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- dev의 룸 피드·멈춘 태스크(BLOCKED·AGENT_STOPPED)·G3·dep:add·대표 토큰 GitHub 등을 합쳤다. 충돌은 양쪽 유지로 풀었다.
- 질문 중계 마이그레이션 017_agent_questions → 018(dev의 017_rooms와 번호 충돌).
- 답을 기다리느라 내려놓은 실행(BLOCKED·QUESTION)의 종료는 멈춤으로 바꾸지 않는다(waitingQuestion).
- 답이 와서 READY가 되면 "다시 실행해 주세요 (재개)" — task_dispatches.resumes에 TASK_BLOCKED_ON_QUESTION도 센다.
- 룸에 질문 줄(QUESTION_*) — 묻는 쪽 룸과 답할 역할의 룸 둘 다.
- 권한 도구(permission_prompt)의 거부 문구도 멈춤 사유의 거부 명령으로 모인다(테스트 고정).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- projects.question_relay(018, 기본 켜짐). 시작 전에만 PATCH /projects/:id/settings(대표, PROJECT_SETTINGS_UPDATED).
  끄면 서버는 질문을 409 QUESTION_RELAY_OFF로 거부, 브리핑 questionRelay=false → Executor가 권한 도구를 붙이지 않고
  프롬프트는 "가정하고 GOTCHA로 남겨라". 대조군 실험·답할 사람 부재·상담 비용·되돌리기용.
- 다른 역할 소관 계약은 DECIDED가 아니라 GOTCHA "가정함"으로(스위치와 무관, E6) — feat/prompt-foreign-contract 규칙을 합쳤다.
- GET /me/questions: 내가 답할 질문(대표는 전부, 팀원은 자기 역할 대상) — 알림 배지용.
- 만료 주기 정리(startQuestionExpirySweep): 아무도 읽지 않아도 3일 지난 질문을 만료시키고 멈춘 태스크를 ESCALATED로.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@oyeonghun
oyeonghun merged commit acd4bb3 into dev Oct 8, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant