kedovi 0.77.2 → 0.78.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  `kedovi upgrade` 가 올린 버전 사이의 항목을 표로 보여 준다. 배포할 때 이번 버전 항목을 맨 위에 더한다 (`## 버전` 아래 `| 구분 | 내용 |` 표, 한 줄씩 짧게).
4
4
 
5
+ ## 0.78.0
6
+
7
+ | 구분 | 내용 |
8
+ |---|---|
9
+ | 기능 | 토론 모드 (`debate` 스킬): 여러 모델(다른 회사 포함 2~4개)이 따로 보고, 이름을 가린 주장 목록에 서로 동의 · 반대한 뒤 갈린 것만 다시 봄. 결과 = 합의 · 갈림(양쪽 근거) · 기각 · 한 모델만 · 빠진 모델. 코드 · 설계 · 요구 · 문서 어떤 작업이든 |
10
+ | 기능 | 패널은 이 PC 에서 쓸 수 있는 모델(설치된 CLI · `kedovi ai` 설정 · 로그인 요금제) 중 작업에 맞게 에이전트가 고름: `kedovi debate models --task code\|design\|docs` |
11
+ | 기능 | 결과가 미덥다는 말("마음에 안 들어" · "맞아?" · "다시 봐")이면 토론을 사용량과 함께 제안 (훅 `settings-hint`, 같은 세션 30분에 한 번). "토론해줘" 면 바로 |
12
+ | 모델 | Codex 세션의 리뷰도 다른 회사 모델: Claude 위임 (최고 · 표준 Opus, 절약 Sonnet). claude 가 없는 PC 는 GPT-6.1 Sol |
13
+ | 명령 | `kedovi debate list` · `status [id]` · `models` |
14
+ | 적용 | `kedovi upgrade` 뒤 sync. Codex 는 훅이 바뀌어 `/hooks` 에서 다시 신뢰 |
15
+
5
16
  ## 0.77.2
6
17
 
7
18
  | 구분 | 내용 |
package/bin/kedovi.js CHANGED
@@ -45,6 +45,7 @@
45
45
  * kedovi docs pages [on|off] [repo|폴더] 문서 사이트를 Pages 로 올리기 켜고 끄기 · 상태 (원격에 맞는 CI 파일 · .gitignore 의 /docs-site/)
46
46
  * kedovi docs serve [repo|폴더] [--port 8000] [--open] docs-site/ 를 이 PC 에서 웹 주소로 보기 (전체 인쇄 · 새로고침 없는 이동까지. Ctrl+C 로 끝)
47
47
  * kedovi delegate list · status [<id>] · watch [<id>] 다른 도구에 맡긴 작업(위임: Claude Code ↔ Codex 리뷰 등): 목록 · 지금 진행 현황 · 상대 에이전트가 읽고 실행하는 것을 줄마다 보기 (id 를 빼면 마지막 실행 중 작업)
48
+ * kedovi debate list · status [<id>] · models [--task code|design|docs] 토론 모드(여러 모델이 따로 보고 서로 반박): 목록 · 지금 회차와 모델별 상태 · 이 PC 에서 쓸 수 있는 모델과 패널 예시. 시작은 채팅에서 "토론해줘" (debate 스킬)
48
49
  * kedovi board [--no-open] [--no-fetch] 워크스페이스 현황판: 로드맵 · 팀원별 마지막 핸드오프 · 미완 작업 · 공지 + 기록 쪽 (.agents/board/, 이 PC 만 · 보기 전용)
49
50
  * kedovi screens check [repo|폴더] 화면설계서 검사: 근거 ID 가 색인에 있는지 · 화면 ID 중복 · 기능 ID 누락 (screens 팩)
50
51
  * kedovi datamodel check [repo|폴더] 데이터 설계 검사: 참조 · 기본키 · 중복 · 표 형식 · 컨벤션 · 기능 ID 대조 (datamodel 팩)
@@ -3576,7 +3577,7 @@ const HELP_GROUPS = [
3576
3577
  ['프로젝트 설정', '유형 · 진행 방식 · repo · 스택 · 규칙 · 생성물', ['type', 'approach', 'repo', 'detect', 'rules', 'sync', 'check', 'doctor', 'packs', 'reset']],
3577
3578
  ['팀', '팀 현황 · 명단 · 팀 구성', ['team', 'team-mode']],
3578
3579
  ['팀 규칙 점검', '리뷰 확인 · 훅이 알리거나 막는 수준', ['review', 'team-mode enforce']],
3579
- ['진행 · 기록', '로드맵 · 핸드오프 · 현황판 · 위임 · 옛 작업 가져오기', ['plan', 'handoff', 'board', 'delegate', 'import', 'tidy']],
3580
+ ['진행 · 기록', '로드맵 · 핸드오프 · 현황판 · 위임 · 토론 · 옛 작업 가져오기', ['plan', 'handoff', 'board', 'delegate', 'debate', 'import', 'tidy']],
3580
3581
  ['문서', '문서 사이트 · ID 찾기 · 화면설계서 · 데이터 설계 검사', ['docs', 'screens', 'datamodel']],
3581
3582
  ['알림 · 연동', '텔레그램 · 비밀값 · GitLab', ['notify', 'secret', 'gitlab']],
3582
3583
  ];
@@ -3803,6 +3804,7 @@ async function main() {
3803
3804
  case 'plan': return runWsTool('plan.js', rest);
3804
3805
  case 'docs': return runWsTool('docs-site.js', rest);
3805
3806
  case 'delegate': return runWsTool('delegate.js', rest.length ? rest : ['list']);
3807
+ case 'debate': return runWsTool('debate.js', rest.length ? rest : ['list']);
3806
3808
  case 'board': return runWsTool('board.js', rest);
3807
3809
  case 'tidy': return runWsTool('tidy.js', rest);
3808
3810
  case 'screens': return runWsTool('check-screens.js', rest.filter((x) => x !== 'check'), 'screens');
package/docs/commands.md CHANGED
@@ -44,6 +44,7 @@
44
44
  | `kedovi plan` · `--short` · `--json` · `--report [시작 [끝]]` · `init` · `check` | 진행 정도를 볼 때 · 단계를 끝냈을 때 · 주간 보고 | 로드맵(`handoffs/PLAN.md`): 단계 흐름 · 지금 단계 표 · 이번에 생긴 것(커밋 · 새 파일 · ID) · 다음. 이슈형은 이슈 보드: 상태별 건수 · 진행 중 이슈 표 · 멈춘 이슈 ⚠ · 개선 후보. `--report` 는 이슈형 기간 보고(기본 오늘까지 7일: 운영 반영 · 진행 중 · 새 접수 · 보류 · 멈춘 이슈). `--json` 은 다른 도구(전광판 등)용, `check` 는 갱신 누락 경고 |
45
45
  | `kedovi docs build [repo]` · `check` | 기획 문서 · 화면설계서 · 테이블 정의서를 고쳤을 때 | 기획 repo 의 md · 화면설계서 장 → `docs-site/` (정의 ID 앵커 · 근거 색인 `ids.json` · 뷰어 파일 · 정의서 폴더마다 ERD `erd.html`). `check` 는 md 와 `docs-site/` 가 어긋났는지 (기획 repo 의 pre-commit 도 막는다). 둘 다 문서 점검 경고(큰 문서 · 머리말 이력 · 같은 문장 · 긴 표 칸 · 문단형 표 · 칸 안 경위 · 그림 없는 큰 문서 · 메뉴 「기타」 · 규모 문서 없음)를 낸다 |
46
46
  | `kedovi delegate watch [id]` · `status [id]` · `list` | 다른 도구에 맡긴 리뷰 · 작업(Claude Code ↔ Codex)이 지금 무엇을 하는지 볼 때 | `watch` = 상대 에이전트가 읽은 파일 · 찾기 · 실행한 명령 · 고친 파일 · 중간에 한 말을 생기는 대로 한 줄씩(다른 터미널에서), 끝나면 요약. `status` = 경과 · 개수 · 최근 동작을 한 번. id 를 빼면 마지막 실행 중 작업 |
47
+ | `kedovi debate list` · `status [id]` · `models [--task code\|design\|docs]` | 토론 모드(여러 모델이 따로 보고 서로 반박)를 볼 때. 시작은 채팅에서 "토론해줘" | `models` = 이 PC 에서 쓸 수 있는 모델과 강점 점수, 작업에 맞춘 패널 예시. `status` = 지금 회차 · 모델별 상태 |
47
48
  | `kedovi docs serve [repo]` | Pages 없이 이 PC 에서 문서 사이트를 볼 때 | `docs-site/` 를 `http://127.0.0.1:8000/` 로 연다 (`--port` · `--open`). `file://` 로 열면 쪽 이동이 새로고침이 되고 전체 인쇄가 안 된다 |
48
49
  | `kedovi docs pages [on\|off] [repo]` | 문서 사이트를 Pages 로 올릴지 정할 때 | on = 원격에 맞는 CI 파일 · `docs-site/` 커밋 대상, off = `.gitignore` 에 `/docs-site/`. 인자 없이 = repo 마다 상태 |
49
50
  | `kedovi docs show <ID>` · `refs <ID>` | 근거를 볼 때 · 결정이 바뀌어 고칠 곳을 찾을 때 | `show` = 그 ID 의 정의 부분만(제목 절 · 표 행 · 문단 · 화면설계서 장). `refs` = 그 ID 가 나오는 곳(기획 · 코드 · 이슈 repo, 파일:줄, ★ 정의 · [동결]). 에이전트도 큰 문서를 통째로 열지 않고 이것을 쓴다 |
package/docs/packs.md CHANGED
@@ -62,6 +62,7 @@
62
62
  | `autopilot` | 자율완주 모드: 묻고 멈추는 대신 결정 필요 항목은 목록에 표시만 하고, 할 수 있는 구현은 끝까지 한 뒤 검증·핸드오프·커밋(푸시 없음)까지 마친다. "알아서 끝까지", "자리 비운다" 요청에 쓴다. |
63
63
  | `common-practice` | 흔한 방식 · 업계 관례 · 표준을 웹 검색으로 확인해 출처와 함께 비교해 답한다. 사용자가 "아닌가?", "보통 ~하던데", "다른 데는 어떻게 해?", "일반적으로", "표준이 뭐야", "찾아봐줘", "검색해봐" 라고 묻거나 다른 서비스 화면 · 문서를 예로 들며 우리 것과 다르다고 할 때, 또는 내가 "보통은 ~" 이라고 말하려 할 때 쓴다. |
64
64
  | `cross-review` | 다른 벤더의 에이전트에게 읽기 전용 교차 리뷰(두 번째 의견)를 받는다: Claude Code 세션이면 Codex, Codex 세션이면 Claude Code 를 비대화형으로 돌린다. "교차 리뷰", "다른 모델 의견", "codex 한테도 봐 달라" 요청에 쓴다. |
65
+ | `debate` | 토론 모드. 여러 모델(다른 회사 모델 포함 2~4개)이 같은 문제를 따로 보고, 이름을 가린 주장 목록에 서로 동의 · 반대해 갈린 것만 다시 본다. 코드 리뷰 · 설계 · 요구 · 문서 · 원인 분석 어떤 작업이든. "토론해줘", "토론 모드", "여러 모델로 봐줘", "다른 모델들한테도", 또는 결과가 마음에 안 든다 · 맞는지 모르겠다는 말에 쓴다. |
65
66
  | `doctor` | 워크스페이스 셋업·환경(Node·git·repos/ 의 repo·훅 경로·kit 생성물·에이전트 CLI)을 진단한다. 처음 받았을 때나 뭔가 깨졌을 때 쓴다. |
66
67
  | `go` | 이 워크스페이스에서 작업을 시작한다: 본인 핸드오프·repo 상태를 확인하고, 요청을 다시 정리해 영향 범위·할 일·검증 계획·안 할 것을 먼저 제시한 뒤 착수한다. "시작", "작업하자", 새 요구사항을 받았을 때 쓴다. |
67
68
  | `handoff` | 작업을 마무리하고 본인 핸드오프(작업별 기록 · 주간 기록 · LATEST · TODO)를 남긴다. "핸드오프", "마무리", "오늘 정리" 요청에 쓴다. "TODO 에 넣어줘", "TODO 정리" 도 이 스킬 5절을 따른다. |
@@ -79,7 +80,7 @@
79
80
 
80
81
  **팀 에이전트 역할**: 분석 (`analyst`) · 설계 (`architect`) · 문서화 (`documenter`) · 구현 (`implementer`) · 리뷰 (`reviewer`) · 설계 문서 (`spec-writer`) · 테스트 (`tester`)
81
82
 
82
- **도구**: `bin/board.js` · `bin/brief.js` · `bin/budget.js` · `bin/check-docs.js` · `bin/check-refs.js` · `bin/cross-review.js` · `bin/delegate.js` · `bin/docs-site.js` · `bin/doctor.js` · `bin/handoff.js` · `bin/import-scan.js` · `bin/issue-check.js` · `bin/notify.js` · `bin/plan.js` · `bin/rename-ref.js` · `bin/repo-activity.js` · `bin/repo-check.js` · `bin/review.js` · `bin/secret.js` · `bin/strip-ai-trailers.js` · `bin/team-status.js` · `bin/tidy.js`
83
+ **도구**: `bin/board.js` · `bin/brief.js` · `bin/budget.js` · `bin/check-docs.js` · `bin/check-refs.js` · `bin/cross-review.js` · `bin/debate.js` · `bin/delegate.js` · `bin/docs-site.js` · `bin/doctor.js` · `bin/handoff.js` · `bin/import-scan.js` · `bin/issue-check.js` · `bin/notify.js` · `bin/plan.js` · `bin/rename-ref.js` · `bin/repo-activity.js` · `bin/repo-check.js` · `bin/review.js` · `bin/secret.js` · `bin/strip-ai-trailers.js` · `bin/team-status.js` · `bin/tidy.js`
83
84
 
84
85
  **훅**: 세션 시작: `check-repo-updates.js` · 세션 시작: `check-context-sync.js` · 세션 시작: `handoff-brief.js` · 세션 시작: `session-brief.js` · 세션 시작: `model-profile.js` · 세션 시작: `team-mode.js` · 세션 시작: `channel-mode.js` · 세션 끝: `task-check.js` · 세션 끝: `decision-ask.js` · 세션 끝: `handoff-reminder.js` · 세션 끝: `agent-cleanup.js` · 세션 끝: `budget-check.js` · 세션 끝: `team-idle.js` · 세션 끝: `notify.js` · UserPromptSubmit: `agent-budget.js` · UserPromptSubmit: `task-focus.js` · UserPromptSubmit: `prompt-time.js` · UserPromptSubmit: `chat-prefs.js` · UserPromptSubmit: `settings-hint.js` · UserPromptSubmit: `budget-check.js` · UserPromptSubmit: `team-idle.js` · UserPromptSubmit: `decision-ask.js` · UserPromptSubmit: `plan-first.js` · UserPromptSubmit: `notify.js` · PreToolUse: `agent-budget.js` · PreToolUse: `notify.js` · PreToolUse: `decision-ask.js` · PreToolUse: `notify.js` · PreToolUse: `plan-first.js` · PreToolUse: `team-guard.js` · PreToolUse: `repo-guard.js` · PreToolUse: `review-gate.js` · PreToolUse: `spec-commit.js` · PostToolUse: `channel-progress.js` · PostToolUse: `chat-prefs.js` · PostToolUse: `notify.js` · PostToolUse: `notify.js` · Notification: `team-idle.js` · Notification: `notify.js` · SubagentStop: `agent-budget.js` · SubagentStop: `notify.js` · TeammateIdle: `team-idle.js` · TeammateIdle: `notify.js`
85
86
 
@@ -71,7 +71,7 @@
71
71
  | 설계 | Opus 5.5 (Claude 위임 · max) | GPT-6.1 Sol (xhigh) | GPT-6.1 Sol (high) |
72
72
  | 설계 문서 | GPT-6.1 Sol (xhigh) | GPT-6.1 Sol (high) | GPT-6.1 Sol (medium) |
73
73
  | 구현 | Opus 5.5 (Claude 위임 · xhigh) | Opus 5.5 (Claude 위임 · high) | GPT-6.1 Sol (medium) |
74
- | 리뷰 | GPT-6.1 Sol (high) | GPT-6.1 Sol (high) | GPT-6.1 Sol (high) |
74
+ | 리뷰 | Opus 5.5 (Claude 위임 · xhigh) | Opus 5.5 (Claude 위임 · high) | Sonnet 5.5 (Claude 위임 · high) |
75
75
  | 테스트 | GPT-6.1 Sol (high) | GPT-6 Luna (max) | GPT-6 Luna (max) |
76
76
  | 문서화 | GPT-6 Luna (high) | GPT-6 Luna (medium) | GPT-6 Luna (medium) |
77
77
  | 교차 검토 → Claude | Opus 5.5 (xhigh) | Opus 5.5 (high) | Sonnet 5.5 |
@@ -184,8 +184,9 @@ flowchart LR
184
184
  L1 --> T1["테스트: 서브에이전트 (Haiku 5.5)"]
185
185
  end
186
186
  subgraph CX["Codex 로 실행 (표준)"]
187
- L2["리드: GPT-6.1 Sol"] --> A2["분석 · 설계 · 리뷰<br/>Codex 에이전트 (6.1 Sol)"]
187
+ L2["리드: GPT-6.1 Sol"] --> A2["분석 · 설계<br/>Codex 에이전트 (6.1 Sol)"]
188
188
  L2 -->|"claude -p · 쓰기"| I2["구현: Opus"]
189
+ L2 -->|"claude -p · 읽기"| R2["리뷰: Opus"]
189
190
  L2 --> T2["테스트: Codex 에이전트 (Luna)"]
190
191
  end
191
192
  ```
@@ -202,3 +203,27 @@ flowchart LR
202
203
  | 사용량 한도(429) | 결과 머리에 「상대 도구로 다시: …」 명령 한 줄이 나온다. 같은 지시서(`task.md`)로 다른 도구에 맡긴다 |
203
204
 
204
205
  **교차 검토** (`cross-review` 스킬): 변경을 상대 회사 모델에게 읽기 전용으로 검토받아 두 의견을 대조한다. 모델은 `kedovi models` 의 "교차 검토 모델".
206
+
207
+ ## 토론 모드
208
+
209
+ 평소 리뷰는 다른 회사 모델 하나가 본다. 사용자가 "토론해줘" 라고 하거나 결과가 미덥다는 말("마음에 안 들어" · "맞아?" · "다시 봐")을 하면, 에이전트가 토론을 제안하고 확인받은 뒤 여러 모델(2~4개, 다른 회사 모델 포함)이 같은 문제를 본다. 코드 리뷰 · 설계 · 요구 · 문서 · 원인 분석 어떤 작업이든 된다 (`debate` 스킬).
210
+
211
+ ```mermaid
212
+ flowchart LR
213
+ A["평소: 모델 1개"] -->|"토론해줘 · 결과가 미덥다"| B["1차: 따로 보기<br/>모델마다 주장 · 근거"]
214
+ B --> C["2차: 서로 판정<br/>이름을 가린 주장 목록에 동의 · 반대 + 놓친 것"]
215
+ C -->|"갈린 것만"| D["3차: 다시 판정"]
216
+ C --> E["리더가 직접 확인 · 보고"]
217
+ D --> E
218
+ ```
219
+
220
+ | 항목 | 내용 |
221
+ |---|---|
222
+ | 패널 고르기 | 이 PC 에서 쓸 수 있는 모델(설치된 CLI · `kedovi ai` 설정 · 로그인 요금제) 중 작업에 맞는 것을 에이전트가 고른다. 다른 회사 모델을 꼭 넣고, 작업 강점(코드 = 코딩 점수, 설계 = 추론, 문서 = 언어)을 보고, 가장 비싼 급은 하나만. 목록: `kedovi debate models --task code` |
223
+ | 판정 | 근거(파일:줄 · 확인한 내용) 없는 동의 · 반대는 보류로 센다. 누가 냈는지 가려 자기 주장 편들기를 줄인다 |
224
+ | 결과 | 합의 · 갈림(양쪽 근거) · 기각 · 한 모델만. 리더가 발견마다 직접 확인해 판정하고, 갈린 것은 근거를 놓고 정하거나 사용자에게 묻는다 |
225
+ | 실패 | 사용량 한도 · 로그인으로 빠진 모델은 다른 모델로 대신하지 않고 「빠진 모델」로 적는다. 2개 미만이면 멈춘다 |
226
+ | 비용 | 모델 수 × 회차라 보통 평소 리뷰의 5~7배. 요청할 때만 돈다. 읽기 전용이다 |
227
+ | 보기 | `kedovi debate list` · `kedovi debate status [id]` |
228
+
229
+ > 근거: 같은 모델끼리의 토론은 비슷한 답으로 다수결이 되고, 다른 회사 모델을 섞을 때 정확도가 오른다 ([arXiv 2502.08788](https://arxiv.org/abs/2502.08788v3) · [2410.12853](https://arxiv.org/abs/2410.12853v1) · [2601.05746](https://arxiv.org/abs/2601.05746)). 진행 방식은 공개 프로젝트를 참고했다: 따로 답한 뒤 이름을 가려 서로 평가(karpathy/llm-council), 모두 동의할 때까지 비판하고 근거 없는 동의를 다시 묻기(spec-debate), 빠진 모델 기록(claude-octopus).
package/docs/workflow.md CHANGED
@@ -219,7 +219,7 @@ Codex 는 역할 파일(`.codex/agents`)을 하위 에이전트에 적용하지
219
219
  | 설계 | Opus 5.5 (Claude 위임 · max) | GPT-6.1 Sol (xhigh) | GPT-6.1 Sol (high) |
220
220
  | 설계 문서 | GPT-6.1 Sol (xhigh) | GPT-6.1 Sol (high) | GPT-6.1 Sol (medium) |
221
221
  | 구현 | Opus 5.5 (Claude 위임 · xhigh) | Opus 5.5 (Claude 위임 · high) | GPT-6.1 Sol (medium) |
222
- | 리뷰 | GPT-6.1 Sol (high) | GPT-6.1 Sol (high) | GPT-6.1 Sol (high) |
222
+ | 리뷰 | Opus 5.5 (Claude 위임 · xhigh) | Opus 5.5 (Claude 위임 · high) | Sonnet 5.5 (Claude 위임 · high) |
223
223
  | 테스트 | GPT-6.1 Sol (high) | GPT-6 Luna (max) | GPT-6 Luna (max) |
224
224
  | 문서화 | GPT-6 Luna (high) | GPT-6 Luna (medium) | GPT-6 Luna (medium) |
225
225
  | 교차 검토 → Claude | Opus 5.5 (xhigh) | Opus 5.5 (high) | Sonnet 5.5 |
@@ -382,6 +382,7 @@ flowchart TD
382
382
  | `kedovi docs build [repo]` | 기획 repo 의 md · 화면설계서 장 → 같은 경로 구조의 `docs-site/` (정의 ID 앵커 · 근거 색인 · 정의서 폴더마다 ERD). 묶음 · 포함 범위는 `docs-site.json`, 옛 `html/` 은 처음 build 때 옮긴다. `check` 는 md 와 어긋났는지. 둘 다 문서 점검 경고와 mermaid 문법 오류를 낸다. 자세히는 [문서 사이트](docs-site.md) |
383
383
  | `kedovi docs show <ID>` · `refs <ID>` | 기획 문서를 ID 단위로: 정의 부분만 출력 · 그 ID 가 나오는 곳(기획 · 코드 · 이슈 repo, 파일:줄, ★ 정의 · [동결]) |
384
384
  | `kedovi delegate watch [id]` · `status [id]` · `list` | 다른 도구에 맡긴 작업(위임)의 진행: 상대 에이전트가 읽고 실행하는 것을 줄마다 · 지금 현황 한 번 · 목록 |
385
+ | `kedovi debate list` · `status [id]` · `models` | 토론 모드: 목록 · 지금 회차와 모델별 상태 · 쓸 수 있는 모델과 패널 예시 (시작은 채팅에서 "토론해줘") |
385
386
  | `kedovi docs serve [repo]` | `docs-site/` 를 이 PC 에서 웹 주소(`http://127.0.0.1:8000/`)로 보기. 전체 인쇄와 새로고침 없는 이동까지 된다 |
386
387
  | `kedovi docs pages [on\|off] [repo]` | 문서 사이트를 Pages 로 올리기 켜고 끄기 · 상태. on = 원격(GitLab · GitHub)에 맞는 CI 파일(`.gitlab-ci.yml` · `.github/workflows/pages.yml`)을 두고 `docs-site/` 를 커밋 대상으로(원격이 없으면 둘 다), off = `.gitignore` 에 `/docs-site/` (CI 는 두어도 돌지 않는다) |
387
388
  | `kedovi docs refs-quote <파일…>` | 본문 괄호 속 근거(파일:줄)를 그 문단 · 목록 · 표 아래 「> 근거:」 인용구로 옮긴다 (`--dry` = 수만) |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kedovi",
3
- "version": "0.77.2",
3
+ "version": "0.78.0",
4
4
  "private": false,
5
5
  "description": "Claude Code · Codex 겸용 AI 협업 워크스페이스 킷",
6
6
  "license": "MIT",
@@ -0,0 +1,431 @@
1
+ #!/usr/bin/env node
2
+ // 토론 모드: 여러 모델(다른 회사 우선)이 같은 문제를 따로 보고, 이름을 가린 주장 목록에 서로 동의 · 반대해 갈린 것만 다시 본다. 실행: node packs/core/bin/delegate.js 를 회차마다 부른다
3
+ /*
4
+ * models [--task code|design|docs] 이 PC 에서 쓸 수 있는 모델 (설치된 CLI · kedovi ai 설정 · 로그인 요금제) 과 강점 점수, 작업에 맞춘 패널 예시
5
+ * start --panel 도구:모델[:effort[:관점]],… [--role 역할] [--rounds N] [--topic 라벨] 작업 지시서는 stdin (또는 --task-file)
6
+ * 패널 2~4개. 도구 = codex | claude. 관점 = 보안 · 성능 · 반대 역할 같은 맡을 시각(선택). 역할 기본 reviewer (설계는 architect, 요구는 analyst)
7
+ * rounds = 최대 회차 (기본 3, 2~4): 1차 따로 보기 → 2차 서로 판정 → 3차부터 갈린 주장만
8
+ * → 토론 id 를 출력하고 바로 끝난다. 패널이 이 PC 에 없으면 시작하지 않는다 (같은 회사 모델로 몰래 바꾸지 않는다)
9
+ * wait <id> [--timeout 초(기본 900)] 끝날 때까지 기다려 결과(합의 · 갈림 · 기각 · 한 모델만 · 빠진 모델)를 보여 준다
10
+ * status [<id>] 지금 회차 · 모델별 상태
11
+ * list 이 워크스페이스의 토론
12
+ * run … (내부) start 가 떼어 낸 진행 프로세스
13
+ * --dry-run 실행하지 않고 패널 · 회차 · 1차 지시서만
14
+ * 결과의 주장도 주장이다: 리더가 발견마다 직접 확인해 판정한다 (debate 스킬).
15
+ */
16
+ 'use strict';
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { spawn, spawnSync } = require('child_process');
21
+ const { load, stateDir } = require('../lib/workspace');
22
+ const { findCli } = require('../lib/cli');
23
+ const { REVIEW_ROLE, hasCheckLine, CHECK_MESSAGE, checkLevel } = require('../lib/review');
24
+
25
+ const argv = process.argv.slice(2);
26
+ const cmd = argv[0];
27
+ const opt = (k, d = null) => { const i = argv.indexOf(`--${k}`); return i >= 0 ? argv[i + 1] : d; };
28
+ const has = (k) => argv.includes(`--${k}`);
29
+
30
+ // 명령으로 돌 때만 워크스페이스를 읽는다 (판정 함수는 시험에서 따로 쓴다)
31
+ const MAIN = require.main === module;
32
+ const ws = MAIN ? load() : null;
33
+ const ROOT = ws ? ws.root : null;
34
+ const DIR = ROOT ? path.join(stateDir(ROOT), 'debate') : null;
35
+ const DELEGATE = path.join(__dirname, 'delegate.js');
36
+ if (DIR) fs.mkdirSync(DIR, { recursive: true });
37
+
38
+ const LETTERS = ['A', 'B', 'C', 'D'];
39
+ const LEVELS = ['블로커', '권장', '참고'];
40
+ const STANCES = { 동의: 'agree', 반대: 'oppose', 보류: 'hold' };
41
+
42
+ // ───────── 쓸 수 있는 모델 ─────────
43
+
44
+ const TASK_FIELDS = { code: ['에이전트 코딩', '코딩'], design: ['추론', '데이터 분석'], docs: ['언어', '지시 따르기'] };
45
+
46
+ /** 세션을 연 도구 (Claude Code 는 CLAUDECODE · Codex 는 CODEX_* 환경변수) */
47
+ function caller(env = process.env) {
48
+ if (env.CLAUDECODE === '1' || env.CLAUDE_CODE_ENTRYPOINT) return 'claude';
49
+ if (Object.keys(env).some((k) => k.startsWith('CODEX_'))) return 'codex';
50
+ return null;
51
+ }
52
+
53
+ /** 이 PC 에서 쓸 수 있는 모델: 설치된 CLI · kedovi ai 의 회사 설정 · 퇴역 · 목록에서 빠진 모델 제외 */
54
+ function usable() {
55
+ let kit = null;
56
+ try { kit = require(path.join(ROOT, 'bin', 'kit.js')); } catch { /* 옛 워크스페이스 */ }
57
+ let cat = {};
58
+ try { cat = JSON.parse(fs.readFileSync(path.join(ROOT, 'packs', 'core', 'models.json'), 'utf8')); } catch { /* 없음 */ }
59
+ let access = { vendors: ['claude', 'codex'] };
60
+ let plans = {};
61
+ try { access = kit.resolveModels(ROOT, kit.loadWorkspace(ROOT)).access || access; } catch { /* 기본 */ }
62
+ try { plans = kit.detectPlans(cat); } catch { /* 모름 */ }
63
+ const retired = (cat.retired || []).map((r) => ({ tool: r.tool, re: new RegExp(r.match) }));
64
+ const out = [];
65
+ for (const tool of ['claude', 'codex']) {
66
+ const cli = findCli(tool);
67
+ const allowed = (access.vendors || []).includes(tool);
68
+ for (const m of (cat.tools || {})[tool] || []) {
69
+ if (m.id === 'opusplan' || /목록에서 빠짐/.test(m.hint || '') || retired.some((r) => r.tool === tool && r.re.test(m.id))) continue;
70
+ const st = (m.strengths || [])[0] || {};
71
+ const price = ((m.hint || '').match(/API (\$[\d.]+·\$[\d.]+)/) || [])[1] || '';
72
+ out.push({ tool, id: m.id, label: m.label, hint: (m.hint || '').replace(/\s*\(API[^)]*\)/, ''), strengths: st, price, ok: !!cli && allowed, why: !cli ? `${tool} 가 설치돼 있지 않음` : !allowed ? '이 PC 설정(kedovi ai)이 이 회사 모델을 쓰지 않음' : '' });
73
+ }
74
+ }
75
+ return { list: out, plans, access, cat };
76
+ }
77
+
78
+ /** 작업 종류에 맞는 패널 예시: 다른 회사 1위 → 같은 회사 1위 → 다른 회사 2위 (쓸 수 있는 것만) */
79
+ function suggest(list, task, leader) {
80
+ const f = TASK_FIELDS[task] || TASK_FIELDS.code;
81
+ const score = (m) => f.reduce((s, k) => s + (Number(m.strengths[k]) || 0), 0);
82
+ const ok = list.filter((m) => m.ok && score(m) > 0).sort((a, b) => score(b) - score(a));
83
+ const other = ok.filter((m) => m.tool !== (leader || 'claude'));
84
+ const same = ok.filter((m) => m.tool === (leader || 'claude'));
85
+ const pick = [other[0], same[0], other[1] || same[1]].filter(Boolean);
86
+ return [...new Map(pick.map((m) => [`${m.tool}:${m.id}`, m])).values()].slice(0, 3);
87
+ }
88
+
89
+ function models() {
90
+ const task = opt('task', 'code');
91
+ const leader = caller();
92
+ const { list, plans, cat } = usable();
93
+ const f = TASK_FIELDS[task] || TASK_FIELDS.code;
94
+ console.log(`이 PC 에서 쓸 수 있는 모델 (작업: ${task} · 강점 점수 = LiveBench ${f.join(' + ')}${leader ? ` · 지금 세션: ${leader === 'claude' ? 'Claude Code' : 'Codex'}` : ''})`);
95
+ for (const tool of ['claude', 'codex']) {
96
+ const rows = list.filter((m) => m.tool === tool);
97
+ if (!rows.length) continue;
98
+ const pl = plans[tool] && plans[tool].id ? ((cat.plans || {})[tool] || []).find((x) => x.id === plans[tool].id) : null;
99
+ const plan = pl ? ` · 요금제 ${pl.label}` : '';
100
+ console.log(`\n${tool === 'claude' ? 'Claude (도구 claude)' : 'GPT (도구 codex)'}${plan}`);
101
+ for (const m of rows) {
102
+ const sc = f.map((k) => (m.strengths[k] != null ? `${k} ${m.strengths[k]}` : '')).filter(Boolean).join(' · ') || '독립 점수 없음';
103
+ console.log(` ${m.ok ? '✔' : '✖'} ${tool}:${m.id} ${m.label} · ${m.hint}${m.price ? ` · API ${m.price}` : ''} · ${sc}${m.ok ? '' : ` (${m.why})`}`);
104
+ }
105
+ }
106
+ const s = suggest(list, task, leader);
107
+ if (s.length >= 2) console.log(`\n패널 예시 (다른 회사 먼저): --panel ${s.map((m) => `${m.tool}:${m.id}:high`).join(',')}`);
108
+ else console.log('\n⚠ 쓸 수 있는 모델이 2개가 안 돼 토론할 수 없습니다. 다른 회사 CLI 를 설치 · 로그인하거나 kedovi ai 에서 두 회사를 켠다');
109
+ console.log('고르는 기준: 다른 회사 모델을 꼭 넣는다 (같은 모델끼리는 다수결로 끝난다) · 작업의 강점 점수 · 리더(지금 세션) 모델과 다른 것 · 비싼 모델은 하나만');
110
+ }
111
+
112
+ // ───────── 주장 · 판정 읽기 ─────────
113
+
114
+ /** 1차 · 놓친 것: `- [블로커|권장|참고] 주장 / 근거` */
115
+ function parseClaims(text, section) {
116
+ const lines = sectionLines(text, section);
117
+ const out = [];
118
+ for (const l of lines) {
119
+ const m = l.match(/^\s*(?:[-*]|\d+\.)\s*\[(블로커|권장|참고)\]\s*(.+)$/);
120
+ if (m && !/^(?:없음|없다|해당\s*없음|none)(?:[\s.,:/]|$)/i.test(m[2].trim())) out.push({ level: m[1], text: m[2].trim().slice(0, 400) });
121
+ }
122
+ return out.slice(0, 12);
123
+ }
124
+
125
+ /** 절(## 제목) 안의 줄. 절이 없으면 전체 */
126
+ function sectionLines(text, title) {
127
+ const lines = String(text || '').split('\n');
128
+ const i = lines.findIndex((l) => new RegExp(`^#{1,4}\\s*${title}`).test(l.trim()));
129
+ if (i < 0) return lines;
130
+ const rest = lines.slice(i + 1);
131
+ const j = rest.findIndex((l) => /^#{1,4}\s/.test(l.trim()));
132
+ return j < 0 ? rest : rest.slice(0, j);
133
+ }
134
+
135
+ /** 판정: `- A1: 동의|반대|보류 / 근거`. 근거 없는 동의는 보류로 센다 */
136
+ function parseVotes(text) {
137
+ const out = {};
138
+ for (const l of sectionLines(text, '판정')) {
139
+ const m = l.match(/^\s*(?:[-*]|\d+\.)?\s*\**([A-DN]\d+)\**\s*[::]\s*\**(동의|반대|보류)\**\s*(?:[—–\-/|]+\s*(.*))?$/);
140
+ if (!m) continue;
141
+ const why = (m[3] || '').trim();
142
+ let stance = STANCES[m[2]];
143
+ if (stance !== 'hold' && why.replace(/\s/g, '').length < 8) stance = 'hold';
144
+ out[m[1]] = { stance, why: why.slice(0, 300), lazy: STANCES[m[2]] !== 'hold' && stance === 'hold' };
145
+ }
146
+ return out;
147
+ }
148
+
149
+ /** 주장 하나의 상태: 합의 = 판정한 모델(2개 이상)이 모두 동의 · 기각 = 모두 반대 · 판정한 모델이 하나 이하면 한 모델만 · 그 밖은 갈림. 낸 모델은 따로 표가 없으면 동의로 센다 */
150
+ function verdict(c, active) {
151
+ const votes = { ...(c.by && active.includes(c.by) && !c.votes[c.by] ? { [c.by]: { stance: 'agree' } } : {}), ...c.votes };
152
+ const v = Object.entries(votes).filter(([who]) => active.includes(who)).map(([, x]) => x.stance);
153
+ const agree = v.filter((s) => s === 'agree').length;
154
+ const oppose = v.filter((s) => s === 'oppose').length;
155
+ if (v.length < 2) return 'single';
156
+ if (agree && !oppose && agree === v.length) return 'agreed';
157
+ if (oppose && !agree && oppose === v.length) return 'rejected';
158
+ return 'split';
159
+ }
160
+
161
+ // ───────── 지시서 ─────────
162
+
163
+ function round1(task, p, n) {
164
+ return [task.trim(), '', '# 토론 1차: 따로 보기', '',
165
+ `이 문제를 다른 AI 모델 ${n - 1}개도 따로 보고 있다. 서로의 답은 보지 않는다. 다음 회차에 모델들이 서로의 주장을 근거로 판정한다.`,
166
+ p.view ? `너의 관점: **${p.view}** (이 시각에서 특히 본다. 다른 문제도 보이면 적는다)` : '',
167
+ '보고 끝에 `## 주장` 절을 두고 주장(발견 · 의견 · 제안)을 한 줄씩 적는다 (최대 12개):',
168
+ '- [블로커|권장|참고] 주장 / 근거 (파일:줄 · 재현 입력 · 문서 위치 · 출처)',
169
+ '근거를 댈 수 없는 것은 [참고] 로 둔다. 같은 내용을 여러 줄로 나누지 않는다.'].filter((l) => l !== '').join('\n');
170
+ }
171
+
172
+ function claimLine(c) { return `- ${c.id} [${c.level}] ${c.text}`; }
173
+
174
+ function round2(task, claims) {
175
+ return [task.trim(), '', '# 토론 2차: 서로 판정', '',
176
+ '1차에서 모델들이 따로 낸 주장이다. 누가 냈는지는 가렸고 네 주장도 섞여 있다. 코드 · 문서를 직접 확인해 하나씩 판정한다.',
177
+ '- 동의는 확인한 근거(파일:줄 · 본 내용)를 댄다. 근거 없는 동의는 보류로 센다.',
178
+ '- 반대는 왜 틀렸는지(사실과 다른 곳 · 반례 · 범위 밖)를 댄다. 강도(블로커 · 권장)만 틀렸으면 반대하고 맞는 강도를 적는다.',
179
+ '- 많은 쪽을 따르지 말고 증거를 따른다. 확인할 수 없으면 보류.',
180
+ '', '`## 판정` 절에 주장마다 한 줄:', '- A1: 동의|반대|보류 / 근거',
181
+ '그다음 `## 놓친 것` 절에 모두가 놓친 주장을 1차 형식(`- [블로커|권장|참고] 주장 / 근거`)으로. 없으면 「없음」.',
182
+ '', '# 주장 목록', '', ...claims.map(claimLine)].join('\n');
183
+ }
184
+
185
+ function roundN(task, claims, n) {
186
+ const body = [];
187
+ for (const c of claims) {
188
+ body.push(claimLine(c));
189
+ for (const [who, v] of Object.entries(c.votes)) body.push(` - 모델 ${who}: ${({ agree: '동의', oppose: '반대', hold: '보류' })[v.stance]}${v.why ? ` / ${v.why}` : ''}`);
190
+ }
191
+ return [task.trim(), '', `# 토론 ${n}차: 갈린 주장 다시 보기`, '',
192
+ '아래 주장은 모델들의 판정이 갈렸거나 앞 회차에 새로 나왔다. 다른 모델의 근거를 읽고 코드 · 문서를 다시 확인해 최종 판정한다. 근거가 더 강한 쪽으로 바꿔도 된다. 버티기 위한 반대는 하지 않는다.',
193
+ '', '`## 판정` 절에 주장마다 한 줄: `- A1: 동의|반대|보류 / 근거`. 새 주장은 내지 않는다.',
194
+ '', '# 갈린 주장', '', ...body].join('\n');
195
+ }
196
+
197
+ // ───────── 실행 ─────────
198
+
199
+ function parsePanel(s) {
200
+ const list = String(s || '').split(',').map((x) => x.trim()).filter(Boolean).map((x) => {
201
+ const [tool, model, effort, ...view] = x.split(':');
202
+ if (!['codex', 'claude'].includes(tool) || !model) throw new Error(`패널 형식: 도구:모델[:effort[:관점]] (도구 = codex | claude). 받은 값: ${x}`);
203
+ return { tool, model, effort: effort || null, view: view.join(':') || null };
204
+ });
205
+ if (list.length < 2 || list.length > 4) throw new Error('패널은 2~4개입니다');
206
+ return list;
207
+ }
208
+
209
+ function readTask() {
210
+ const f = opt('task-file');
211
+ if (f) return fs.readFileSync(path.resolve(f), 'utf8');
212
+ if (process.stdin.isTTY) return '';
213
+ try { return fs.readFileSync(0, 'utf8'); } catch { return ''; }
214
+ }
215
+
216
+ const save = (id, name, data) => fs.writeFileSync(path.join(DIR, id, name), typeof data === 'string' ? data : JSON.stringify(data, null, 2));
217
+ const readJson = (id, name) => { try { return JSON.parse(fs.readFileSync(path.join(DIR, id, name), 'utf8')); } catch { return null; } };
218
+
219
+ function start() {
220
+ const panel = parsePanel(opt('panel'));
221
+ const role = opt('role', 'reviewer');
222
+ const rounds = Math.min(4, Math.max(2, Number(opt('rounds', 3)) || 3));
223
+ const task = readTask();
224
+ if (!task.trim()) throw new Error('작업 지시서가 비었습니다. stdin 이나 --task-file 로 주세요');
225
+ const missing = [...new Set(panel.map((p) => p.tool))].filter((t) => !findCli(t));
226
+ if (missing.length) { console.log(`✖ ${missing.join(' · ')} 가 이 PC 에 없어 토론을 시작하지 않습니다. 패널에서 빼거나 설치 · 로그인한 뒤 다시 (같은 회사 모델로 대신하지 않는다. 쓸 수 있는 모델: node packs/core/bin/debate.js models)`); process.exit(3); }
227
+ if (REVIEW_ROLE.test(role) && !hasCheckLine(task) && checkLevel(ROOT) === 'block') { console.log(`✖ ${CHECK_MESSAGE}`); process.exit(6); }
228
+ const vendors = new Set(panel.map((p) => p.tool));
229
+ const letters = LETTERS.slice(0, panel.length).sort(() => Math.random() - 0.5);
230
+ const members = panel.map((p, i) => ({ ...p, who: letters[i] })).sort((a, b) => a.who.localeCompare(b.who));
231
+ const id = `${new Date().toISOString().replace(/[-:T]/g, '').slice(0, 14)}-debate-${process.pid}`;
232
+ const meta = { id, role, rounds, topic: opt('topic') || '', members, startedAt: Date.now() };
233
+ const warn = vendors.size < 2 ? '⚠ 패널이 모두 같은 회사 모델입니다. 같은 회사 모델끼리는 의견이 비슷해 다수결로 끝나기 쉽습니다 (가능하면 다른 회사 모델을 넣는다)' : '';
234
+ if (has('dry-run')) {
235
+ console.log(JSON.stringify({ ...meta, warn: warn || undefined, round1: `${round1(task, members[0], members.length).slice(-600)}` }, null, 2));
236
+ return;
237
+ }
238
+ fs.mkdirSync(path.join(DIR, id), { recursive: true });
239
+ save(id, 'meta.json', meta);
240
+ save(id, 'task.md', task);
241
+ const child = spawn(process.execPath, [__filename, 'run', '--debate', id], { cwd: ROOT, detached: true, stdio: 'ignore' });
242
+ child.unref();
243
+ console.log(`토론 시작: ${members.map((m) => `${m.tool}:${m.model}${m.effort ? `·${m.effort}` : ''}${m.view ? `(${m.view})` : ''}`).join(' · ')} · 역할 ${role} · 최대 ${rounds}회`);
244
+ if (warn) console.log(warn);
245
+ console.log(`토론 id: ${id}`);
246
+ console.log(`다음: node packs/core/bin/debate.js wait ${id} (회차마다 모델들이 동시에 돈다. 진행: node packs/core/bin/debate.js status ${id})`);
247
+ }
248
+
249
+ /** 위임 하나 시작 → 작업 id */
250
+ function delegateStart(id, m, text, n) {
251
+ const f = path.join(DIR, id, `round-${n}-${m.who}.task.md`);
252
+ fs.writeFileSync(f, text);
253
+ const r = spawnSync(process.execPath, [DELEGATE, 'start', '--to', m.tool, '--role', readJson(id, 'meta.json').role, '--mode', 'read', '--model', m.model,
254
+ ...(m.effort ? ['--effort', m.effort] : []), '--group', id, '--task-file', f], { cwd: ROOT, encoding: 'utf8' });
255
+ const job = (r.stdout.match(/작업 id: (\S+)/) || [])[1];
256
+ return job ? { job } : { error: (r.stdout + r.stderr).trim().split('\n').slice(-2).join(' ') || `종료 코드 ${r.status}` };
257
+ }
258
+
259
+ /** 작업 끝까지 → { ok, out, fail } */
260
+ function delegateWait(job) {
261
+ const res = spawnSync(process.execPath, [DELEGATE, 'wait', job, '--timeout', String(Number(process.env.KEDOVI_DEBATE_TIMEOUT) || 1500)], { cwd: ROOT, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
262
+ const text = res.stdout || '';
263
+ const fail = (text.match(/⚠ 실패 원인: ([^\n]+)/) || text.match(/⚠ (실패|보고가 비었음)[^\n]*/) || [])[1] || (res.status === 2 ? '시간 초과 (아직 도는 중)' : '');
264
+ const out = text.split(/\n## \S+ 의 보고\n/)[1] || '';
265
+ return { ok: res.status === 0 && !!out.trim() && !/^\(보고 없음\)/.test(out.trim()), out, fail: fail || (res.status !== 0 ? `종료 코드 ${res.status}` : '') };
266
+ }
267
+
268
+ /** 한 회차: 살아 있는 모델에게 동시에 → 모델별 보고 */
269
+ async function runRound(id, n, active, textFor) {
270
+ const meta = readJson(id, 'meta.json');
271
+ const st = readJson(id, 'state.json');
272
+ st.round = n;
273
+ save(id, 'state.json', st);
274
+ const started = active.map((who) => {
275
+ const m = meta.members.find((x) => x.who === who);
276
+ return { who, ...delegateStart(id, m, textFor(m), n) };
277
+ });
278
+ const results = {};
279
+ await Promise.all(started.map((s) => new Promise((ok) => {
280
+ if (s.error) { results[s.who] = { ok: false, fail: s.error }; ok(); return; }
281
+ // wait 는 동기 실행이라 하위 프로세스로 돌려 회차 안의 모델이 동시에 끝나기를 기다린다
282
+ const c = spawn(process.execPath, [__filename, 'wait-job', '--job', s.job], { cwd: ROOT, stdio: ['ignore', 'pipe', 'ignore'] });
283
+ let buf = '';
284
+ c.stdout.on('data', (d) => { buf += d; });
285
+ c.on('close', () => { try { results[s.who] = { ...JSON.parse(buf), job: s.job }; } catch { results[s.who] = { ok: false, fail: '결과를 읽지 못함', job: s.job }; } ok(); });
286
+ })));
287
+ for (const [who, r] of Object.entries(results)) fs.writeFileSync(path.join(DIR, id, `round-${n}-${who}.md`), r.out || `(실패: ${r.fail})`);
288
+ return results;
289
+ }
290
+
291
+ async function run() {
292
+ const id = opt('debate');
293
+ const meta = readJson(id, 'meta.json');
294
+ const task = fs.readFileSync(path.join(DIR, id, 'task.md'), 'utf8');
295
+ const st = { round: 0, claims: [], dropped: {}, lazy: {}, history: [] };
296
+ save(id, 'state.json', st);
297
+ let active = meta.members.map((m) => m.who);
298
+ const drop = (results, n) => {
299
+ for (const [who, r] of Object.entries(results)) if (!r.ok) { st.dropped[who] = `${n}차: ${r.fail || '실패'}`; active = active.filter((x) => x !== who); }
300
+ };
301
+ // 1차: 따로
302
+ const r1 = await runRound(id, 1, active, (m) => round1(task, m, meta.members.length));
303
+ Object.assign(st, readJson(id, 'state.json'));
304
+ drop(r1, 1);
305
+ for (const [who, r] of Object.entries(r1)) if (r.ok) parseClaims(r.out, '주장').forEach((c, i) => st.claims.push({ id: `${who}${i + 1}`, by: who, round: 1, ...c, votes: {} }));
306
+ st.history.push({ round: 1, active: [...active], claims: st.claims.length });
307
+ save(id, 'state.json', st);
308
+ // 2차 ~ : 판정 (2차는 전부, 3차부터 갈린 것 · 새로 나온 것만)
309
+ let newCount = 0;
310
+ for (let n = 2; n <= meta.rounds && active.length >= 2; n++) {
311
+ const open = n === 2 ? st.claims : st.claims.filter((c) => verdict(c, active) === 'split' || verdict(c, active) === 'single');
312
+ if (!open.length) break;
313
+ const rn = await runRound(id, n, active, () => (n === 2 ? round2(task, open) : roundN(task, open, n)));
314
+ Object.assign(st, { round: n });
315
+ drop(rn, n);
316
+ for (const [who, r] of Object.entries(rn)) {
317
+ if (!r.ok) continue;
318
+ const votes = parseVotes(r.out);
319
+ for (const c of open) if (votes[c.id]) { c.votes[who] = votes[c.id]; if (votes[c.id].lazy) st.lazy[who] = (st.lazy[who] || 0) + 1; }
320
+ if (n === 2) parseClaims(r.out, '놓친 것').forEach((c) => { newCount++; st.claims.push({ id: `N${newCount}`, by: who, round: 2, ...c, votes: { [who]: { stance: 'agree', why: '(낸 모델)' } } }); });
321
+ }
322
+ st.history.push({ round: n, active: [...active], open: open.length });
323
+ save(id, 'state.json', st);
324
+ }
325
+ const endedAt = Date.now();
326
+ save(id, 'result.json', { endedAt, active, report: report(meta, st, active, endedAt) });
327
+ notifyDone(meta, active, endedAt);
328
+ }
329
+
330
+ function report(meta, st, active, endedAt) {
331
+ const name = (who) => { const m = meta.members.find((x) => x.who === who); return `${who}=${m.tool}:${m.model}${m.view ? `(${m.view})` : ''}`; };
332
+ const by = { agreed: [], split: [], rejected: [], single: [] };
333
+ for (const c of st.claims) by[verdict(c, active)].push(c);
334
+ const stanceText = { agree: '동의', oppose: '반대', hold: '보류' };
335
+ const votesOf = (c) => Object.entries(c.votes).map(([w, v]) => `${w} ${stanceText[v.stance]}`).join(' · ');
336
+ const mins = ((endedAt - meta.startedAt) / 60000).toFixed(1);
337
+ const L = [`# 토론 결과${meta.topic ? `: ${meta.topic}` : ''} · 패널 ${meta.members.map((m) => name(m.who)).join(' · ')} · ${st.round}회 · ${mins}분`,
338
+ `주장 ${st.claims.length}개: 합의 ${by.agreed.length} · 갈림 ${by.split.length} · 기각 ${by.rejected.length} · 한 모델만 ${by.single.length}`];
339
+ const out = Object.entries(st.dropped);
340
+ if (out.length) L.push('', `⚠ 빠진 모델 (다른 모델로 대신하지 않았다): ${out.map(([w, why]) => `${name(w)} ${why}`).join(' · ')}`);
341
+ if (active.length < 2) L.push('', '⚠ 판정할 모델이 2개가 안 돼 토론이 중간에 멈췄습니다. 남은 주장은 한 모델 의견입니다');
342
+ const lazy = Object.entries(st.lazy);
343
+ if (lazy.length) L.push('', `근거 없는 동의 · 반대는 보류로 셈: ${lazy.map(([w, k]) => `${w} ${k}건`).join(' · ')}`);
344
+ const sec = (title, list, detail) => {
345
+ if (!list.length) return;
346
+ L.push('', `## ${title} (${list.length})`);
347
+ for (const c of LEVELS.flatMap((lv) => list.filter((x) => x.level === lv))) {
348
+ L.push(`- ${c.id} [${c.level}] ${c.text} · 낸 모델 ${c.by} · ${votesOf(c) || '판정 없음'}`);
349
+ if (detail) for (const [w, v] of Object.entries(c.votes)) if (v.why && v.why !== '(낸 모델)') L.push(` - ${w} ${stanceText[v.stance]}: ${v.why}`);
350
+ }
351
+ };
352
+ sec('합의', by.agreed, false);
353
+ sec('갈림: 양쪽 근거', by.split, true);
354
+ sec('기각 (판정한 모델 모두 반대)', by.rejected, true);
355
+ sec('한 모델만 (다른 모델이 판정하지 못함)', by.single, false);
356
+ L.push('', '## 다음 (리더)', '주장도 주장이다. 발견마다 파일:줄을 직접 열어 확인됨 · 추정 · 기각으로 판정한다. 갈린 것은 양쪽 근거를 놓고 판정하고, 못 하면 사용자에게 선택지로 묻는다. 고치는 것은 사용자가 요청할 때만.',
357
+ `원문: ${path.join(DIR, meta.id)} (round-<회차>-<모델>.md)`);
358
+ return L.join('\n');
359
+ }
360
+
361
+ async function notifyDone(meta, active, endedAt) {
362
+ try {
363
+ const notify = require('../lib/notify');
364
+ const cfg = notify.personal(ROOT, ws);
365
+ const sec = (endedAt - meta.startedAt) / 1000;
366
+ if (!notify.wantsProgress(cfg) || !cfg.token || !cfg.chat || sec < cfg.minSeconds || notify.atDesk(cfg)) return;
367
+ await notify.send(`🤖 [${notify.projectName(ROOT, ws)}] 토론 끝: 모델 ${active.length}/${meta.members.length} · ${(sec / 60).toFixed(1)}분\n결과: node packs/core/bin/debate.js wait ${meta.id}`, cfg);
368
+ } catch { /* 알림은 부가 기능 */ }
369
+ }
370
+
371
+ /** (내부) 위임 작업 하나를 기다려 JSON 한 줄 */
372
+ function waitJob() {
373
+ process.stdout.write(JSON.stringify(delegateWait(opt('job'))));
374
+ }
375
+
376
+ async function wait() {
377
+ const id = argv[1];
378
+ if (!id || !readJson(id, 'meta.json')) throw new Error(`토론 id 가 없습니다: ${id} (목록: node packs/core/bin/debate.js list)`);
379
+ const limit = Number(opt('timeout', 900)) * 1000;
380
+ const t0 = Date.now();
381
+ while (!readJson(id, 'result.json')) {
382
+ if (Date.now() - t0 > limit) { statusOf(id); console.log(`다시: node packs/core/bin/debate.js wait ${id}`); process.exit(2); }
383
+ await new Promise((r) => setTimeout(r, 3000));
384
+ }
385
+ console.log(readJson(id, 'result.json').report);
386
+ }
387
+
388
+ function statusOf(id) {
389
+ const meta = readJson(id, 'meta.json');
390
+ const st = readJson(id, 'state.json') || { round: 0, claims: [], dropped: {} };
391
+ const res = readJson(id, 'result.json');
392
+ const mins = (((res ? res.endedAt : Date.now()) - meta.startedAt) / 60000).toFixed(1);
393
+ console.log(`토론 ${id} · ${res ? '끝' : `${st.round}/${meta.rounds}차 진행 중`} · ${mins}분 · 주장 ${st.claims.length}개`);
394
+ for (const m of meta.members) {
395
+ const done = fs.existsSync(path.join(DIR, id, `round-${st.round}-${m.who}.md`));
396
+ console.log(` ${m.who} ${m.tool}:${m.model}${m.view ? `(${m.view})` : ''} ${st.dropped[m.who] ? `빠짐 (${st.dropped[m.who]})` : res || done ? `${st.round}차 끝` : `${st.round}차 도는 중`}`);
397
+ }
398
+ }
399
+
400
+ function status() {
401
+ const ids = fs.readdirSync(DIR).sort();
402
+ const id = argv[1] || ids.filter((x) => !readJson(x, 'result.json')).slice(-1)[0] || ids.slice(-1)[0];
403
+ if (!id) return console.log('토론 없음');
404
+ statusOf(id);
405
+ }
406
+
407
+ function list() {
408
+ const ids = fs.readdirSync(DIR).sort();
409
+ if (!ids.length) return console.log('토론 없음');
410
+ for (const id of ids) {
411
+ const meta = readJson(id, 'meta.json') || {};
412
+ console.log(`${id} ${readJson(id, 'result.json') ? '끝' : '진행 중'} ${meta.topic || ''} ${(meta.members || []).map((m) => `${m.tool}:${m.model}`).join(' · ')}`);
413
+ }
414
+ }
415
+
416
+ if (MAIN) {
417
+ (async () => {
418
+ try {
419
+ if (cmd === 'models') models();
420
+ else if (cmd === 'start') start();
421
+ else if (cmd === 'run') await run();
422
+ else if (cmd === 'wait') await wait();
423
+ else if (cmd === 'wait-job') waitJob();
424
+ else if (cmd === 'status') status();
425
+ else if (cmd === 'list') list();
426
+ else { console.log(fs.readFileSync(__filename, 'utf8').split('\n').slice(2, 15).map((l) => l.replace(/^ \* ?/, '')).join('\n')); process.exit(cmd ? 1 : 0); }
427
+ } catch (e) { console.error(`✖ ${e.message}`); process.exit(1); }
428
+ })();
429
+ }
430
+
431
+ module.exports = { parseClaims, parseVotes, verdict, parsePanel, suggest };
@@ -5,7 +5,8 @@
5
5
  * codex : codex exec --sandbox <read-only|workspace-write> -C <워크스페이스> [-m] [-c model_reasoning_effort=] -o <결과> -
6
6
  * claude: claude -p [--model] [--effort] --permission-mode <plan|acceptEdits> --allowedTools … --disallowedTools git commit·push
7
7
  * 오래 걸리므로 백그라운드 작업으로 돈다:
8
- * start --to codex|claude --role <역할> [--model m] [--effort e] [--mode write|read] [--scope <라벨>] 작업 지시서는 stdin (또는 --task-file)
8
+ * start --to codex|claude --role <역할> [--model m] [--effort e] [--mode write|read] [--scope <라벨>] [--group <묶음>] 작업 지시서는 stdin (또는 --task-file)
9
+ * --group: 토론(debate.js)처럼 한 번에 여러 모델을 돌리는 묶음. 묶음 안끼리는 동시 상한에 세지 않고 끝 알림도 묶음 단위
9
10
  * 리뷰 역할에 --scope 를 주면 같은 범위의 차수를 센다: 앞 차수 블로커를 지시서에 붙이고, 3차부터 시작할 때 경고한다
10
11
  * → 작업 id 를 출력하고 바로 끝난다
11
12
  * wait <id> [--timeout 초(기본 480)] 끝날 때까지(또는 timeout 까지) 기다린다. 끝났으면 결과(보고 · 바뀐 파일 · 종료 코드), 아니면 경과 시간 · 마지막 로그 한 줄
@@ -183,7 +184,7 @@ function parseJobArgs() {
183
184
  if (!role) throw new Error('--role <역할> 이 필요합니다');
184
185
  const mode = opt('mode', 'write');
185
186
  if (!['write', 'read'].includes(mode)) throw new Error('--mode 는 write | read');
186
- return { to, role, mode, model: opt('model'), effort: opt('effort'), scope: opt('scope') };
187
+ return { to, role, mode, model: opt('model'), effort: opt('effort'), scope: opt('scope'), group: opt('group') };
187
188
  }
188
189
 
189
190
  function readTask() {
@@ -220,7 +221,9 @@ function start() {
220
221
  process.exit(3);
221
222
  }
222
223
  // 동시 상한 — 역할 에이전트 상한(kedovi ai show 의 limits.parallel)을 위임에도 적용한다
223
- const busy = runningJobs().filter((x) => x !== id);
224
+ // 같은 묶음(--group: 토론 한 번)의 작업은 서로 세지 않는다. 묶음 밖 작업만 상한에 센다
225
+ const groupOf = (x) => { try { return JSON.parse(fs.readFileSync(path.join(JOBS, x, 'meta.json'), 'utf8')).group || null; } catch { return null; } };
226
+ const busy = runningJobs().filter((x) => x !== id && !(job.group && groupOf(x) === job.group));
224
227
  let parallel = 2;
225
228
  try { const kit = require(path.join(ROOT, 'bin', 'kit.js')); parallel = kit.resolveModels(ROOT, kit.loadWorkspace(ROOT)).limits.parallel || parallel; } catch { /* 옛 워크스페이스 */ }
226
229
  if (busy.length >= parallel) {
@@ -304,6 +307,7 @@ async function notifyDone(meta, code, endedAt) {
304
307
  const notify = require('../lib/notify');
305
308
  const cfg = notify.personal(ROOT, ws);
306
309
  const sec = (endedAt - meta.startedAt) / 1000;
310
+ if (meta.group) return; // 토론 같은 묶음은 묶음이 끝날 때 한 번 (debate.js)
307
311
  if (!notify.wantsProgress(cfg) || !cfg.token || !cfg.chat || sec < cfg.minSeconds || notify.atDesk(cfg)) return;
308
312
  const mins = (sec / 60).toFixed(1);
309
313
  await notify.send(`🤖 [${notify.projectName(ROOT, ws)}] 위임 ${code === 0 ? '끝' : `실패(${code})`}: ${meta.role} → ${meta.to} · ${mins}분\n결과: node packs/core/bin/delegate.js wait ${meta.id}`, cfg);
@@ -423,6 +427,6 @@ function list() {
423
427
  else if (cmd === 'status') status();
424
428
  else if (cmd === 'watch') await watch();
425
429
  else if (cmd === 'list') list();
426
- else { console.log(fs.readFileSync(__filename, 'utf8').split('\n').slice(2, 16).map((l) => l.replace(/^ \* ?/, '')).join('\n')); process.exit(cmd ? 1 : 0); }
430
+ else { console.log(fs.readFileSync(__filename, 'utf8').split('\n').slice(2, 18).map((l) => l.replace(/^ \* ?/, '')).join('\n')); process.exit(cmd ? 1 : 0); }
427
431
  } catch (e) { console.error(`✖ ${e.message}`); process.exit(1); }
428
432
  })();
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  // 설정 안내 (Claude Code · Codex UserPromptSubmit): 사용자가 불만 · 바람을 말하고("왜 이래" · "이런 거 안 되나" · "매번 …") 주제어가 킷 설정과 맞으면,
3
3
  // (흔한 말 "왜 · 너무 · 할 수 있나" 는 에이전트 · 킷 · 설정을 가리킬 때만) 관련 설정 후보(무엇 · 범위 · 명령)를 에이전트에게 한 줄씩 넣는다. 에이전트는 맞을 때만 안내하고 바꿀지 묻는다 (규칙 core 10-communication).
4
+ // 결과에 대한 불만 · 의심("마음에 안 들어" · "맞아?" · "다시 봐")이나 토론 요청이면 토론 모드(debate 스킬) 제안 후보도 넣는다 (지도의 debate, 같은 세션 30분에 한 번).
4
5
  // 지도: packs/core/settings-map.json. 같은 세션에서 같은 안내는 다시 넣지 않는다 (상태: stateDir 의 <세션>.hints). 출력: 평문 (두 도구 모두 문맥에 더함)
5
6
  'use strict';
6
7
  const fs = require('fs');
@@ -10,6 +11,16 @@ const { stateDir, load } = require('../lib/workspace');
10
11
 
11
12
  const MAX = 2;
12
13
 
14
+ /** 토론 제안: 결과 불만 · 의심(ask) 또는 토론 요청(explicit). 없으면 null */
15
+ function debateHint(prompt, map) {
16
+ const d = map.debate;
17
+ const text = String(prompt || '');
18
+ if (!d || !text) return null;
19
+ if (new RegExp(d.explicit, 'i').test(text)) return 'explicit';
20
+ if (new RegExp(d.ask, 'i').test(text)) return 'ask';
21
+ return null;
22
+ }
23
+
13
24
  /** 프롬프트 → 맞는 안내 항목 (불만 표현이 없으면 빈 목록) */
14
25
  function hints(prompt, map) {
15
26
  const text = String(prompt || '');
@@ -26,14 +37,26 @@ if (require.main === module) {
26
37
  const input = readInput();
27
38
  const R = root();
28
39
  const map = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'settings-map.json'), 'utf8'));
29
- const found = hints(input.prompt || input.user_prompt || '', map);
30
- if (!found.length) return;
40
+ const prompt = input.prompt || input.user_prompt || '';
41
+ const found = hints(prompt, map);
31
42
  const id = safeId(input.session_id);
43
+ const out = [];
44
+ // 토론 제안: 같은 세션에서 again 분 안에는 한 번 (토론을 직접 말했으면 매번)
45
+ const deb = debateHint(prompt, map);
46
+ if (deb) {
47
+ const dfile = id ? path.join(stateDir(R), `${id}.debate`) : null;
48
+ let last = 0;
49
+ try { last = Number(fs.readFileSync(dfile, 'utf8')) || 0; } catch { /* 처음 */ }
50
+ if (deb === 'explicit' || Date.now() - last > (map.debate.again || 30) * 60000) {
51
+ if (dfile && deb === 'ask') try { fs.writeFileSync(dfile, String(Date.now())); } catch { /* 상태는 편의 */ }
52
+ out.push(`토론 모드 안내 (${deb === 'explicit' ? '사용자가 토론 · 다른 모델을 말함' : '결과에 대한 불만 · 의심일 수 있음. 아니면 무시'}): ${map.debate.say}.`);
53
+ }
54
+ }
32
55
  const file = id ? path.join(stateDir(R), `${id}.hints`) : null;
33
56
  let seen = [];
34
57
  try { seen = JSON.parse(fs.readFileSync(file, 'utf8')); } catch { /* 처음 */ }
35
58
  const fresh = found.filter((e) => !seen.includes(e.id)).slice(0, MAX);
36
- if (!fresh.length) return;
59
+ if (!fresh.length) { if (out.length) context(out.join(' ')); return; }
37
60
  if (file) try { fs.writeFileSync(file, JSON.stringify([...seen, ...fresh.map((e) => e.id)])); } catch { /* 상태는 편의 */ }
38
61
  let lead = '';
39
62
  try {
@@ -43,7 +66,8 @@ if (require.main === module) {
43
66
  const leads = T.roster(ws).leads.map(T.callName).filter(Boolean);
44
67
  if (!w.lead && leads.length) lead = ` 팀 범위 설정은 팀장(${leads.join(' · ')})님이 바꾼다.`;
45
68
  } catch { /* 팀 없음 */ }
46
- context(`설정 안내 후보 (사용자 말이 이 설정으로 풀리는 불만일 때만 안내하고 바꿀지 묻는다. 아니면 무시):${fresh.map((e) => ` ${e.say} → \`${e.cmd}\` (${e.scope})`).join(' ·')}.${lead}`);
69
+ out.push(`설정 안내 후보 (사용자 말이 이 설정으로 풀리는 불만일 때만 안내하고 바꿀지 묻는다. 아니면 무시):${fresh.map((e) => ` ${e.say} → \`${e.cmd}\` (${e.scope})`).join(' ·')}.${lead}`);
70
+ context(out.join(' '));
47
71
  });
48
72
  }
49
- module.exports = { hints };
73
+ module.exports = { hints, debateHint };
@@ -551,7 +551,7 @@
551
551
  "profiles": {
552
552
  "rich": {
553
553
  "label": "최고",
554
- "hint": "무제한·엔터프라이즈: 상위 모델 위주, 리뷰·테스트(Claude) 또는 설계·구현(Codex)은 상대 회사 모델에게 위임",
554
+ "hint": "무제한·엔터프라이즈: 상위 모델 위주, 리뷰·테스트(Claude 세션) 또는 설계·구현·리뷰(Codex 세션)는 상대 회사 모델에게 위임",
555
555
  "limits": {
556
556
  "parallel": 3,
557
557
  "total": 8
@@ -613,8 +613,10 @@
613
613
  "fallback": "gpt-6.1-sol"
614
614
  },
615
615
  "reviewer": {
616
- "model": "gpt-6.1-sol",
617
- "effort": "high"
616
+ "tool": "claude",
617
+ "model": "opus",
618
+ "effort": "xhigh",
619
+ "fallback": "gpt-6.1-sol"
618
620
  },
619
621
  "tester": {
620
622
  "model": "gpt-6.1-sol",
@@ -662,7 +664,7 @@
662
664
  },
663
665
  "standard": {
664
666
  "label": "표준",
665
- "hint": "추천 기본: 설계·구현은 Opus, 리뷰는 다른 회사 모델, 테스트는 가성비 모델",
667
+ "hint": "추천 기본: 설계·구현은 Opus, 리뷰는 다른 회사 모델(두 도구 모두), 테스트는 가성비 모델",
666
668
  "limits": {
667
669
  "parallel": 2,
668
670
  "total": 5
@@ -723,8 +725,10 @@
723
725
  "fallback": "gpt-6.1-sol"
724
726
  },
725
727
  "reviewer": {
726
- "model": "gpt-6.1-sol",
727
- "effort": "high"
728
+ "tool": "claude",
729
+ "model": "opus",
730
+ "effort": "high",
731
+ "fallback": "gpt-6.1-sol"
728
732
  },
729
733
  "tester": {
730
734
  "model": "gpt-6-luna",
@@ -769,7 +773,7 @@
769
773
  },
770
774
  "lean": {
771
775
  "label": "절약",
772
- "hint": "사용량이 빠듯할 때: 최상위 모델 없이. Claude 세션은 테스트만 저렴한 Luna 에게 위임",
776
+ "hint": "사용량이 빠듯할 때: 최상위 모델 없이. 리뷰는 다른 회사 모델(Claude 세션은 분석도 GPT-6.1 Sol)",
773
777
  "limits": {
774
778
  "parallel": 1,
775
779
  "total": 3
@@ -827,8 +831,10 @@
827
831
  "effort": "medium"
828
832
  },
829
833
  "reviewer": {
830
- "model": "gpt-6.1-sol",
831
- "effort": "high"
834
+ "tool": "claude",
835
+ "model": "sonnet",
836
+ "effort": "high",
837
+ "fallback": "gpt-6.1-sol"
832
838
  },
833
839
  "tester": {
834
840
  "model": "gpt-6-luna",
@@ -11,6 +11,7 @@ load: always
11
11
  - **언어** — 문서 {{ws:vars.language.docs}}, 코드·커밋·식별자 {{ws:vars.language.code}}. 대화 언어 · 스타일은 개인 지침 머리의 「응답 언어 · 대화 스타일」(kedovi prefs)이 먼저다.
12
12
  - **행동 요청은 누가·무엇을** — `{id}님, {repo}에서 {명령} 실행해 주세요`.
13
13
  - **불만 · 바람은 설정으로 잇는다** — "왜 이래 · 이런 거 안 되나 · 매번 …" 이 킷 설정으로 풀리는 것이면(훅이 「설정 안내 후보」 를 준다) 그 설정 · 범위(이 PC · 팀) · 바꾸는 명령을 한 줄로 알리고 바꿀지 묻는다. 팀 범위면 팀장에게 요청하게 한다. 설정으로 안 풀리는 말에는 꺼내지 않는다.
14
+ - **결과가 미덥다는 말엔 토론을 제안한다** — "마음에 안 들어 · 맞아? · 다시 봐" 처럼 결과를 의심하면 고치기 전에 토론 모드(debate 스킬: 다른 회사 모델 포함 2~3개가 따로 보고 서로 반박)를 사용량과 함께 한 줄로 제안하고 묻는다. 토론을 말하면 바로 debate 스킬.
14
15
  - **결정은 선택지 질문으로** — 2~4개, 권장안을 첫째에 `(권장)` + 이유 한 줄. **권장은 제대로 하는 안이다**: 요구사항을 빠짐없이 채우고 사이드이펙트까지 처리하는 안. 작업량 · 시간이 적다는 것은 권장 이유가 아니다. 더 간단한 안은 보기로 함께 내고, 빠지는 것(못 채우는 요구 · 남는 위험 · 나중에 드는 일)을 적는다. 코드·문서로 확인되는 사실은 묻지 않는다. 보기에 미리보기를 붙이면 모든 보기에 붙이고, 모양을 비교하는 질문이 아니면 붙이지 않는다.
15
16
  - **단계 전환과 진행 방식도 결정이다** — 단계(분석 → 기획 → 설계 → 구축, 작업 안의 큰 단계)를 끝내면 결과 보고와 다음 단계 추천(무엇을·왜·산출물)까지만 하고 멈춘다. 사용자가 다음 단계를 고르면 산출물을 바로 만들지 않고 진행 방식(논의할 항목·순서·산출물 형태)을 제안해 합의한 뒤 그대로 진행한다. 요구사항·범위·설계처럼 논의가 필요한 단계는 초안을 먼저 쓰지 않는다. 단계·영역을 끝낸 보고에는 `node packs/core/bin/plan.js` 출력(로드맵 흐름 · 지금 단계 표 · 이번에 생긴 것 · 다음)을 그대로 붙이고, `node packs/core/bin/tidy.js --count --new` 가 후보를 내면 정리할지 묻는다. 진행은 퍼센트가 아니라 `완료 n / 전체 N`, 세지 않은 숫자는 쓰지 않는다.
16
17
  - **결정 질문은 나눠 묻는다** — 앞 답에 따라 뒤 질문이 달라지면 순서대로 하나씩. 몰아서 내지 않는다. 여러 개면 제목 끝에 `주제 n/N`, 한 주제 안의 하위 질문은 `(m/k)` 를 붙인다.
@@ -198,5 +198,12 @@
198
198
  }
199
199
  ],
200
200
  "weak": "왜|너무|할\\s*수\\s*(?:있|없)|안\\s*되나|안되나|못\\s*해|못\\s*하나|안\\s*해|없나|있나\\?|가능(?:해|한가|할까)|이상해",
201
- "about": "너|네가|니가|에이전트|ai\\b|kedovi|킷|설정|답변|대답|답이|진행\\s*알림|말투|알림|워크스페이스|훅"
201
+ "about": "너|네가|니가|에이전트|ai\\b|kedovi|킷|설정|답변|대답|답이|진행\\s*알림|말투|알림|워크스페이스|훅",
202
+ "debate": {
203
+ "_note": "결과에 대한 불만 · 의심(ask) 이나 토론 요청(explicit) 이면 토론 모드(debate 스킬)를 제안하게 한다. 같은 세션에서 again 분 안에는 다시 넣지 않는다",
204
+ "ask": "마음에\\s*안\\s*들|맘에\\s*안\\s*들|별로(?:야|네|다|인데|임)|이상한데|이상하네|틀린\\s*(?:거|것)\\s*같|틀렸|맞(?:아|나|는\\s*거야|는지\\s*모르)\\s*\\?|맞는\\s*건가|확실해\\s*\\?|납득(?:이)?\\s*안|설득력|부족해|대충|엉망|다시\\s*(?:봐|검토|생각|확인)|아닌\\s*(?:거|것)\\s*같|못\\s*믿|의심",
205
+ "explicit": "토론|여러\\s*모델|다른\\s*모델(?:들)?(?:로|한테|에게|도|의)|교차\\s*(?:검토|리뷰)|두\\s*번째\\s*의견",
206
+ "again": 30,
207
+ "say": "결과가 미덥지 않으면 토론 모드(다른 회사 모델 포함 2~3개가 따로 보고 서로 반박, 평소의 몇 배 사용량)를 한 줄로 제안하고 물음. 토론을 말했으면 바로 debate 스킬"
208
+ }
202
209
  }
@@ -0,0 +1,67 @@
1
+ ---
2
+ name: debate
3
+ description: 토론 모드. 여러 모델(다른 회사 모델 포함 2~4개)이 같은 문제를 따로 보고, 이름을 가린 주장 목록에 서로 동의 · 반대해 갈린 것만 다시 본다. 코드 리뷰 · 설계 · 요구 · 문서 · 원인 분석 어떤 작업이든. "토론해줘", "토론 모드", "여러 모델로 봐줘", "다른 모델들한테도", 또는 결과가 마음에 안 든다 · 맞는지 모르겠다는 말에 쓴다.
4
+ ---
5
+
6
+ # debate: 여러 모델 토론
7
+
8
+ 평소 작업은 역할 모델 하나(리뷰는 다른 회사 모델 하나)로 한다. 토론은 **요청할 때만** 돈다. 모델 수 × 회차만큼 비용 · 시간이 든다 (보통 평소 리뷰의 5~7배).
9
+
10
+ > 근거: 같은 모델끼리의 토론은 다수결로 끝나 효과가 작고, 다른 회사 모델을 섞을 때 정확도가 오른다 (arXiv 2502.08788 · 2410.12853 · 2601.05746).
11
+
12
+ ## 1. 언제 제안하는가
13
+
14
+ - 사용자가 토론 · 여러 모델 · 다른 모델 의견을 말하면 바로 2절로.
15
+ - 결과가 마음에 안 든다 · 맞는지 모르겠다 · 다시 봐 달라는 뉘앙스면 **고치기 전에** 한 줄로 제안하고 묻는다. 「다른 모델 2~3개가 따로 보고 서로 반박하는 토론으로 확인할까요? (약 N분 · 평소의 몇 배 사용량)」
16
+ 사용자가 원하지 않으면 평소대로 고친다. 같은 대화에서 거절했으면 다시 권하지 않는다.
17
+ - 쓰지 않는 때: 사실 하나 확인 · 명령 실행 결과처럼 답이 정해진 것, 사용자가 빨리 끝내길 원할 때.
18
+
19
+ ## 2. 패널 고르기: 쓸 수 있는 모델에서 작업에 맞게
20
+
21
+ ```bash
22
+ node packs/core/bin/debate.js models --task code # code | design | docs
23
+ ```
24
+
25
+ 설치된 CLI · 이 PC 설정(kedovi ai) · 로그인 요금제로 쓸 수 있는 모델과 강점 점수(LiveBench), 패널 예시가 나온다. 리더(지금 세션)가 작업을 보고 고른다.
26
+
27
+ | 기준 | 내용 |
28
+ |---|---|
29
+ | 다른 회사 | 꼭 하나 이상 넣는다. 다른 회사 CLI 가 없으면 같은 회사 다른 모델로 하되 그 한계를 사용자에게 알린다 |
30
+ | 작업 강점 | 코드 = 에이전트 코딩 · 코딩, 설계 · 원인 분석 = 추론 · 데이터 분석, 기획 · 문서 = 언어 · 지시 따르기 |
31
+ | 리더와 다르게 | 리더 모델과 같은 모델은 되도록 빼거나 관점을 달리 준다 |
32
+ | 비용 | 가장 비싼 급(API $10 이상)은 하나만. 요금제가 빠듯하면 2개 · 낮은 effort |
33
+ | 관점 (선택) | 범위가 넓으면 모델마다 관점을 나눈다: 정확성 · 보안 · 성능 · 사용자 흐름 · 반대 역할(일부러 약점을 찾는다) |
34
+
35
+ 역할은 작업에 맞춘다: 코드 리뷰 `reviewer`, 설계 `architect`, 요구 · 기획 `analyst`. 패널과 예상 시간 · 사용량을 한 줄로 알리고 확인받은 뒤 시작한다.
36
+
37
+ ## 3. 실행
38
+
39
+ ```bash
40
+ node packs/core/bin/debate.js start --panel codex:gpt-6.1-sol:high,claude:opus:high:보안 --role reviewer --topic "주문 탭" <<'EOF'
41
+ <작업 지시서: 무엇을 · 어디를(repo · 파일 · 문서 ID) · 무엇이 쟁점인지 · 검사: <명령> 통과>
42
+ EOF
43
+ node packs/core/bin/debate.js wait <토론 id> # 오래 걸리면 다시 wait. 진행: status <id>
44
+ ```
45
+
46
+ - 지시서가 전부다. 패널은 대화를 모른다. 리뷰 역할은 `검사:` 줄이 필요하다 (리뷰 전 검사).
47
+ - 회차: 1차 따로 보기 → 2차 이름을 가린 주장 목록에 동의 · 반대(근거 필수, 근거 없는 동의는 보류) + 놓친 것 → 3차부터 갈린 주장만. 모두 판정이 맞으면 일찍 끝난다. `--rounds` 로 최대 회차(기본 3).
48
+ - 모델이 실패하면(사용량 한도 · 로그인) 다른 모델로 몰래 바꾸지 않고 「빠진 모델」로 적는다. 2개 미만이 되면 멈춘다.
49
+ - 읽기 전용이다. 패널은 파일을 고치지 않는다.
50
+
51
+ ## 4. 결과 맞추기: 다수결로 받아쓰지 않는다
52
+
53
+ 결과는 합의 · 갈림 · 기각 · 한 모델만으로 나온다. 모두 **주장**이다.
54
+
55
+ 1. 합의도 블로커는 파일:줄을 직접 열어 확인한다 (모델들이 같은 착각을 할 수 있다).
56
+ 2. 갈림은 양쪽 근거를 나란히 놓고 직접 확인해 판정한다. 판정이 안 되면 사용자에게 선택지로 묻는다.
57
+ 3. 한 모델만 낸 것도 버리지 않는다. 확인되면 가장 쓸모 있는 발견이다.
58
+ 4. 고치는 것은 사용자가 요청할 때만.
59
+
60
+ ## 5. 보고
61
+
62
+ ```
63
+ 결론: 토론(모델 N개 · M회 · K분) 주장 X개: 합의 A · 갈림 B · 기각 C · 한 모델만 D / 내가 확인: 확인됨 E · 추정 F · 기각 G
64
+ | # | 주장 | 토론 결과 (동의 · 반대 모델) | 내 판정 | 근거 |
65
+ 갈린 것: <주장: 양쪽 근거 · 결정 필요 여부>
66
+ 빠진 모델: <없음 | 모델 · 이유>
67
+ ```
@@ -78,6 +78,7 @@ description: 워크스페이스 설정을 바꾸거나 확인하는 요청을 ke
78
78
  | Codex 웹 검색 끄기 · 켜기 | `workspace.json` 의 `agents.webSearch` 를 `false` 로 (지우면 켬. `kedovi start` 가 Codex 에 `--search` 를 붙인다) | 팀 |
79
79
  | 워크스페이스 현황판 | `kedovi board` (로드맵 · 팀원별 마지막 핸드오프 · 미완 작업 · 공지 + 기록 쪽을 `.agents/board/` 에 만들어 브라우저로 연다. 보기 전용) | 이 PC |
80
80
  | 다른 도구에 맡긴 작업(위임) 진행 보기 | `kedovi delegate watch [작업 id]` (상대 에이전트가 읽고 실행하는 것이 줄마다) · `kedovi delegate status [작업 id]` (지금 현황 한 번) · `kedovi delegate list` | 워크스페이스 |
81
+ | 토론 모드 보기 · 쓸 수 있는 모델 | `kedovi debate list` · `kedovi debate status [토론 id]` · `kedovi debate models [--task code\|design\|docs]` (토론 시작은 debate 스킬: "토론해줘") | 워크스페이스 |
81
82
  | 문서 사이트를 이 PC 에서 웹 주소로 보기 | `kedovi docs serve [repo]` (`http://127.0.0.1:8000/`, `--open` 이면 브라우저도 연다. 끝내기 Ctrl+C) | 기획 repo |
82
83
  | 문서 사이트 메뉴 묶음 · 이름 · 버전 바꾸기 | 기획 repo 의 `docs-site.json` 을 고치고 `kedovi docs build` (README 의 「문서 사이트」 절 형식) | 기획 repo |
83
84
  | 기획 문서 ID 하나 보기 · 쓰는 곳 찾기 | `kedovi docs show <ID> [--in <파일 조각>] [--all]` (정의 부분만) · `kedovi docs refs <ID> [--docs]` (기획 · 코드 · 이슈 repo 의 파일:줄). "FN-001 내용" · "이 결정 어디서 써?" 같은 말에 쓴다 | 보기 |