forge-workflow 0.0.3 → 0.0.5

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 (209) hide show
  1. package/.claude/commands/dev.md +340 -314
  2. package/.claude/commands/plan.md +521 -478
  3. package/.claude/commands/premerge.md +176 -179
  4. package/.claude/commands/research.md +42 -42
  5. package/.claude/commands/review.md +442 -442
  6. package/.claude/commands/rollback.md +721 -721
  7. package/.claude/commands/ship.md +164 -134
  8. package/.claude/commands/sonarcloud.md +152 -152
  9. package/.claude/commands/status.md +48 -77
  10. package/.claude/commands/validate.md +282 -237
  11. package/.claude/commands/verify.md +221 -221
  12. package/.claude/rules/greptile-review-process.md +285 -285
  13. package/.claude/rules/workflow.md +105 -105
  14. package/.claude/scripts/greptile-resolve.sh +526 -526
  15. package/.claude/scripts/load-env.sh +32 -32
  16. package/.cline/workflows/dev.md +337 -311
  17. package/.cline/workflows/plan.md +518 -475
  18. package/.cline/workflows/premerge.md +173 -176
  19. package/.cline/workflows/research.md +39 -39
  20. package/.cline/workflows/review.md +439 -439
  21. package/.cline/workflows/rollback.md +718 -718
  22. package/.cline/workflows/ship.md +161 -131
  23. package/.cline/workflows/sonarcloud.md +146 -146
  24. package/.cline/workflows/status.md +45 -74
  25. package/.cline/workflows/validate.md +279 -234
  26. package/.cline/workflows/verify.md +218 -218
  27. package/.codex/config.toml +11 -11
  28. package/.codex/skills/dev/SKILL.md +340 -314
  29. package/.codex/skills/plan/SKILL.md +521 -478
  30. package/.codex/skills/premerge/SKILL.md +176 -179
  31. package/.codex/skills/research/SKILL.md +42 -42
  32. package/.codex/skills/review/SKILL.md +442 -442
  33. package/.codex/skills/rollback/SKILL.md +721 -721
  34. package/.codex/skills/ship/SKILL.md +164 -134
  35. package/.codex/skills/sonarcloud/SKILL.md +149 -149
  36. package/.codex/skills/status/SKILL.md +48 -77
  37. package/.codex/skills/validate/SKILL.md +282 -237
  38. package/.codex/skills/verify/SKILL.md +221 -221
  39. package/.cursor/commands/dev.md +337 -311
  40. package/.cursor/commands/plan.md +518 -475
  41. package/.cursor/commands/premerge.md +173 -176
  42. package/.cursor/commands/research.md +39 -39
  43. package/.cursor/commands/review.md +439 -439
  44. package/.cursor/commands/rollback.md +718 -718
  45. package/.cursor/commands/ship.md +161 -131
  46. package/.cursor/commands/sonarcloud.md +146 -146
  47. package/.cursor/commands/status.md +45 -74
  48. package/.cursor/commands/validate.md +279 -234
  49. package/.cursor/commands/verify.md +218 -218
  50. package/.cursor/rules/permissions-guidance.mdc +37 -37
  51. package/.forge/hooks/check-tdd.js +240 -240
  52. package/.github/PLUGIN_TEMPLATE.json +32 -32
  53. package/.github/prompts/dev.prompt.md +342 -316
  54. package/.github/prompts/plan.prompt.md +523 -480
  55. package/.github/prompts/premerge.prompt.md +178 -181
  56. package/.github/prompts/research.prompt.md +44 -44
  57. package/.github/prompts/review.prompt.md +444 -444
  58. package/.github/prompts/rollback.prompt.md +723 -723
  59. package/.github/prompts/ship.prompt.md +166 -136
  60. package/.github/prompts/sonarcloud.prompt.md +151 -151
  61. package/.github/prompts/status.prompt.md +50 -79
  62. package/.github/prompts/validate.prompt.md +284 -239
  63. package/.github/prompts/verify.prompt.md +223 -223
  64. package/.github/workflows/beads-to-github.yml +56 -0
  65. package/.github/workflows/github-to-beads.yml +97 -0
  66. package/.kilocode/workflows/dev.md +341 -315
  67. package/.kilocode/workflows/plan.md +522 -479
  68. package/.kilocode/workflows/premerge.md +177 -180
  69. package/.kilocode/workflows/research.md +43 -43
  70. package/.kilocode/workflows/review.md +443 -443
  71. package/.kilocode/workflows/rollback.md +722 -722
  72. package/.kilocode/workflows/ship.md +165 -135
  73. package/.kilocode/workflows/sonarcloud.md +150 -150
  74. package/.kilocode/workflows/status.md +49 -78
  75. package/.kilocode/workflows/validate.md +283 -238
  76. package/.kilocode/workflows/verify.md +222 -222
  77. package/.mcp.json.example +12 -12
  78. package/.opencode/commands/dev.md +340 -314
  79. package/.opencode/commands/plan.md +521 -478
  80. package/.opencode/commands/premerge.md +176 -179
  81. package/.opencode/commands/research.md +42 -42
  82. package/.opencode/commands/review.md +442 -442
  83. package/.opencode/commands/rollback.md +721 -721
  84. package/.opencode/commands/ship.md +164 -134
  85. package/.opencode/commands/sonarcloud.md +149 -149
  86. package/.opencode/commands/status.md +48 -77
  87. package/.opencode/commands/validate.md +282 -237
  88. package/.opencode/commands/verify.md +221 -221
  89. package/.roo/commands/dev.md +341 -315
  90. package/.roo/commands/plan.md +522 -479
  91. package/.roo/commands/premerge.md +177 -180
  92. package/.roo/commands/research.md +43 -43
  93. package/.roo/commands/review.md +443 -443
  94. package/.roo/commands/rollback.md +722 -722
  95. package/.roo/commands/ship.md +165 -135
  96. package/.roo/commands/sonarcloud.md +150 -150
  97. package/.roo/commands/status.md +49 -78
  98. package/.roo/commands/validate.md +283 -238
  99. package/.roo/commands/verify.md +222 -222
  100. package/AGENTS.md +175 -169
  101. package/CLAUDE.md +100 -99
  102. package/LICENSE +21 -21
  103. package/README.md +429 -414
  104. package/bin/forge-cmd.js +313 -313
  105. package/bin/{forge-validate.js → forge-preflight.js} +309 -303
  106. package/bin/forge.js +4596 -4232
  107. package/docs/AGENT_INSTALL_PROMPT.md +342 -342
  108. package/docs/BEADS_GITHUB_SYNC.md +251 -0
  109. package/docs/ENHANCED_ONBOARDING.md +602 -602
  110. package/docs/EXAMPLES.md +482 -482
  111. package/docs/GREPTILE_SETUP.md +400 -400
  112. package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
  113. package/docs/ROADMAP.md +359 -359
  114. package/docs/SETUP.md +663 -632
  115. package/docs/TOOLCHAIN.md +630 -630
  116. package/docs/VALIDATION.md +363 -363
  117. package/install.sh +40 -1058
  118. package/lefthook.yml +39 -39
  119. package/lib/agents/README.md +198 -198
  120. package/lib/agents/claude.plugin.json +28 -28
  121. package/lib/agents/cline.plugin.json +22 -22
  122. package/lib/agents/codex.plugin.json +19 -19
  123. package/lib/agents/copilot.plugin.json +24 -24
  124. package/lib/agents/cursor.plugin.json +25 -25
  125. package/lib/agents/kilocode.plugin.json +22 -22
  126. package/lib/agents/opencode.plugin.json +20 -20
  127. package/lib/agents/roo.plugin.json +23 -23
  128. package/lib/agents-config.js +2112 -2112
  129. package/lib/beads-health-check.js +143 -0
  130. package/lib/beads-setup.js +341 -0
  131. package/lib/beads-sync-scaffold.js +260 -0
  132. package/lib/commands/dev.js +513 -513
  133. package/lib/commands/plan.js +692 -692
  134. package/lib/commands/recommend.js +119 -119
  135. package/lib/commands/ship.js +377 -377
  136. package/lib/commands/status.js +378 -378
  137. package/lib/commands/validate.js +602 -602
  138. package/lib/context-merge.js +359 -359
  139. package/lib/dep-guard/analyzer.js +294 -294
  140. package/lib/dep-guard/behavior-detector.js +98 -98
  141. package/lib/dep-guard/contract-detector.js +162 -162
  142. package/lib/dep-guard/import-detector.js +498 -498
  143. package/lib/dep-guard/path-utils.js +13 -13
  144. package/lib/dep-guard/rubric.js +120 -120
  145. package/lib/dep-guard/task-parser.js +318 -318
  146. package/lib/detect-agent.js +191 -0
  147. package/lib/detect-worktree.js +47 -0
  148. package/lib/file-hash.js +26 -0
  149. package/lib/husky-migration.js +450 -0
  150. package/lib/lefthook-check.js +65 -0
  151. package/lib/pat-setup.js +207 -0
  152. package/lib/plugin-catalog.js +350 -350
  153. package/lib/plugin-manager.js +166 -166
  154. package/lib/plugin-recommender.js +141 -141
  155. package/lib/project-discovery.js +491 -491
  156. package/lib/setup-action-log.js +139 -0
  157. package/lib/setup-summary-renderer.js +106 -0
  158. package/lib/setup-utils.js +96 -0
  159. package/lib/setup.js +192 -118
  160. package/lib/smart-merge.js +64 -0
  161. package/lib/symlink-utils.js +81 -0
  162. package/lib/workflow-profiles.js +197 -197
  163. package/package.json +131 -129
  164. package/scripts/beads-context.sh +291 -0
  165. package/scripts/beads-context.test.js +563 -0
  166. package/scripts/behavioral-judge.sh +378 -0
  167. package/scripts/benchmark.js +85 -0
  168. package/scripts/branch-protection.js +183 -0
  169. package/scripts/check-agents.js +172 -0
  170. package/scripts/commitlint.js +42 -0
  171. package/scripts/conflict-detect.sh +323 -0
  172. package/scripts/dep-guard-analyze.js +71 -0
  173. package/scripts/dep-guard.sh +811 -0
  174. package/scripts/eval_win.py +249 -0
  175. package/scripts/file-index.sh +399 -0
  176. package/scripts/github-beads-sync/comment.mjs +64 -0
  177. package/scripts/github-beads-sync/config.mjs +148 -0
  178. package/scripts/github-beads-sync/github-api.mjs +131 -0
  179. package/scripts/github-beads-sync/index.mjs +332 -0
  180. package/scripts/github-beads-sync/label-mapper.mjs +54 -0
  181. package/scripts/github-beads-sync/mapping.mjs +78 -0
  182. package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
  183. package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
  184. package/scripts/github-beads-sync/run-bd.mjs +159 -0
  185. package/scripts/github-beads-sync/sanitize.mjs +121 -0
  186. package/scripts/github-beads-sync.config.json +26 -0
  187. package/scripts/improve-command.js +375 -0
  188. package/scripts/lib/eval-runner.js +229 -0
  189. package/scripts/lib/eval-schema.js +135 -0
  190. package/scripts/lib/eval-storage.js +78 -0
  191. package/scripts/lib/grading.js +203 -0
  192. package/scripts/lib/transcript-parser.js +63 -0
  193. package/scripts/lint.js +47 -0
  194. package/scripts/migrate-to-bun-test.js +412 -0
  195. package/scripts/run-command-eval.js +236 -0
  196. package/scripts/smart-status.sh +782 -0
  197. package/scripts/sync-commands.js +571 -0
  198. package/scripts/sync-utils.sh +460 -0
  199. package/scripts/test-dashboard.js +123 -0
  200. package/scripts/test.js +44 -0
  201. package/scripts/validate.sh +94 -0
  202. package/skills/parallel-deep-research/SKILL.md +108 -108
  203. package/skills/parallel-deep-research/evals/README.md +27 -27
  204. package/skills/parallel-deep-research/evals/evals.json +62 -62
  205. package/skills/sonarcloud-analysis/SKILL.md +171 -171
  206. package/skills/sonarcloud-analysis/evals/README.md +27 -27
  207. package/skills/sonarcloud-analysis/evals/evals.json +50 -50
  208. package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
  209. package/docs/WORKFLOW.md +0 -400
@@ -1,526 +1,526 @@
1
- #!/bin/bash
2
- # Greptile Review Thread Resolution Tool
3
- # Uses GitHub GraphQL API to systematically resolve review threads
4
- # Uses GitHub REST API to reply to review comments
5
-
6
- set -e
7
-
8
- # Colors for output (defined before use)
9
- RED='\033[0;31m'
10
- GREEN='\033[0;32m'
11
- YELLOW='\033[1;33m'
12
- BLUE='\033[0;34m'
13
- NC='\033[0m' # No Color
14
-
15
- # Auto-detect repository from git config
16
- detect_repo() {
17
- local repo_info
18
- repo_info=$(gh repo view --json owner,name 2>/dev/null) || {
19
- echo -e "${RED}Error: Not in a git repository or gh CLI not authenticated${NC}"
20
- echo "Run: gh auth login"
21
- exit 1
22
- }
23
-
24
- OWNER=$(echo "$repo_info" | jq -r '.owner.login')
25
- REPO=$(echo "$repo_info" | jq -r '.name')
26
-
27
- if [ -z "$OWNER" ] || [ -z "$REPO" ]; then
28
- echo -e "${RED}Error: Could not detect repository${NC}"
29
- exit 1
30
- fi
31
- }
32
-
33
- # Initialize repo detection
34
- detect_repo
35
-
36
- # Check for jq dependency
37
- # Note: exit 1 is intentional - script cannot function without jq for JSON parsing
38
- check_jq() {
39
- if ! command -v jq &> /dev/null; then
40
- echo -e "${RED}Error: jq is required but not installed${NC}"
41
- echo ""
42
- echo "This script requires jq for JSON parsing and cannot continue without it."
43
- echo ""
44
- echo "Please install jq:"
45
- echo " • Ubuntu/Debian: sudo apt-get install jq"
46
- echo " • macOS: brew install jq"
47
- echo " • Windows: choco install jq"
48
- echo " • Or download from: https://jqlang.github.io/jq/download/"
49
- echo ""
50
- exit 1
51
- fi
52
- }
53
-
54
- usage() {
55
- cat <<EOF
56
- Usage: $(basename "$0") <command> <pr-number> [options]
57
-
58
- Commands:
59
- list <pr-number> List all review threads
60
- list <pr-number> --unresolved List only unresolved threads
61
- list-all <pr-number> List inline threads + direct PR comments from Greptile
62
- reply <pr-number> <comment-id> <message> Reply to a review comment
63
- resolve <pr-number> <thread-id> Resolve a specific thread
64
- reply-and-resolve <pr-number> <comment-id> <thread-id> <message> Reply and resolve in one step
65
- resolve-all <pr-number> Resolve ALL unresolved Greptile threads
66
- stats <pr-number> Show resolution statistics
67
-
68
- Examples:
69
- $(basename "$0") list 24
70
- $(basename "$0") list 24 --unresolved
71
- $(basename "$0") list-all 24
72
- $(basename "$0") reply 24 2787717459 "Fixed in commit abc123"
73
- $(basename "$0") resolve 24 PRRT_kwDORErEU85tuh6I
74
- $(basename "$0") reply-and-resolve 24 2787717459 PRRT_kwDORErEU85tuh6I "Fixed in commit abc123"
75
- $(basename "$0") resolve-all 24
76
- $(basename "$0") stats 24
77
-
78
- Comment IDs (databaseId) and Thread IDs are shown in the list command output.
79
- EOF
80
- exit 1
81
- }
82
-
83
- # Fetch all review threads for a PR (with pagination)
84
- fetch_threads() {
85
- local pr_number="$1"
86
- local all_threads="[]"
87
- local has_next_page=true
88
- local end_cursor="null"
89
-
90
- while [ "$has_next_page" = "true" ]; do
91
- local response
92
- if [ "$end_cursor" != "null" ]; then
93
- response=$(gh api graphql -f owner="$OWNER" -f repo="$REPO" -F prNumber="$pr_number" -f after="$end_cursor" -f query='
94
- query($owner: String!, $repo: String!, $prNumber: Int!, $after: String) {
95
- repository(owner: $owner, name: $repo) {
96
- pullRequest(number: $prNumber) {
97
- reviewThreads(first: 100, after: $after) {
98
- pageInfo {
99
- hasNextPage
100
- endCursor
101
- }
102
- nodes {
103
- id
104
- isResolved
105
- comments(first: 1) {
106
- nodes {
107
- databaseId
108
- author { login }
109
- body
110
- path
111
- line
112
- createdAt
113
- }
114
- }
115
- }
116
- }
117
- }
118
- }
119
- }
120
- ')
121
- else
122
- response=$(gh api graphql -f owner="$OWNER" -f repo="$REPO" -F prNumber="$pr_number" -f query='
123
- query($owner: String!, $repo: String!, $prNumber: Int!) {
124
- repository(owner: $owner, name: $repo) {
125
- pullRequest(number: $prNumber) {
126
- reviewThreads(first: 100) {
127
- pageInfo {
128
- hasNextPage
129
- endCursor
130
- }
131
- nodes {
132
- id
133
- isResolved
134
- comments(first: 1) {
135
- nodes {
136
- databaseId
137
- author { login }
138
- body
139
- path
140
- line
141
- createdAt
142
- }
143
- }
144
- }
145
- }
146
- }
147
- }
148
- }
149
- ')
150
- fi
151
-
152
- # Extract pagination info
153
- has_next_page=$(echo "$response" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage')
154
- end_cursor=$(echo "$response" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.endCursor')
155
-
156
- # Merge threads
157
- local page_threads
158
- page_threads=$(echo "$response" | jq -r '.data.repository.pullRequest.reviewThreads.nodes')
159
- all_threads=$(echo "$all_threads" | jq --argjson new "$page_threads" '. + $new')
160
- done
161
-
162
- # Return in original format
163
- echo "{\"data\":{\"repository\":{\"pullRequest\":{\"reviewThreads\":{\"nodes\":$all_threads}}}}}"
164
- }
165
-
166
- # Reply to a review comment (REST API)
167
- reply_to_comment() {
168
- local pr_number="$1"
169
- local comment_id="$2"
170
- local message="$3"
171
-
172
- echo -e "${BLUE}Replying to comment: $comment_id${NC}"
173
-
174
- gh api "repos/$OWNER/$REPO/pulls/$pr_number/comments/$comment_id/replies" \
175
- -f body="$message" || {
176
- echo -e "${RED}❌ Failed to reply to comment $comment_id${NC}"
177
- return 1
178
- }
179
-
180
- echo -e "${GREEN}✅ Reply posted successfully${NC}"
181
- }
182
-
183
- # Resolve a specific thread (GraphQL)
184
- resolve_thread() {
185
- local thread_id="$1"
186
-
187
- echo -e "${BLUE}Resolving thread: $thread_id${NC}"
188
-
189
- gh api graphql -f threadId="$thread_id" -f query='
190
- mutation($threadId: ID!) {
191
- resolveReviewThread(input: { threadId: $threadId }) {
192
- thread {
193
- id
194
- isResolved
195
- resolvedBy { login }
196
- }
197
- }
198
- }
199
- ' || {
200
- echo -e "${RED}❌ Failed to resolve thread $thread_id${NC}"
201
- return 1
202
- }
203
-
204
- echo -e "${GREEN}✅ Thread resolved successfully${NC}"
205
- }
206
-
207
- # List threads command
208
- cmd_list() {
209
- local pr_number="$1"
210
- local unresolved_only="${2:-}"
211
-
212
- echo -e "${BLUE}Fetching review threads for PR #$pr_number...${NC}\n"
213
-
214
- local response
215
- response=$(fetch_threads "$pr_number")
216
-
217
- # Parse with jq
218
- local threads
219
- threads=$(echo "$response" | jq -r '.data.repository.pullRequest.reviewThreads.nodes')
220
-
221
- local total_count
222
- total_count=$(echo "$threads" | jq 'length')
223
-
224
- local resolved_count=0
225
- local unresolved_count=0
226
- local greptile_count=0
227
-
228
- while IFS=$'\t' read -r thread_id is_resolved author body_b64 path line comment_id; do
229
- # Values already extracted by jq (body is base64-encoded to handle newlines)
230
- local body
231
- body=$(echo "$body_b64" | base64 -d 2>/dev/null || echo "")
232
-
233
- # Count
234
- if [ "$is_resolved" = "true" ]; then
235
- resolved_count=$((resolved_count + 1))
236
- else
237
- unresolved_count=$((unresolved_count + 1))
238
- fi
239
-
240
- if [[ "$author" == "greptile-apps"* ]]; then
241
- greptile_count=$((greptile_count + 1))
242
- fi
243
-
244
- # Filter if needed
245
- if [ "$unresolved_only" = "--unresolved" ] && [ "$is_resolved" = "true" ]; then
246
- continue
247
- fi
248
-
249
- # Display
250
- if [ "$is_resolved" = "true" ]; then
251
- echo -e "${GREEN}✓ RESOLVED${NC} | $path:$line"
252
- else
253
- echo -e "${RED}✗ UNRESOLVED${NC} | $path:$line"
254
- fi
255
-
256
- echo -e " ${BLUE}Thread ID:${NC} $thread_id"
257
- echo -e " ${BLUE}Comment ID:${NC} $comment_id"
258
- echo -e " ${BLUE}Author:${NC} $author"
259
-
260
- # Extract title from body (first line)
261
- local title
262
- title=$(echo "$body" | head -n 1 | sed 's/\*\*//g')
263
- echo -e " ${BLUE}Issue:${NC} $title"
264
- echo ""
265
- done < <(echo "$threads" | jq -r '.[] | [.id, .isResolved, (.comments.nodes[0].author.login // "unknown"), ((.comments.nodes[0].body // "") | @base64), (.comments.nodes[0].path // "unknown"), (.comments.nodes[0].line // "?"), (.comments.nodes[0].databaseId // "?")] | join("\t")')
266
-
267
- echo -e "\n${YELLOW}═══════════════════════════════════════${NC}"
268
- echo -e "${BLUE}Statistics for PR #$pr_number:${NC}"
269
- echo -e " Total threads: $total_count"
270
- echo -e " ${GREEN}Resolved: $resolved_count${NC}"
271
- echo -e " ${RED}Unresolved: $unresolved_count${NC}"
272
- echo -e " Greptile threads: $greptile_count"
273
- echo -e "${YELLOW}═══════════════════════════════════════${NC}"
274
- }
275
-
276
- # Reply to comment command
277
- cmd_reply() {
278
- local pr_number="$1"
279
- local comment_id="$2"
280
- local message="${3:-}"
281
-
282
- if [ -z "$comment_id" ]; then
283
- echo -e "${RED}Error: Comment ID required${NC}"
284
- usage
285
- fi
286
-
287
- if [ -z "$message" ]; then
288
- echo -e "${RED}Error: Message required${NC}"
289
- usage
290
- fi
291
-
292
- echo -e "${BLUE}Replying to comment for PR #$pr_number...${NC}\n"
293
-
294
- reply_to_comment "$pr_number" "$comment_id" "$message"
295
- }
296
-
297
- # Resolve specific thread
298
- cmd_resolve() {
299
- local pr_number="$1"
300
- local thread_id="$2"
301
-
302
- if [ -z "$thread_id" ]; then
303
- echo -e "${RED}Error: Thread ID required${NC}"
304
- usage
305
- fi
306
-
307
- echo -e "${BLUE}Resolving thread for PR #$pr_number...${NC}\n"
308
-
309
- resolve_thread "$thread_id"
310
- }
311
-
312
- # Reply and resolve in one step
313
- cmd_reply_and_resolve() {
314
- local pr_number="$1"
315
- local comment_id="$2"
316
- local thread_id="$3"
317
- local message="${4:-}"
318
-
319
- if [ -z "$comment_id" ] || [ -z "$thread_id" ]; then
320
- echo -e "${RED}Error: Both comment ID and thread ID required${NC}"
321
- usage
322
- fi
323
-
324
- if [ -z "$message" ]; then
325
- echo -e "${RED}Error: Message required${NC}"
326
- usage
327
- fi
328
-
329
- echo -e "${BLUE}Replying and resolving for PR #$pr_number...${NC}\n"
330
-
331
- # Step 1: Reply
332
- reply_to_comment "$pr_number" "$comment_id" "$message" || return 1
333
-
334
- # Step 2: Resolve
335
- resolve_thread "$thread_id" || return 1
336
-
337
- echo -e "\n${GREEN}✓ Successfully replied and resolved!${NC}"
338
- }
339
-
340
- # Resolve all unresolved Greptile threads
341
- cmd_resolve_all() {
342
- local pr_number="$1"
343
-
344
- echo -e "${YELLOW}⚠️ WARNING: This will resolve ALL unresolved Greptile threads for PR #$pr_number${NC}"
345
- echo -e "${YELLOW}Make sure you have fixed all issues AND replied to each thread before running this!${NC}\n"
346
-
347
- read -p "Continue? (y/n) " -n 1 -r
348
- echo
349
-
350
- if [[ ! $REPLY =~ ^[Yy]$ ]]; then
351
- echo "Cancelled."
352
- exit 0
353
- fi
354
-
355
- echo -e "\n${BLUE}Fetching unresolved Greptile threads...${NC}\n"
356
-
357
- local response
358
- response=$(fetch_threads "$pr_number")
359
-
360
- local threads
361
- threads=$(echo "$response" | jq -r '.data.repository.pullRequest.reviewThreads.nodes')
362
-
363
- local resolved_count=0
364
- local failed_count=0
365
-
366
- while IFS= read -r thread; do
367
- local thread_id
368
- thread_id=$(echo "$thread" | jq -r '.id')
369
-
370
- local is_resolved
371
- is_resolved=$(echo "$thread" | jq -r '.isResolved')
372
-
373
- local author
374
- author=$(echo "$thread" | jq -r '.comments.nodes[0].author.login // "unknown"')
375
-
376
- # Skip if already resolved
377
- if [ "$is_resolved" = "true" ]; then
378
- continue
379
- fi
380
-
381
- # Skip if not Greptile
382
- if [[ "$author" != "greptile-apps"* ]]; then
383
- continue
384
- fi
385
-
386
- # Resolve
387
- echo -e "${BLUE}Resolving thread $thread_id...${NC}"
388
- if resolve_thread "$thread_id" > /dev/null 2>&1; then
389
- resolved_count=$((resolved_count + 1))
390
- echo -e "${GREEN}✓ Resolved${NC}"
391
- else
392
- failed_count=$((failed_count + 1))
393
- echo -e "${RED}✗ Failed${NC}"
394
- fi
395
-
396
- # Small delay to avoid rate limiting
397
- sleep 0.5
398
- done < <(echo "$threads" | jq -r '.[] | @json')
399
-
400
- echo -e "\n${YELLOW}═══════════════════════════════════════${NC}"
401
- echo -e "${GREEN}✓ Resolved: $resolved_count threads${NC}"
402
- if [ "$failed_count" -gt 0 ]; then
403
- echo -e "${RED}✗ Failed: $failed_count threads${NC}"
404
- fi
405
- echo -e "${YELLOW}═══════════════════════════════════════${NC}"
406
- }
407
-
408
- # List all Greptile feedback: inline review threads + direct PR issue comments
409
- cmd_list_all() {
410
- local pr_number="$1"
411
-
412
- echo -e "${BLUE}═══ Greptile Inline Review Threads ═══${NC}\n"
413
- cmd_list "$pr_number"
414
-
415
- echo -e "\n${BLUE}═══ Greptile Direct PR Comments ═══${NC}\n"
416
-
417
- local comments
418
- comments=$(gh api "repos/$OWNER/$REPO/issues/$pr_number/comments" \
419
- --jq '[.[] | select(.user.login | startswith("greptile-apps"))]')
420
-
421
- local count
422
- count=$(echo "$comments" | jq 'length')
423
-
424
- if [ "$count" -eq 0 ]; then
425
- echo -e "${GREEN}No direct PR comments from Greptile.${NC}"
426
- else
427
- echo -e "Found ${YELLOW}$count${NC} direct comment(s) from Greptile:\n"
428
- while IFS=$'\t' read -r comment_id created_at body_preview; do
429
- echo -e " ${BLUE}Comment ID:${NC} $comment_id"
430
- echo -e " ${BLUE}Posted:${NC} $created_at"
431
- echo -e " ${BLUE}Preview:${NC} $body_preview"
432
- echo -e " ${BLUE}Reply with:${NC} gh pr comment $pr_number --body \"...\""
433
- echo ""
434
- done < <(echo "$comments" | jq -r '.[] | [.id, .created_at, (.body | split("\n")[0:3] | join(" | ") | .[0:120])] | join("\t")')
435
- fi
436
- }
437
-
438
- # Stats command
439
- cmd_stats() {
440
- local pr_number="$1"
441
-
442
- echo -e "${BLUE}Fetching statistics for PR #$pr_number...${NC}\n"
443
-
444
- local response
445
- response=$(fetch_threads "$pr_number")
446
-
447
- local threads
448
- threads=$(echo "$response" | jq -r '.data.repository.pullRequest.reviewThreads.nodes')
449
-
450
- local total_count
451
- total_count=$(echo "$threads" | jq 'length')
452
-
453
- local resolved_count
454
- resolved_count=$(echo "$threads" | jq '[.[] | select(.isResolved == true)] | length')
455
-
456
- local unresolved_count
457
- unresolved_count=$(echo "$threads" | jq '[.[] | select(.isResolved == false)] | length')
458
-
459
- local greptile_count
460
- greptile_count=$(echo "$threads" | jq '[.[] | select(.comments.nodes[0].author.login | startswith("greptile-apps"))] | length')
461
-
462
- local greptile_unresolved
463
- greptile_unresolved=$(echo "$threads" | jq '[.[] | select(.isResolved == false and (.comments.nodes[0].author.login | startswith("greptile-apps")))] | length')
464
-
465
- echo -e "${YELLOW}═══════════════════════════════════════${NC}"
466
- echo -e "${BLUE}PR #$pr_number Review Thread Statistics:${NC}"
467
- echo -e "${YELLOW}═══════════════════════════════════════${NC}"
468
- echo -e "Total threads: $total_count"
469
- echo -e "${GREEN}Resolved: $resolved_count${NC}"
470
- echo -e "${RED}Unresolved: $unresolved_count${NC}"
471
- echo -e ""
472
- echo -e "Greptile threads: $greptile_count"
473
- echo -e "${RED}Greptile unresolved: $greptile_unresolved${NC}"
474
- echo -e "${YELLOW}═══════════════════════════════════════${NC}"
475
-
476
- if [ "$greptile_unresolved" -eq 0 ]; then
477
- echo -e "\n${GREEN}✓ All Greptile threads resolved!${NC}"
478
- else
479
- echo -e "\n${YELLOW}⚠️ $greptile_unresolved Greptile thread(s) still unresolved${NC}"
480
- echo -e "Run: $(basename "$0") list $pr_number --unresolved"
481
- fi
482
- }
483
-
484
- # Main command dispatcher
485
- main() {
486
- # Check for jq dependency
487
- check_jq
488
-
489
- if [ $# -lt 2 ]; then
490
- usage
491
- fi
492
-
493
- local command="$1"
494
- local pr_number="$2"
495
- shift 2
496
-
497
- case "$command" in
498
- list)
499
- cmd_list "$pr_number" "$@"
500
- ;;
501
- list-all)
502
- cmd_list_all "$pr_number"
503
- ;;
504
- reply)
505
- cmd_reply "$pr_number" "$@"
506
- ;;
507
- resolve)
508
- cmd_resolve "$pr_number" "$@"
509
- ;;
510
- reply-and-resolve)
511
- cmd_reply_and_resolve "$pr_number" "$@"
512
- ;;
513
- resolve-all)
514
- cmd_resolve_all "$pr_number"
515
- ;;
516
- stats)
517
- cmd_stats "$pr_number"
518
- ;;
519
- *)
520
- echo -e "${RED}Unknown command: $command${NC}\n"
521
- usage
522
- ;;
523
- esac
524
- }
525
-
526
- main "$@"
1
+ #!/bin/bash
2
+ # Greptile Review Thread Resolution Tool
3
+ # Uses GitHub GraphQL API to systematically resolve review threads
4
+ # Uses GitHub REST API to reply to review comments
5
+
6
+ set -e
7
+
8
+ # Colors for output (defined before use)
9
+ RED='\033[0;31m'
10
+ GREEN='\033[0;32m'
11
+ YELLOW='\033[1;33m'
12
+ BLUE='\033[0;34m'
13
+ NC='\033[0m' # No Color
14
+
15
+ # Auto-detect repository from git config
16
+ detect_repo() {
17
+ local repo_info
18
+ repo_info=$(gh repo view --json owner,name 2>/dev/null) || {
19
+ echo -e "${RED}Error: Not in a git repository or gh CLI not authenticated${NC}"
20
+ echo "Run: gh auth login"
21
+ exit 1
22
+ }
23
+
24
+ OWNER=$(echo "$repo_info" | jq -r '.owner.login')
25
+ REPO=$(echo "$repo_info" | jq -r '.name')
26
+
27
+ if [ -z "$OWNER" ] || [ -z "$REPO" ]; then
28
+ echo -e "${RED}Error: Could not detect repository${NC}"
29
+ exit 1
30
+ fi
31
+ }
32
+
33
+ # Initialize repo detection
34
+ detect_repo
35
+
36
+ # Check for jq dependency
37
+ # Note: exit 1 is intentional - script cannot function without jq for JSON parsing
38
+ check_jq() {
39
+ if ! command -v jq &> /dev/null; then
40
+ echo -e "${RED}Error: jq is required but not installed${NC}"
41
+ echo ""
42
+ echo "This script requires jq for JSON parsing and cannot continue without it."
43
+ echo ""
44
+ echo "Please install jq:"
45
+ echo " • Ubuntu/Debian: sudo apt-get install jq"
46
+ echo " • macOS: brew install jq"
47
+ echo " • Windows: choco install jq"
48
+ echo " • Or download from: https://jqlang.github.io/jq/download/"
49
+ echo ""
50
+ exit 1
51
+ fi
52
+ }
53
+
54
+ usage() {
55
+ cat <<EOF
56
+ Usage: $(basename "$0") <command> <pr-number> [options]
57
+
58
+ Commands:
59
+ list <pr-number> List all review threads
60
+ list <pr-number> --unresolved List only unresolved threads
61
+ list-all <pr-number> List inline threads + direct PR comments from Greptile
62
+ reply <pr-number> <comment-id> <message> Reply to a review comment
63
+ resolve <pr-number> <thread-id> Resolve a specific thread
64
+ reply-and-resolve <pr-number> <comment-id> <thread-id> <message> Reply and resolve in one step
65
+ resolve-all <pr-number> Resolve ALL unresolved Greptile threads
66
+ stats <pr-number> Show resolution statistics
67
+
68
+ Examples:
69
+ $(basename "$0") list 24
70
+ $(basename "$0") list 24 --unresolved
71
+ $(basename "$0") list-all 24
72
+ $(basename "$0") reply 24 2787717459 "Fixed in commit abc123"
73
+ $(basename "$0") resolve 24 PRRT_kwDORErEU85tuh6I
74
+ $(basename "$0") reply-and-resolve 24 2787717459 PRRT_kwDORErEU85tuh6I "Fixed in commit abc123"
75
+ $(basename "$0") resolve-all 24
76
+ $(basename "$0") stats 24
77
+
78
+ Comment IDs (databaseId) and Thread IDs are shown in the list command output.
79
+ EOF
80
+ exit 1
81
+ }
82
+
83
+ # Fetch all review threads for a PR (with pagination)
84
+ fetch_threads() {
85
+ local pr_number="$1"
86
+ local all_threads="[]"
87
+ local has_next_page=true
88
+ local end_cursor="null"
89
+
90
+ while [ "$has_next_page" = "true" ]; do
91
+ local response
92
+ if [ "$end_cursor" != "null" ]; then
93
+ response=$(gh api graphql -f owner="$OWNER" -f repo="$REPO" -F prNumber="$pr_number" -f after="$end_cursor" -f query='
94
+ query($owner: String!, $repo: String!, $prNumber: Int!, $after: String) {
95
+ repository(owner: $owner, name: $repo) {
96
+ pullRequest(number: $prNumber) {
97
+ reviewThreads(first: 100, after: $after) {
98
+ pageInfo {
99
+ hasNextPage
100
+ endCursor
101
+ }
102
+ nodes {
103
+ id
104
+ isResolved
105
+ comments(first: 1) {
106
+ nodes {
107
+ databaseId
108
+ author { login }
109
+ body
110
+ path
111
+ line
112
+ createdAt
113
+ }
114
+ }
115
+ }
116
+ }
117
+ }
118
+ }
119
+ }
120
+ ')
121
+ else
122
+ response=$(gh api graphql -f owner="$OWNER" -f repo="$REPO" -F prNumber="$pr_number" -f query='
123
+ query($owner: String!, $repo: String!, $prNumber: Int!) {
124
+ repository(owner: $owner, name: $repo) {
125
+ pullRequest(number: $prNumber) {
126
+ reviewThreads(first: 100) {
127
+ pageInfo {
128
+ hasNextPage
129
+ endCursor
130
+ }
131
+ nodes {
132
+ id
133
+ isResolved
134
+ comments(first: 1) {
135
+ nodes {
136
+ databaseId
137
+ author { login }
138
+ body
139
+ path
140
+ line
141
+ createdAt
142
+ }
143
+ }
144
+ }
145
+ }
146
+ }
147
+ }
148
+ }
149
+ ')
150
+ fi
151
+
152
+ # Extract pagination info
153
+ has_next_page=$(echo "$response" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage')
154
+ end_cursor=$(echo "$response" | jq -r '.data.repository.pullRequest.reviewThreads.pageInfo.endCursor')
155
+
156
+ # Merge threads
157
+ local page_threads
158
+ page_threads=$(echo "$response" | jq -r '.data.repository.pullRequest.reviewThreads.nodes')
159
+ all_threads=$(echo "$all_threads" | jq --argjson new "$page_threads" '. + $new')
160
+ done
161
+
162
+ # Return in original format
163
+ echo "{\"data\":{\"repository\":{\"pullRequest\":{\"reviewThreads\":{\"nodes\":$all_threads}}}}}"
164
+ }
165
+
166
+ # Reply to a review comment (REST API)
167
+ reply_to_comment() {
168
+ local pr_number="$1"
169
+ local comment_id="$2"
170
+ local message="$3"
171
+
172
+ echo -e "${BLUE}Replying to comment: $comment_id${NC}"
173
+
174
+ gh api "repos/$OWNER/$REPO/pulls/$pr_number/comments/$comment_id/replies" \
175
+ -f body="$message" || {
176
+ echo -e "${RED}❌ Failed to reply to comment $comment_id${NC}"
177
+ return 1
178
+ }
179
+
180
+ echo -e "${GREEN}✅ Reply posted successfully${NC}"
181
+ }
182
+
183
+ # Resolve a specific thread (GraphQL)
184
+ resolve_thread() {
185
+ local thread_id="$1"
186
+
187
+ echo -e "${BLUE}Resolving thread: $thread_id${NC}"
188
+
189
+ gh api graphql -f threadId="$thread_id" -f query='
190
+ mutation($threadId: ID!) {
191
+ resolveReviewThread(input: { threadId: $threadId }) {
192
+ thread {
193
+ id
194
+ isResolved
195
+ resolvedBy { login }
196
+ }
197
+ }
198
+ }
199
+ ' || {
200
+ echo -e "${RED}❌ Failed to resolve thread $thread_id${NC}"
201
+ return 1
202
+ }
203
+
204
+ echo -e "${GREEN}✅ Thread resolved successfully${NC}"
205
+ }
206
+
207
+ # List threads command
208
+ cmd_list() {
209
+ local pr_number="$1"
210
+ local unresolved_only="${2:-}"
211
+
212
+ echo -e "${BLUE}Fetching review threads for PR #$pr_number...${NC}\n"
213
+
214
+ local response
215
+ response=$(fetch_threads "$pr_number")
216
+
217
+ # Parse with jq
218
+ local threads
219
+ threads=$(echo "$response" | jq -r '.data.repository.pullRequest.reviewThreads.nodes')
220
+
221
+ local total_count
222
+ total_count=$(echo "$threads" | jq 'length')
223
+
224
+ local resolved_count=0
225
+ local unresolved_count=0
226
+ local greptile_count=0
227
+
228
+ while IFS=$'\t' read -r thread_id is_resolved author body_b64 path line comment_id; do
229
+ # Values already extracted by jq (body is base64-encoded to handle newlines)
230
+ local body
231
+ body=$(echo "$body_b64" | base64 -d 2>/dev/null || echo "")
232
+
233
+ # Count
234
+ if [ "$is_resolved" = "true" ]; then
235
+ resolved_count=$((resolved_count + 1))
236
+ else
237
+ unresolved_count=$((unresolved_count + 1))
238
+ fi
239
+
240
+ if [[ "$author" == "greptile-apps"* ]]; then
241
+ greptile_count=$((greptile_count + 1))
242
+ fi
243
+
244
+ # Filter if needed
245
+ if [ "$unresolved_only" = "--unresolved" ] && [ "$is_resolved" = "true" ]; then
246
+ continue
247
+ fi
248
+
249
+ # Display
250
+ if [ "$is_resolved" = "true" ]; then
251
+ echo -e "${GREEN}✓ RESOLVED${NC} | $path:$line"
252
+ else
253
+ echo -e "${RED}✗ UNRESOLVED${NC} | $path:$line"
254
+ fi
255
+
256
+ echo -e " ${BLUE}Thread ID:${NC} $thread_id"
257
+ echo -e " ${BLUE}Comment ID:${NC} $comment_id"
258
+ echo -e " ${BLUE}Author:${NC} $author"
259
+
260
+ # Extract title from body (first line)
261
+ local title
262
+ title=$(echo "$body" | head -n 1 | sed 's/\*\*//g')
263
+ echo -e " ${BLUE}Issue:${NC} $title"
264
+ echo ""
265
+ done < <(echo "$threads" | jq -r '.[] | [.id, .isResolved, (.comments.nodes[0].author.login // "unknown"), ((.comments.nodes[0].body // "") | @base64), (.comments.nodes[0].path // "unknown"), (.comments.nodes[0].line // "?"), (.comments.nodes[0].databaseId // "?")] | join("\t")')
266
+
267
+ echo -e "\n${YELLOW}═══════════════════════════════════════${NC}"
268
+ echo -e "${BLUE}Statistics for PR #$pr_number:${NC}"
269
+ echo -e " Total threads: $total_count"
270
+ echo -e " ${GREEN}Resolved: $resolved_count${NC}"
271
+ echo -e " ${RED}Unresolved: $unresolved_count${NC}"
272
+ echo -e " Greptile threads: $greptile_count"
273
+ echo -e "${YELLOW}═══════════════════════════════════════${NC}"
274
+ }
275
+
276
+ # Reply to comment command
277
+ cmd_reply() {
278
+ local pr_number="$1"
279
+ local comment_id="$2"
280
+ local message="${3:-}"
281
+
282
+ if [ -z "$comment_id" ]; then
283
+ echo -e "${RED}Error: Comment ID required${NC}"
284
+ usage
285
+ fi
286
+
287
+ if [ -z "$message" ]; then
288
+ echo -e "${RED}Error: Message required${NC}"
289
+ usage
290
+ fi
291
+
292
+ echo -e "${BLUE}Replying to comment for PR #$pr_number...${NC}\n"
293
+
294
+ reply_to_comment "$pr_number" "$comment_id" "$message"
295
+ }
296
+
297
+ # Resolve specific thread
298
+ cmd_resolve() {
299
+ local pr_number="$1"
300
+ local thread_id="$2"
301
+
302
+ if [ -z "$thread_id" ]; then
303
+ echo -e "${RED}Error: Thread ID required${NC}"
304
+ usage
305
+ fi
306
+
307
+ echo -e "${BLUE}Resolving thread for PR #$pr_number...${NC}\n"
308
+
309
+ resolve_thread "$thread_id"
310
+ }
311
+
312
+ # Reply and resolve in one step
313
+ cmd_reply_and_resolve() {
314
+ local pr_number="$1"
315
+ local comment_id="$2"
316
+ local thread_id="$3"
317
+ local message="${4:-}"
318
+
319
+ if [ -z "$comment_id" ] || [ -z "$thread_id" ]; then
320
+ echo -e "${RED}Error: Both comment ID and thread ID required${NC}"
321
+ usage
322
+ fi
323
+
324
+ if [ -z "$message" ]; then
325
+ echo -e "${RED}Error: Message required${NC}"
326
+ usage
327
+ fi
328
+
329
+ echo -e "${BLUE}Replying and resolving for PR #$pr_number...${NC}\n"
330
+
331
+ # Step 1: Reply
332
+ reply_to_comment "$pr_number" "$comment_id" "$message" || return 1
333
+
334
+ # Step 2: Resolve
335
+ resolve_thread "$thread_id" || return 1
336
+
337
+ echo -e "\n${GREEN}✓ Successfully replied and resolved!${NC}"
338
+ }
339
+
340
+ # Resolve all unresolved Greptile threads
341
+ cmd_resolve_all() {
342
+ local pr_number="$1"
343
+
344
+ echo -e "${YELLOW}⚠️ WARNING: This will resolve ALL unresolved Greptile threads for PR #$pr_number${NC}"
345
+ echo -e "${YELLOW}Make sure you have fixed all issues AND replied to each thread before running this!${NC}\n"
346
+
347
+ read -p "Continue? (y/n) " -n 1 -r
348
+ echo
349
+
350
+ if [[ ! $REPLY =~ ^[Yy]$ ]]; then
351
+ echo "Cancelled."
352
+ exit 0
353
+ fi
354
+
355
+ echo -e "\n${BLUE}Fetching unresolved Greptile threads...${NC}\n"
356
+
357
+ local response
358
+ response=$(fetch_threads "$pr_number")
359
+
360
+ local threads
361
+ threads=$(echo "$response" | jq -r '.data.repository.pullRequest.reviewThreads.nodes')
362
+
363
+ local resolved_count=0
364
+ local failed_count=0
365
+
366
+ while IFS= read -r thread; do
367
+ local thread_id
368
+ thread_id=$(echo "$thread" | jq -r '.id')
369
+
370
+ local is_resolved
371
+ is_resolved=$(echo "$thread" | jq -r '.isResolved')
372
+
373
+ local author
374
+ author=$(echo "$thread" | jq -r '.comments.nodes[0].author.login // "unknown"')
375
+
376
+ # Skip if already resolved
377
+ if [ "$is_resolved" = "true" ]; then
378
+ continue
379
+ fi
380
+
381
+ # Skip if not Greptile
382
+ if [[ "$author" != "greptile-apps"* ]]; then
383
+ continue
384
+ fi
385
+
386
+ # Resolve
387
+ echo -e "${BLUE}Resolving thread $thread_id...${NC}"
388
+ if resolve_thread "$thread_id" > /dev/null 2>&1; then
389
+ resolved_count=$((resolved_count + 1))
390
+ echo -e "${GREEN}✓ Resolved${NC}"
391
+ else
392
+ failed_count=$((failed_count + 1))
393
+ echo -e "${RED}✗ Failed${NC}"
394
+ fi
395
+
396
+ # Small delay to avoid rate limiting
397
+ sleep 0.5
398
+ done < <(echo "$threads" | jq -r '.[] | @json')
399
+
400
+ echo -e "\n${YELLOW}═══════════════════════════════════════${NC}"
401
+ echo -e "${GREEN}✓ Resolved: $resolved_count threads${NC}"
402
+ if [ "$failed_count" -gt 0 ]; then
403
+ echo -e "${RED}✗ Failed: $failed_count threads${NC}"
404
+ fi
405
+ echo -e "${YELLOW}═══════════════════════════════════════${NC}"
406
+ }
407
+
408
+ # List all Greptile feedback: inline review threads + direct PR issue comments
409
+ cmd_list_all() {
410
+ local pr_number="$1"
411
+
412
+ echo -e "${BLUE}═══ Greptile Inline Review Threads ═══${NC}\n"
413
+ cmd_list "$pr_number"
414
+
415
+ echo -e "\n${BLUE}═══ Greptile Direct PR Comments ═══${NC}\n"
416
+
417
+ local comments
418
+ comments=$(gh api "repos/$OWNER/$REPO/issues/$pr_number/comments" \
419
+ --jq '[.[] | select(.user.login | startswith("greptile-apps"))]')
420
+
421
+ local count
422
+ count=$(echo "$comments" | jq 'length')
423
+
424
+ if [ "$count" -eq 0 ]; then
425
+ echo -e "${GREEN}No direct PR comments from Greptile.${NC}"
426
+ else
427
+ echo -e "Found ${YELLOW}$count${NC} direct comment(s) from Greptile:\n"
428
+ while IFS=$'\t' read -r comment_id created_at body_preview; do
429
+ echo -e " ${BLUE}Comment ID:${NC} $comment_id"
430
+ echo -e " ${BLUE}Posted:${NC} $created_at"
431
+ echo -e " ${BLUE}Preview:${NC} $body_preview"
432
+ echo -e " ${BLUE}Reply with:${NC} gh pr comment $pr_number --body \"...\""
433
+ echo ""
434
+ done < <(echo "$comments" | jq -r '.[] | [.id, .created_at, (.body | split("\n")[0:3] | join(" | ") | .[0:120])] | join("\t")')
435
+ fi
436
+ }
437
+
438
+ # Stats command
439
+ cmd_stats() {
440
+ local pr_number="$1"
441
+
442
+ echo -e "${BLUE}Fetching statistics for PR #$pr_number...${NC}\n"
443
+
444
+ local response
445
+ response=$(fetch_threads "$pr_number")
446
+
447
+ local threads
448
+ threads=$(echo "$response" | jq -r '.data.repository.pullRequest.reviewThreads.nodes')
449
+
450
+ local total_count
451
+ total_count=$(echo "$threads" | jq 'length')
452
+
453
+ local resolved_count
454
+ resolved_count=$(echo "$threads" | jq '[.[] | select(.isResolved == true)] | length')
455
+
456
+ local unresolved_count
457
+ unresolved_count=$(echo "$threads" | jq '[.[] | select(.isResolved == false)] | length')
458
+
459
+ local greptile_count
460
+ greptile_count=$(echo "$threads" | jq '[.[] | select(.comments.nodes[0].author.login | startswith("greptile-apps"))] | length')
461
+
462
+ local greptile_unresolved
463
+ greptile_unresolved=$(echo "$threads" | jq '[.[] | select(.isResolved == false and (.comments.nodes[0].author.login | startswith("greptile-apps")))] | length')
464
+
465
+ echo -e "${YELLOW}═══════════════════════════════════════${NC}"
466
+ echo -e "${BLUE}PR #$pr_number Review Thread Statistics:${NC}"
467
+ echo -e "${YELLOW}═══════════════════════════════════════${NC}"
468
+ echo -e "Total threads: $total_count"
469
+ echo -e "${GREEN}Resolved: $resolved_count${NC}"
470
+ echo -e "${RED}Unresolved: $unresolved_count${NC}"
471
+ echo -e ""
472
+ echo -e "Greptile threads: $greptile_count"
473
+ echo -e "${RED}Greptile unresolved: $greptile_unresolved${NC}"
474
+ echo -e "${YELLOW}═══════════════════════════════════════${NC}"
475
+
476
+ if [ "$greptile_unresolved" -eq 0 ]; then
477
+ echo -e "\n${GREEN}✓ All Greptile threads resolved!${NC}"
478
+ else
479
+ echo -e "\n${YELLOW}⚠️ $greptile_unresolved Greptile thread(s) still unresolved${NC}"
480
+ echo -e "Run: $(basename "$0") list $pr_number --unresolved"
481
+ fi
482
+ }
483
+
484
+ # Main command dispatcher
485
+ main() {
486
+ # Check for jq dependency
487
+ check_jq
488
+
489
+ if [ $# -lt 2 ]; then
490
+ usage
491
+ fi
492
+
493
+ local command="$1"
494
+ local pr_number="$2"
495
+ shift 2
496
+
497
+ case "$command" in
498
+ list)
499
+ cmd_list "$pr_number" "$@"
500
+ ;;
501
+ list-all)
502
+ cmd_list_all "$pr_number"
503
+ ;;
504
+ reply)
505
+ cmd_reply "$pr_number" "$@"
506
+ ;;
507
+ resolve)
508
+ cmd_resolve "$pr_number" "$@"
509
+ ;;
510
+ reply-and-resolve)
511
+ cmd_reply_and_resolve "$pr_number" "$@"
512
+ ;;
513
+ resolve-all)
514
+ cmd_resolve_all "$pr_number"
515
+ ;;
516
+ stats)
517
+ cmd_stats "$pr_number"
518
+ ;;
519
+ *)
520
+ echo -e "${RED}Unknown command: $command${NC}\n"
521
+ usage
522
+ ;;
523
+ esac
524
+ }
525
+
526
+ main "$@"