@tienne/gestalt 0.65.0 → 0.66.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.
Files changed (48) hide show
  1. package/CLAUDE.md +1 -1
  2. package/dist/package.json +1 -1
  3. package/dist/plugin/review-agents/quality-reviewer/AGENT.md +14 -1
  4. package/dist/plugin/role-agents/_shared/references/README.md +5 -4
  5. package/dist/plugin/role-agents/_shared/references/truncation-rules.md +104 -0
  6. package/dist/plugin/skills/blast-radius/SKILL.md +12 -1
  7. package/dist/plugin/skills/build-graph/SKILL.md +11 -1
  8. package/dist/plugin/skills/diff-radius/SKILL.md +7 -0
  9. package/dist/plugin/skills/jira-create/SKILL.md +7 -1
  10. package/dist/plugin/skills/review-reply/SKILL.md +19 -5
  11. package/dist/plugin/skills/slack-send/SKILL.md +8 -1
  12. package/dist/src/cli/commands/status.d.ts.map +1 -1
  13. package/dist/src/cli/commands/status.js +17 -3
  14. package/dist/src/cli/commands/status.js.map +1 -1
  15. package/dist/src/code-graph/blast-radius.d.ts.map +1 -1
  16. package/dist/src/code-graph/blast-radius.js +20 -4
  17. package/dist/src/code-graph/blast-radius.js.map +1 -1
  18. package/dist/src/code-graph/engine.d.ts.map +1 -1
  19. package/dist/src/code-graph/engine.js +17 -2
  20. package/dist/src/code-graph/engine.js.map +1 -1
  21. package/dist/src/code-graph/types.d.ts +18 -0
  22. package/dist/src/code-graph/types.d.ts.map +1 -1
  23. package/dist/src/execute/orchestrators/execution.d.ts.map +1 -1
  24. package/dist/src/execute/orchestrators/execution.js +2 -1
  25. package/dist/src/execute/orchestrators/execution.js.map +1 -1
  26. package/dist/src/execute/prompts.d.ts.map +1 -1
  27. package/dist/src/execute/prompts.js +28 -5
  28. package/dist/src/execute/prompts.js.map +1 -1
  29. package/dist/src/llm/adapter.d.ts.map +1 -1
  30. package/dist/src/llm/adapter.js +7 -0
  31. package/dist/src/llm/adapter.js.map +1 -1
  32. package/dist/src/llm/openai-adapter.d.ts.map +1 -1
  33. package/dist/src/llm/openai-adapter.js +6 -0
  34. package/dist/src/llm/openai-adapter.js.map +1 -1
  35. package/dist/src/mcp/tools/code-graph-passthrough.d.ts.map +1 -1
  36. package/dist/src/mcp/tools/code-graph-passthrough.js +4 -0
  37. package/dist/src/mcp/tools/code-graph-passthrough.js.map +1 -1
  38. package/package.json +1 -1
  39. package/plugin/.codex-plugin/plugin.json +1 -1
  40. package/plugin/review-agents/quality-reviewer/AGENT.md +14 -1
  41. package/plugin/role-agents/_shared/references/README.md +5 -4
  42. package/plugin/role-agents/_shared/references/truncation-rules.md +104 -0
  43. package/plugin/skills/blast-radius/SKILL.md +12 -1
  44. package/plugin/skills/build-graph/SKILL.md +11 -1
  45. package/plugin/skills/diff-radius/SKILL.md +7 -0
  46. package/plugin/skills/jira-create/SKILL.md +7 -1
  47. package/plugin/skills/review-reply/SKILL.md +19 -5
  48. package/plugin/skills/slack-send/SKILL.md +8 -1
@@ -0,0 +1,104 @@
1
+ # 잘린 결과 룰북
2
+
3
+ 리뷰 파이프라인이 **일부만 가져와놓고 전부인 것처럼 돌려주는 코드**를 검토할 때 쓰는 룰북이다.
4
+ 룰 원본은 이 문서이며 에이전트 문서에 룰을 복사하지 않는다. 복사해 두면 사본이 조용히 갈라진다.
5
+
6
+ 자르는 것 자체는 문제가 아니다. 응답 크기든 프롬프트 예산이든 상한은 있어야 한다. 문제는
7
+ 잘라놓고 잘렸다는 걸 안 알리는 것이다. 받는 쪽은 그게 전부인 줄 알고 판정하는데, 그 판정은
8
+ 겉보기에 정상이라 사람도 못 잡는다.
9
+
10
+ 이 레포에서만 같은 모양이 여덟 자리에서 나왔다(`47b2990`, `aa2ad05`, `0f5c25f`). 고친 뒤에
11
+ 룰로 내린다.
12
+
13
+ ## 무엇을 대상으로 보는가
14
+
15
+ - diff에서 **추가되거나 수정된 라인**을 본다. 손대지 않은 기존 코드는 대상이 아니다
16
+ - 외부 목록을 받아오거나(`search`, `list`, 페이지네이션 API), 개수를 세거나, 결과를 잘라
17
+ 다른 쪽에 넘기는 코드가 후보다
18
+ - 스킬 문서(`SKILL.md`)도 대상이다. 도구를 몇 건 받아오라고 적는 자리가 코드와 같은 결함을 만든다
19
+
20
+ ## Do-NOT (검토 제외)
21
+
22
+ - **개수는 따로 싣고 목록만 앞 N개 보내는 코드.** 잘린 사실이 이미 드러나 있다
23
+ - 상한을 넘기면 에러를 던지는 코드. 조용하지 않으니 대상이 아니다
24
+ - 화면과 로그의 미리보기. 사람이 더 있다는 걸 아는 자리다
25
+ - 테스트 픽스처와 예시 코드
26
+ - 룰 문서 안의 예시. 예시로 적힌 나쁜 코드를 실제 위반으로 세지 않는다
27
+
28
+ ## 심각도
29
+
30
+ 리뷰 파이프라인 스키마를 따른다(`critical` | `high` | `warning`). AI-tell 룰북의 S1, S2와는
31
+ 다른 기준이므로 섞지 않는다.
32
+
33
+ - `critical` — 잘린 값이 그럴듯한 정상값으로 둔갑한다. 후보 1건, 위험도 0.23, 해상도 0.00처럼
34
+ 화면에 멀쩡히 찍히는 숫자로 나오거나, 그 값으로 되돌릴 수 없는 동작(발송, 생성)이 나간다
35
+ - `high` — 사용자가 잘렸다는 걸 알 방법이 없다
36
+ - `warning` — 잘린 사실이 결과에는 담겼는데 사람이 보는 자리까지 안 올라온다
37
+
38
+ ## A. 가져올 때
39
+
40
+ | ID | 패턴 | 심각도 | 처방 |
41
+ |---|---|---|---|
42
+ | TR-1 | 페이지네이션을 안 돌고 첫 페이지를 전부로 취급 — 돌려받은 건수를 그대로 후보 수로 센다 | critical | 돌려받은 건수가 요청한 페이지 크기와 같으면 더 있다고 본다. 0건, 1건, 다수 판정은 전부 받은 뒤에 한다 |
43
+ | TR-2 | 정렬 방향과 자르는 방향이 어긋나 판단 근거가 있는 쪽을 버린다 | high | 무엇을 보고 판정하는지 먼저 정하고 그게 남는 쪽을 남긴다. 실패 상세는 출력 끝에 있고, 마지막 작성자는 목록 끝에 있다 |
44
+
45
+ TR-1은 상한이 낮을수록 잘 걸린다. `slack_search_channels`는 20건, `getVisibleJiraProjects`는
46
+ 50건, GitHub REST 기본 페이지는 30건이다. 워크스페이스에 비슷한 이름이 많으면 첫 페이지가
47
+ 엉뚱한 것으로만 채워진다.
48
+
49
+ TR-2는 상한을 올려도 안 없어진다. 방향이 틀린 것이라서 100건을 200건으로 늘려도 버리는 쪽은
50
+ 그대로다.
51
+
52
+ ## B. 셀 때
53
+
54
+ | ID | 패턴 | 심각도 | 처방 |
55
+ |---|---|---|---|
56
+ | TR-3 | 잘린 개수를 전체 개수로 보고 — `total: items.length` | high | `shown`과 `truncated`로 나눠 적는다. 한 건 더 요청하면 초과분이 있는지 알 수 있다 |
57
+ | TR-4 | 분자만 잘리고 분모는 전체인 비율 | critical | 비율을 낼 때 양쪽이 같은 범위인지 본다. 아니면 비율을 내지 말고 왜 못 내는지 적는다 |
58
+ | TR-5 | 상한에 닿았다는 사실이 결과 타입에 없다 | high | `truncated`, `depthExhausted`, 남은 커서처럼 **필드로** 담는다. 문자열 안에 적으면 받는 쪽이 못 읽는다 |
59
+
60
+ TR-4가 제일 나쁘다. 위험도, 커버리지, 진행률처럼 사람이 한 숫자만 보고 판단하는 값이 여기서
61
+ 나온다. 분자가 깊이 2에서 잘리고 분모가 그래프 전체면 위험도는 구조적으로 낮게 나온다.
62
+ 사용자는 "위험도 0.23 낮음"을 완전한 답으로 읽는다. 사이드 이펙트를 놓치지 않으려고 쓰는
63
+ 도구가 놓친 걸 놓친 줄 모르게 만드는 방향으로 틀린다.
64
+
65
+ ## C. 버릴 때
66
+
67
+ | ID | 패턴 | 심각도 | 처방 |
68
+ |---|---|---|---|
69
+ | TR-6 | 빈 `catch`로 항목을 버린다 | high | 사유와 함께 모아 결과에 싣는다. 여기 빠진 것은 상한과 달리 파라미터를 올려도 안 되살아난다 |
70
+ | TR-7 | 잘린 응답이 파싱에 실패해 조용한 기본값으로 떨어진다 | critical | 절단을 먼저 감지해 원인과 대처를 담아 던진다. 기본값은 정상값과 구분이 안 된다 |
71
+ | TR-8 | 잘린 사실이 결과에는 있는데 요약에 안 올라온다 | warning | 요약 첫 줄에 올린다. 사람은 요약만 읽고 판단하는 일이 잦다 |
72
+
73
+ TR-7은 재시도로 덮으려 하지 않는다. 같은 프롬프트는 같은 데서 잘리므로 재시도는 헛돈다.
74
+
75
+ ## D. 처방 원칙
76
+
77
+ - **자르지 말라고 하지 않는다.** 상한은 필요하다. 잘렸다는 사실이 결과 타입에 들어가게 한다
78
+ - 표시는 **필드로** 낸다. 사람이 읽는 문장에만 적으면 다음 단계 코드는 그대로 전부인 줄 안다
79
+ - 잘린 목록을 그대로 두고 개수만 붙이는 것도 답이다. 전량을 실으면 응답만 커진다
80
+ - 페이지를 끝까지 못 받았으면 그 사실을 사용자에게 알리고 확인을 받는다. 부분 목록으로 조용히
81
+ 확정하지 않는다
82
+ - 외부로 나가는 동작(메시지 발송, 티켓 생성, 커밋)이 뒤에 있으면 심각도를 한 단계 올린다.
83
+ 되돌릴 수 없어서다
84
+
85
+ ## 출력
86
+
87
+ `category`는 `quality:truncation`을 쓰고 `message` 앞에 룰 ID를 적는다.
88
+
89
+ ```
90
+ TR-3 잘린 개수를 전체로 적고 있어요 — 세션이 150개여도 total은 100으로 찍힙니다.
91
+ ```
92
+
93
+ ID를 안 적으면 리포트에서 어느 룰로 걸렸는지 추적이 끊긴다.
94
+
95
+ ## 룰을 추가할 때
96
+
97
+ - 번호는 `TR-` 연번으로 이어 붙인다. AI-tell 룰북의 `A-1`~`J-3`, 주석 룰북의 `CM-` ID와
98
+ 섞지 않는다 — `verify:rules`는 AI-tell ID만 룰북 정합 대상으로 본다
99
+ - 추가한 뒤 `pnpm verify:rules`를 돌린다. 이 문서도 `plugin/role-agents/` 아래에 두므로
100
+ 금지 표현 검사와 S1 어투 검사를 함께 받는다
101
+ - 정규식으로 밀지 않는다. `slice(0, N)` 하나로는 잘못 잡는 게 너무 많다. 무엇을 보고
102
+ 판정하는지 알아야 걸리는 룰이라 모델 쪽에 남긴다
103
+
104
+ 주 참조자는 `quality-reviewer`다.
@@ -103,9 +103,13 @@ ges_code_graph {
103
103
  |------|------|
104
104
  | `changedFiles` | 변경된 파일 목록 |
105
105
  | `impactedFiles` | 영향받는 파일 목록 (테스트 파일 우선 정렬) |
106
- | `riskScore` | 위험도 점수 0~1 (전체 대비 영향 노드 비율) |
106
+ | `riskScore` | 위험도 점수 0~1 (전체 대비 영향 노드 비율). `depthExhausted`면 하한이다 |
107
+ | `depthExhausted` | `maxDepth`에 걸려 탐색이 멈췄고 갈 곳이 남아 있었다 |
108
+ | `unexploredNodes` | 그때 다음 홉에서 기다리던 노드 수 |
107
109
  | `summary` | 한 줄 요약 |
108
110
 
111
+ **`depthExhausted: true`면 결과는 전부가 아니라 하한이다.** 기본 `maxDepth`가 2라 3홉 이상 떨어진 호출부는 목록에 없다. 이걸 안 알리면 사용자는 "영향받는 파일 12개, 위험도 낮음"을 완전한 답으로 읽고 나머지를 안 읽는다 — 이 스킬을 쓰는 이유가 사이드 이펙트를 놓치지 않으려는 것이므로 그 오해가 가장 비싸다.
112
+
109
113
  ## Skill Instructions
110
114
 
111
115
  1. `repoRoot`가 주어지지 않으면 현재 작업 디렉토리를 절대 경로로 사용합니다.
@@ -131,6 +135,13 @@ ges_code_graph {
131
135
  **요약**: {summary}
132
136
  ```
133
137
 
138
+ `depthExhausted: true`면 위 표시 바로 아래에 한 줄을 덧붙입니다. 빠뜨리지 않습니다.
139
+
140
+ ```
141
+ ⚠️ 깊이 {maxDepthUsed}에서 탐색이 멈췄고 {unexploredNodes}개 노드가 남았습니다.
142
+ 위 목록과 위험도는 하한이며 전부가 아닙니다. 전체를 보려면 maxDepth를 올려 다시 부르세요.
143
+ ```
144
+
134
145
  5. `impactedFiles` 목록을 컨텍스트로 활용합니다:
135
146
  - "아래 파일들이 영향을 받을 수 있습니다. 관련 작업 전 이 파일들을 먼저 읽어보겠습니다:" 형식으로 안내
136
147
  - 파일이 많으면 (10개 이상) 가장 중요한 파일(테스트 파일, 핵심 모듈)을 우선 읽도록 제안
@@ -93,11 +93,21 @@ ges_code_graph {
93
93
  | `nodesBuilt` | 인덱싱된 노드 수 (파일·함수·클래스·타입) |
94
94
  | `edgesBuilt` | 인덱싱된 엣지 수 (호출·임포트·상속·포함 관계) |
95
95
  | `timeTakenMs` | 소요 시간 (밀리초) |
96
+ | `skippedCount` | 읽거나 파싱하지 못해 그래프에서 빠진 파일 수 |
97
+ | `skippedFiles` | 그중 앞 20개의 경로와 사유 (진단용) |
98
+
99
+ **`skippedCount`가 0이 아니면 반드시 사용자에게 알린다.** 이 파일들은 그래프에 없으므로 이후 `/blast-radius`가 영향 범위에서 영영 누락한다. 깊이 상한과 달리 파라미터를 올려도 되살아나지 않는다 — 파싱이 실패한 원인을 고쳐 다시 빌드하는 수밖에 없다.
96
100
 
97
101
  ## Skill Instructions
98
102
 
99
103
  1. `repoRoot`가 주어지지 않으면 현재 작업 디렉토리(`cwd`)를 절대 경로로 사용합니다.
100
104
  2. `ges_code_graph { action: "build", repoRoot: "<repoRoot>", mode: "<mode>" }`를 호출합니다.
101
- 3. 빌드 결과를 사용자에게 표시합니다.
105
+ 3. 빌드 결과를 사용자에게 표시합니다. `skippedCount`가 0이 아니면 아래 줄을 빠뜨리지 않고 덧붙입니다.
106
+
107
+ ```
108
+ ⚠️ {skippedCount}개 파일이 그래프에서 빠졌습니다 (읽기·파싱 실패).
109
+ 이 파일들은 영향범위 분석에 잡히지 않습니다: {skippedFiles의 경로와 사유}
110
+ ```
111
+
102
112
  4. "그래프 빌드 완료! 이제 `/blast-radius`로 변경 영향 파일을 분석할 수 있습니다." 안내를 포함합니다.
103
113
  5. 오류가 발생하면 오류 내용을 표시하고 지원 언어인지 확인하도록 안내합니다.
@@ -124,5 +124,12 @@ ges_code_graph {
124
124
  **요약**: {summary}
125
125
  ```
126
126
 
127
+ `depthExhausted: true`면 위 표시 바로 아래에 한 줄을 덧붙입니다. 빠뜨리지 않습니다.
128
+
129
+ ```
130
+ ⚠️ 깊이 {maxDepthUsed}에서 탐색이 멈췄고 {unexploredNodes}개 노드가 남았습니다.
131
+ 위 목록과 위험도는 하한이며 전부가 아닙니다. 전체를 보려면 maxDepth를 올려 다시 부르세요.
132
+ ```
133
+
127
134
  5. `impactedFiles` 목록을 컨텍스트로 활용합니다.
128
135
  6. 변경된 파일이 없으면 "현재 미커밋 변경이 없습니다." 안내합니다.
@@ -69,7 +69,13 @@ Atlassian MCP로 시스템 값을 확정한다. 추측 금지.
69
69
 
70
70
  1. **cloudId**: `getAccessibleAtlassianResources`로 접근 가능한 사이트 확인. 여러 개면 사용자에게 어느 사이트인지 확인.
71
71
  2. **프로젝트**: `getVisibleJiraProjects`로 후보 조회. 입력에 프로젝트키가 없거나 여러 개 매칭되면 **후보를 보여주고 사용자가 선택**하게 한다(오생성 1순위 원인).
72
+ - `searchString`으로 먼저 좁힌다. 사이트에 프로젝트가 수백 개인 조직이 흔하다.
73
+ - `maxResults`는 상한인 `50`으로 준다. 기본값도 50이지만 명시해야 아래 판정 기준이 선다.
74
+ - **돌려받은 건수가 `maxResults`와 같으면 더 있다고 본다.** `startAt`을 50씩 올려 재호출한다. 건수가 50보다 적게 올 때까지 반복한다.
75
+ - **후보 수는 페이지를 다 받은 뒤에 센다.** 첫 페이지만 보고 "1건"이라 확정하면, 실제로는 두 번째 페이지에 있던 진짜 대상 대신 엉뚱한 프로젝트에 티켓을 만든다. 승인 화면에는 그 프로젝트가 정상으로 보이니 사용자도 못 잡는다.
72
76
  3. **이슈타입 + 필수필드**: `getJiraProjectIssueTypesMetadata`로 해당 프로젝트가 지원하는 이슈타입 확인 → `getJiraIssueTypeMetaWithFields`로 **필수 필드**를 확인한다. 프로젝트마다 필수 커스텀 필드(컴포넌트, 스프린트, Epic Link 등)가 다르므로 required 필드가 비면 사용자에게 물어 채운다.
77
+ - `getJiraProjectIssueTypesMetadata`는 `maxResults`를 200까지 받는다. 이슈타입이 50개를 넘는 프로젝트는 드무니 `maxResults: 200`으로 한 번에 받는다. 그래도 200이 꽉 차면 `startAt`으로 이어 받는다.
78
+ - 이슈타입이 잘려서 원하는 타입이 목록에 없으면 "지원하지 않는 타입"으로 잘못 판단하게 된다. 없다고 결론짓기 전에 페이지를 다 받았는지 확인한다.
73
79
  4. **담당자**(선택): 지정 요청이 있으면 `lookupJiraAccountId`로 accountId 확정.
74
80
 
75
81
  ### 4. 미리보기 + 승인 단계 (필수)
@@ -117,7 +123,7 @@ Atlassian MCP로 시스템 값을 확정한다. 추측 금지.
117
123
 
118
124
  | 상황 | 대응 |
119
125
  |------|------|
120
- | 프로젝트 조회 0건/다수 | 후보 보여주고 사용자에게 선택 요청 |
126
+ | 프로젝트 조회 0건/다수 | 후보 보여주고 사용자에게 선택 요청 (**페이지를 다 받은 뒤에 센다** — 3단계 참조) |
121
127
  | 필수 필드 누락 | 어떤 필드가 필요한지 알리고 값 확인 |
122
128
  | 이슈타입 미지원(프로젝트에 없음) | 지원 타입 목록 보여주고 재선택 |
123
129
  | 생성 실패(권한/필드 검증) | Atlassian 에러 메시지 그대로 보고, 재시도 여부 확인 |
@@ -81,11 +81,13 @@ gh api user --jq .login
81
81
  REST(`pulls/{n}/comments`)는 resolved 여부를 주지 않으므로 **GraphQL로 조회**한다. 이미 닫힌 스레드에 답글을 다시 붙이지 않으려면 이 단계가 필요하다.
82
82
 
83
83
  ```bash
84
- gh api graphql -F owner='<owner>' -F repo='<repo>' -F number=<number> -f query='
85
- query($owner:String!, $repo:String!, $number:Int!) {
84
+ gh api graphql --paginate \
85
+ -F owner='<owner>' -F repo='<repo>' -F number=<number> -f query='
86
+ query($owner:String!, $repo:String!, $number:Int!, $endCursor:String) {
86
87
  repository(owner:$owner, name:$repo) {
87
88
  pullRequest(number:$number) {
88
- reviewThreads(first:100) {
89
+ reviewThreads(first:100, after:$endCursor) {
90
+ pageInfo { hasNextPage endCursor }
89
91
  nodes {
90
92
  id
91
93
  isResolved
@@ -93,7 +95,8 @@ query($owner:String!, $repo:String!, $number:Int!) {
93
95
  path
94
96
  line
95
97
  originalLine
96
- comments(first:30) {
98
+ comments(last:100) {
99
+ totalCount
97
100
  nodes { databaseId author { login } body createdAt }
98
101
  }
99
102
  }
@@ -103,6 +106,12 @@ query($owner:String!, $repo:String!, $number:Int!) {
103
106
  }'
104
107
  ```
105
108
 
109
+ **`comments`는 `first`가 아니라 `last`다.** 아래 필터가 스레드의 *마지막* 코멘트 작성자를 보는데 `first`는 가장 오래된 것부터 준다. 왕복이 상한을 넘은 스레드에서 중간 코멘트를 마지막으로 착각하면 판정이 양쪽으로 뒤집힌다 — 리뷰어가 기다리는 질문을 닫힌 걸로 보고 건너뛰거나, 이미 답한 스레드에 또 답글을 단다.
110
+
111
+ **스레드는 `--paginate`로 전부 받는다.** AI 리뷰어가 붙으면 라인마다 스레드가 생겨 한 PR에 100개를 넘기는 일이 흔하다. 커서를 안 돌리면 101번째부터 조용히 사라지고 그 리뷰어들은 답을 못 받는다. 잘렸다는 사실조차 안 보이는 게 이 실패의 고약한 점이다.
112
+
113
+ `first`와 `last`는 100이 GitHub 상한이다(101을 넘기면 `EXCESSIVE_PAGINATION`). 얕은 스레드는 있는 만큼만 오므로 100으로 잡아도 손해가 없다. `comments.totalCount`가 100을 넘는 스레드는 앞부분이 안 실려 온 것이니, 답글에 맥락이 필요하면 그 스레드만 `comments(first:100)`으로 다시 받는다.
114
+
106
115
  수집한 스레드를 아래 기준으로 걸러 `openThreads`를 만든다.
107
116
 
108
117
  - `isResolved: true` → 제외 (이미 닫힘)
@@ -113,11 +122,16 @@ query($owner:String!, $repo:String!, $number:Int!) {
113
122
  PR 전반 코멘트(라인에 안 붙은 것)도 함께 모은다. 스레드 개념이 없어 답글 API가 다르다.
114
123
 
115
124
  ```bash
116
- gh api repos/<owner>/<repo>/issues/<number>/comments --jq '.[] | {id, user: .user.login, body}'
125
+ gh api --paginate 'repos/<owner>/<repo>/issues/<number>/comments?per_page=100' \
126
+ --jq '.[] | {id, user: .user.login, body}'
117
127
  ```
118
128
 
129
+ 여기도 `--paginate`가 필요하다. REST 기본 페이지가 30건이라 그냥 부르면 31번째부터 잘린다.
130
+
119
131
  수집 결과를 한 줄로 알린다: **"미해결 스레드 N건, PR 전반 코멘트 M건을 찾았어요."** 0건이면 여기서 끝낸다 ("답할 코멘트가 없네요").
120
132
 
133
+ 세는 값은 페이지를 전부 받은 뒤의 총계여야 한다. 한 페이지만 보고 "100건"이라고 알리면 사용자는 그게 실제 개수인지 잘린 값인지 알 수 없다.
134
+
121
135
  ### 2단계: 코멘트별 컨텍스트 확인
122
136
 
123
137
  각 스레드가 짚은 **현재 코드**를 읽는다. 코멘트의 `diff_hunk`는 리뷰 시점 스냅샷이라 지금 코드와 다를 수 있다.
@@ -74,6 +74,13 @@ outputs:
74
74
  - DM: `slack_search_users`로 상대 → `user_id`(그대로 channel_id로 사용).
75
75
  - 확정한 채널명, ID를 사용자에게 노출해 **대상이 맞는지 확인**한다.
76
76
 
77
+ **첫 페이지만 보고 후보를 확정하지 않는다.** 두 검색 도구는 `limit` 상한이 20이라 한 번에 최대 20건만 온다. `#release-notice`를 찾는데 첫 페이지에 `#release-notice-dev`만 실려 오면, 후보가 하나뿐이라 "여러 개" 분기를 안 타고 그대로 확정한다. 승인 화면에는 그 채널이 정상으로 보이니 사용자도 못 잡는다. 워크스페이스에 비슷한 이름이 많을수록 잘 걸린다.
78
+
79
+ - **돌려받은 건수가 요청한 `limit`과 같으면 더 있다고 본다.** 응답이 준 커서를 `cursor`에 넣어 다음 페이지를 받는다. 건수가 `limit`보다 적게 올 때까지 반복한다.
80
+ - 커서 필드 이름을 문서에 박아두지 않는다 — 응답이 준 값을 그대로 되돌려 넣는다.
81
+ - 페이지를 다 받은 뒤에 후보 수를 센다. **0건/1건/다수 판정은 전체를 받은 다음에 한다.**
82
+ - 페이지를 끝까지 못 받았으면(도구 오류 등) 그 사실을 사용자에게 알리고 확인을 받는다. 부분 목록으로 조용히 확정하지 않는다.
83
+
77
84
  ### 4. 미리보기 + 승인 단계 (필수)
78
85
 
79
86
  아래를 한 화면에 모아 보여주고 명시적 승인을 받는다.
@@ -116,7 +123,7 @@ outputs:
116
123
 
117
124
  | 상황 | 대응 |
118
125
  |------|------|
119
- | 채널 검색 0건/다수 | 후보 보여주고 사용자에게 선택 요청 |
126
+ | 채널 검색 0건/다수 | 후보 보여주고 사용자에게 선택 요청 (**페이지를 다 받은 뒤에 센다** — 3단계 참조) |
120
127
  | 예약 시각이 2분 미만/120일 초과 | 사용자에게 알리고 시각 재확인 |
121
128
  | 전송 실패(권한/아카이브 채널) | 원인 보고, 재시도 여부 확인 |
122
129
  | 승인 응답 모호 | 전송 보류, 명시 승인 재요청 |