forge-workflow 0.0.1

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 (105) hide show
  1. package/.claude/commands/dev.md +314 -0
  2. package/.claude/commands/plan.md +389 -0
  3. package/.claude/commands/premerge.md +179 -0
  4. package/.claude/commands/research.md +42 -0
  5. package/.claude/commands/review.md +442 -0
  6. package/.claude/commands/rollback.md +721 -0
  7. package/.claude/commands/ship.md +134 -0
  8. package/.claude/commands/sonarcloud.md +152 -0
  9. package/.claude/commands/status.md +77 -0
  10. package/.claude/commands/validate.md +237 -0
  11. package/.claude/commands/verify.md +221 -0
  12. package/.claude/rules/greptile-review-process.md +285 -0
  13. package/.claude/rules/workflow.md +105 -0
  14. package/.claude/scripts/greptile-resolve.sh +526 -0
  15. package/.claude/scripts/load-env.sh +32 -0
  16. package/.forge/hooks/check-tdd.js +240 -0
  17. package/.github/PLUGIN_TEMPLATE.json +32 -0
  18. package/.mcp.json.example +12 -0
  19. package/AGENTS.md +169 -0
  20. package/CLAUDE.md +99 -0
  21. package/LICENSE +21 -0
  22. package/README.md +414 -0
  23. package/bin/forge-cmd.js +313 -0
  24. package/bin/forge-validate.js +303 -0
  25. package/bin/forge.js +4228 -0
  26. package/docs/AGENT_INSTALL_PROMPT.md +342 -0
  27. package/docs/ENHANCED_ONBOARDING.md +602 -0
  28. package/docs/EXAMPLES.md +482 -0
  29. package/docs/GREPTILE_SETUP.md +400 -0
  30. package/docs/MANUAL_REVIEW_GUIDE.md +106 -0
  31. package/docs/ROADMAP.md +359 -0
  32. package/docs/SETUP.md +632 -0
  33. package/docs/TOOLCHAIN.md +849 -0
  34. package/docs/VALIDATION.md +363 -0
  35. package/docs/WORKFLOW.md +400 -0
  36. package/docs/planning/PROGRESS.md +396 -0
  37. package/docs/plans/.gitkeep +0 -0
  38. package/docs/plans/2026-02-27-forge-test-suite-v2-decisions.md +21 -0
  39. package/docs/plans/2026-02-27-forge-test-suite-v2-design.md +362 -0
  40. package/docs/plans/2026-02-27-forge-test-suite-v2-tasks.md +343 -0
  41. package/docs/plans/2026-03-02-superpowers-gaps-decisions.md +26 -0
  42. package/docs/plans/2026-03-02-superpowers-gaps-design.md +239 -0
  43. package/docs/plans/2026-03-02-superpowers-gaps-tasks.md +260 -0
  44. package/docs/plans/2026-03-04-agent-command-parity-design.md +163 -0
  45. package/docs/plans/2026-03-04-verify-worktree-cleanup-decisions.md +7 -0
  46. package/docs/plans/2026-03-04-verify-worktree-cleanup-design.md +165 -0
  47. package/docs/plans/2026-03-05-forge-uto-decisions.md +6 -0
  48. package/docs/plans/2026-03-05-forge-uto-design.md +116 -0
  49. package/docs/plans/2026-03-05-forge-uto-tasks.md +244 -0
  50. package/docs/plans/2026-03-10-command-creator-and-eval-decisions.md +52 -0
  51. package/docs/plans/2026-03-10-command-creator-and-eval-design.md +350 -0
  52. package/docs/plans/2026-03-10-command-creator-and-eval-tasks.md +426 -0
  53. package/docs/plans/2026-03-10-stale-workflow-refs-decisions.md +8 -0
  54. package/docs/plans/2026-03-10-stale-workflow-refs-design.md +80 -0
  55. package/docs/plans/2026-03-10-stale-workflow-refs-tasks.md +90 -0
  56. package/docs/plans/2026-03-14-beads-plan-context-decisions.md +9 -0
  57. package/docs/plans/2026-03-14-beads-plan-context-design.md +171 -0
  58. package/docs/plans/2026-03-14-beads-plan-context-tasks.md +160 -0
  59. package/docs/plans/2026-03-14-skill-eval-loop-decisions.md +33 -0
  60. package/docs/plans/2026-03-14-skill-eval-loop-design.md +118 -0
  61. package/docs/plans/2026-03-14-skill-eval-loop-results.md +78 -0
  62. package/docs/plans/2026-03-14-skill-eval-loop-tasks.md +160 -0
  63. package/docs/plans/2026-03-15-agent-command-parity-v2-decisions.md +11 -0
  64. package/docs/plans/2026-03-15-agent-command-parity-v2-design.md +145 -0
  65. package/docs/plans/2026-03-15-agent-command-parity-v2-tasks.md +211 -0
  66. package/docs/research/TEMPLATE.md +292 -0
  67. package/docs/research/advanced-testing.md +297 -0
  68. package/docs/research/agent-permissions.md +167 -0
  69. package/docs/research/dependency-chain.md +328 -0
  70. package/docs/research/forge-workflow-v2.md +550 -0
  71. package/docs/research/plugin-architecture.md +772 -0
  72. package/docs/research/pr4-cli-automation.md +326 -0
  73. package/docs/research/premerge-verify-restructure.md +205 -0
  74. package/docs/research/skills-restructure.md +508 -0
  75. package/docs/research/sonarcloud-perfection-plan.md +166 -0
  76. package/docs/research/sonarcloud-quality-gate.md +184 -0
  77. package/docs/research/superpowers-integration.md +403 -0
  78. package/docs/research/superpowers.md +319 -0
  79. package/docs/research/test-environment.md +519 -0
  80. package/install.sh +1062 -0
  81. package/lefthook.yml +39 -0
  82. package/lib/agents/README.md +198 -0
  83. package/lib/agents/claude.plugin.json +28 -0
  84. package/lib/agents/cline.plugin.json +22 -0
  85. package/lib/agents/codex.plugin.json +19 -0
  86. package/lib/agents/copilot.plugin.json +24 -0
  87. package/lib/agents/cursor.plugin.json +25 -0
  88. package/lib/agents/kilocode.plugin.json +22 -0
  89. package/lib/agents/opencode.plugin.json +20 -0
  90. package/lib/agents/roo.plugin.json +23 -0
  91. package/lib/agents-config.js +2112 -0
  92. package/lib/commands/dev.js +513 -0
  93. package/lib/commands/plan.js +696 -0
  94. package/lib/commands/recommend.js +119 -0
  95. package/lib/commands/ship.js +377 -0
  96. package/lib/commands/status.js +378 -0
  97. package/lib/commands/validate.js +602 -0
  98. package/lib/context-merge.js +359 -0
  99. package/lib/plugin-catalog.js +360 -0
  100. package/lib/plugin-manager.js +166 -0
  101. package/lib/plugin-recommender.js +141 -0
  102. package/lib/project-discovery.js +491 -0
  103. package/lib/setup.js +118 -0
  104. package/lib/workflow-profiles.js +203 -0
  105. package/package.json +115 -0
@@ -0,0 +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 "$@"
@@ -0,0 +1,32 @@
1
+ #!/bin/bash
2
+ # Load environment variables from .env.local files
3
+ # Works on Windows Git Bash and Unix systems
4
+
5
+ # Function to load env file
6
+ load_env_file() {
7
+ local env_file="$1"
8
+ if [ -f "$env_file" ]; then
9
+ # Export each line that matches KEY=VALUE pattern (skip comments and empty lines)
10
+ while IFS='=' read -r key value; do
11
+ # Skip comments and empty lines
12
+ [[ "$key" =~ ^#.*$ ]] && continue
13
+ [[ -z "$key" ]] && continue
14
+ # Remove quotes from value
15
+ value="${value%\"}"
16
+ value="${value#\"}"
17
+ value="${value%\'}"
18
+ value="${value#\'}"
19
+ # Export the variable
20
+ export "$key=$value"
21
+ done < "$env_file"
22
+ fi
23
+ }
24
+
25
+ # Load from project root .env.local
26
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
27
+ PROJECT_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
28
+
29
+ load_env_file "$PROJECT_ROOT/.env.local"
30
+ load_env_file "$PROJECT_ROOT/next-app/.env.local"
31
+
32
+ echo "Environment loaded. PARALLEL_API_KEY length: ${#PARALLEL_API_KEY}"