forge-workflow 0.0.4 → 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 -340
  2. package/.claude/commands/plan.md +521 -521
  3. package/.claude/commands/premerge.md +176 -176
  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 -164
  8. package/.claude/commands/sonarcloud.md +152 -152
  9. package/.claude/commands/status.md +48 -48
  10. package/.claude/commands/validate.md +282 -282
  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 -337
  17. package/.cline/workflows/plan.md +518 -518
  18. package/.cline/workflows/premerge.md +173 -173
  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 -161
  23. package/.cline/workflows/sonarcloud.md +146 -146
  24. package/.cline/workflows/status.md +45 -45
  25. package/.cline/workflows/validate.md +279 -279
  26. package/.cline/workflows/verify.md +218 -218
  27. package/.codex/config.toml +11 -11
  28. package/.codex/skills/dev/SKILL.md +340 -340
  29. package/.codex/skills/plan/SKILL.md +521 -521
  30. package/.codex/skills/premerge/SKILL.md +176 -176
  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 -164
  35. package/.codex/skills/sonarcloud/SKILL.md +149 -149
  36. package/.codex/skills/status/SKILL.md +48 -48
  37. package/.codex/skills/validate/SKILL.md +282 -282
  38. package/.codex/skills/verify/SKILL.md +221 -221
  39. package/.cursor/commands/dev.md +337 -337
  40. package/.cursor/commands/plan.md +518 -518
  41. package/.cursor/commands/premerge.md +173 -173
  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 -161
  46. package/.cursor/commands/sonarcloud.md +146 -146
  47. package/.cursor/commands/status.md +45 -45
  48. package/.cursor/commands/validate.md +279 -279
  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 -342
  54. package/.github/prompts/plan.prompt.md +523 -523
  55. package/.github/prompts/premerge.prompt.md +178 -178
  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 -166
  60. package/.github/prompts/sonarcloud.prompt.md +151 -151
  61. package/.github/prompts/status.prompt.md +50 -50
  62. package/.github/prompts/validate.prompt.md +284 -284
  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 -341
  67. package/.kilocode/workflows/plan.md +522 -522
  68. package/.kilocode/workflows/premerge.md +177 -177
  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 -165
  73. package/.kilocode/workflows/sonarcloud.md +150 -150
  74. package/.kilocode/workflows/status.md +49 -49
  75. package/.kilocode/workflows/validate.md +283 -283
  76. package/.kilocode/workflows/verify.md +222 -222
  77. package/.mcp.json.example +12 -12
  78. package/.opencode/commands/dev.md +340 -340
  79. package/.opencode/commands/plan.md +521 -521
  80. package/.opencode/commands/premerge.md +176 -176
  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 -164
  85. package/.opencode/commands/sonarcloud.md +149 -149
  86. package/.opencode/commands/status.md +48 -48
  87. package/.opencode/commands/validate.md +282 -282
  88. package/.opencode/commands/verify.md +221 -221
  89. package/.roo/commands/dev.md +341 -341
  90. package/.roo/commands/plan.md +522 -522
  91. package/.roo/commands/premerge.md +177 -177
  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 -165
  96. package/.roo/commands/sonarcloud.md +150 -150
  97. package/.roo/commands/status.md +49 -49
  98. package/.roo/commands/validate.md +283 -283
  99. package/.roo/commands/verify.md +222 -222
  100. package/AGENTS.md +175 -175
  101. package/CLAUDE.md +100 -100
  102. package/README.md +429 -416
  103. package/bin/forge-cmd.js +313 -313
  104. package/bin/forge-preflight.js +309 -309
  105. package/bin/forge.js +4596 -4303
  106. package/docs/AGENT_INSTALL_PROMPT.md +342 -342
  107. package/docs/BEADS_GITHUB_SYNC.md +251 -251
  108. package/docs/ENHANCED_ONBOARDING.md +602 -602
  109. package/docs/EXAMPLES.md +482 -482
  110. package/docs/GREPTILE_SETUP.md +400 -400
  111. package/docs/MANUAL_REVIEW_GUIDE.md +106 -106
  112. package/docs/ROADMAP.md +359 -359
  113. package/docs/SETUP.md +663 -631
  114. package/docs/TOOLCHAIN.md +630 -630
  115. package/docs/VALIDATION.md +363 -363
  116. package/install.sh +40 -1056
  117. package/lefthook.yml +39 -39
  118. package/lib/agents/README.md +198 -198
  119. package/lib/agents/claude.plugin.json +28 -28
  120. package/lib/agents/cline.plugin.json +22 -22
  121. package/lib/agents/codex.plugin.json +19 -19
  122. package/lib/agents/copilot.plugin.json +24 -24
  123. package/lib/agents/cursor.plugin.json +25 -25
  124. package/lib/agents/kilocode.plugin.json +22 -22
  125. package/lib/agents/opencode.plugin.json +20 -20
  126. package/lib/agents/roo.plugin.json +23 -23
  127. package/lib/agents-config.js +2112 -2112
  128. package/lib/beads-health-check.js +143 -0
  129. package/lib/beads-setup.js +341 -0
  130. package/lib/beads-sync-scaffold.js +260 -0
  131. package/lib/commands/dev.js +513 -513
  132. package/lib/commands/plan.js +692 -692
  133. package/lib/commands/recommend.js +119 -119
  134. package/lib/commands/ship.js +377 -377
  135. package/lib/commands/status.js +378 -378
  136. package/lib/commands/validate.js +602 -602
  137. package/lib/context-merge.js +359 -359
  138. package/lib/dep-guard/analyzer.js +294 -294
  139. package/lib/dep-guard/behavior-detector.js +98 -98
  140. package/lib/dep-guard/contract-detector.js +162 -162
  141. package/lib/dep-guard/import-detector.js +498 -498
  142. package/lib/dep-guard/path-utils.js +13 -13
  143. package/lib/dep-guard/rubric.js +120 -120
  144. package/lib/dep-guard/task-parser.js +318 -318
  145. package/lib/detect-agent.js +191 -191
  146. package/lib/detect-worktree.js +47 -47
  147. package/lib/file-hash.js +26 -26
  148. package/lib/husky-migration.js +450 -0
  149. package/lib/lefthook-check.js +65 -0
  150. package/lib/pat-setup.js +207 -0
  151. package/lib/plugin-catalog.js +350 -350
  152. package/lib/plugin-manager.js +166 -166
  153. package/lib/plugin-recommender.js +141 -141
  154. package/lib/project-discovery.js +491 -491
  155. package/lib/setup-action-log.js +139 -139
  156. package/lib/setup-summary-renderer.js +106 -106
  157. package/lib/setup-utils.js +96 -0
  158. package/lib/setup.js +192 -192
  159. package/lib/smart-merge.js +64 -0
  160. package/lib/symlink-utils.js +81 -0
  161. package/lib/workflow-profiles.js +197 -197
  162. package/package.json +131 -128
  163. package/scripts/beads-context.sh +291 -0
  164. package/scripts/beads-context.test.js +563 -0
  165. package/scripts/behavioral-judge.sh +378 -0
  166. package/scripts/benchmark.js +85 -0
  167. package/scripts/branch-protection.js +183 -0
  168. package/scripts/check-agents.js +172 -0
  169. package/scripts/commitlint.js +42 -0
  170. package/scripts/conflict-detect.sh +323 -0
  171. package/scripts/dep-guard-analyze.js +71 -0
  172. package/scripts/dep-guard.sh +811 -0
  173. package/scripts/eval_win.py +249 -0
  174. package/scripts/file-index.sh +399 -0
  175. package/scripts/github-beads-sync/comment.mjs +64 -0
  176. package/scripts/github-beads-sync/config.mjs +148 -0
  177. package/scripts/github-beads-sync/github-api.mjs +131 -0
  178. package/scripts/github-beads-sync/index.mjs +332 -0
  179. package/scripts/github-beads-sync/label-mapper.mjs +54 -0
  180. package/scripts/github-beads-sync/mapping.mjs +78 -0
  181. package/scripts/github-beads-sync/reverse-sync-cli.mjs +31 -0
  182. package/scripts/github-beads-sync/reverse-sync.mjs +138 -0
  183. package/scripts/github-beads-sync/run-bd.mjs +159 -0
  184. package/scripts/github-beads-sync/sanitize.mjs +121 -0
  185. package/scripts/github-beads-sync.config.json +26 -0
  186. package/scripts/improve-command.js +375 -0
  187. package/scripts/lib/eval-runner.js +229 -0
  188. package/scripts/lib/eval-schema.js +135 -0
  189. package/scripts/lib/eval-storage.js +78 -0
  190. package/scripts/lib/grading.js +203 -0
  191. package/scripts/lib/transcript-parser.js +63 -0
  192. package/scripts/lint.js +47 -0
  193. package/scripts/migrate-to-bun-test.js +412 -0
  194. package/scripts/run-command-eval.js +236 -0
  195. package/scripts/smart-status.sh +782 -0
  196. package/scripts/sync-commands.js +571 -0
  197. package/scripts/sync-utils.sh +460 -0
  198. package/scripts/test-dashboard.js +123 -0
  199. package/scripts/test.js +44 -0
  200. package/scripts/validate.sh +94 -0
  201. package/skills/parallel-deep-research/SKILL.md +108 -108
  202. package/skills/parallel-deep-research/evals/README.md +27 -27
  203. package/skills/parallel-deep-research/evals/evals.json +62 -62
  204. package/skills/sonarcloud-analysis/SKILL.md +171 -171
  205. package/skills/sonarcloud-analysis/evals/README.md +27 -27
  206. package/skills/sonarcloud-analysis/evals/evals.json +50 -50
  207. package/skills/sonarcloud-analysis/references/api-reference.md +466 -466
  208. package/.cursor/hooks/state/continual-learning-index.json +0 -19
  209. package/.cursor/hooks/state/continual-learning.json +0 -8
@@ -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 "$@"