@clipboard-health/ai-rules 2.15.2 → 2.15.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/babysit-pr/SKILL.md +17 -17
- package/skills/babysit-pr/scripts/_sentinel.sh +17 -0
- package/skills/babysit-pr/scripts/parseNitpicks.sh +80 -43
- package/skills/babysit-pr/scripts/postSentinelPrComment.sh +4 -9
- package/skills/babysit-pr/scripts/postSentinelReply.sh +4 -10
- package/skills/babysit-pr/scripts/unresolvedPrComments.sh +40 -35
- package/skills/unresolved-pr-comments/SKILL.md +2 -2
- package/skills/unresolved-pr-comments/scripts/parseNitpicks.sh +71 -31
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: babysit-pr
|
|
3
|
-
description: "Watch a PR through CI and review feedback: commit/push, wait for CI, auto-fix high-confidence failures, reply to active review threads, and summarize parsed CodeRabbit
|
|
3
|
+
description: "Watch a PR through CI and review feedback: commit/push, wait for CI, auto-fix high-confidence failures, reply to active review threads, and summarize parsed CodeRabbit review-body comments with sentinel-tagged comments. Runs once by default; pass a short interval like `30s` or `2m` for best-effort same-turn polling; longer cadences should use an external loop wrapper. Use when the user says 'babysit my PR', 'watch my PR', 'keep my PR moving', 'respond to comments', or 'loop on CI'."
|
|
4
4
|
argument-hint: "[interval]"
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Babysit PR
|
|
8
8
|
|
|
9
|
-
Watch one PR through CI, auto-fix high-confidence failures, and leave a paper-trail reply on every active review thread and CodeRabbit
|
|
9
|
+
Watch one PR through CI, auto-fix high-confidence failures, and leave a paper-trail reply on every active review thread and CodeRabbit review-body comment. Threads stay open for human resolution — this skill only posts replies, it never resolves.
|
|
10
10
|
|
|
11
11
|
This skill is self-contained: it does not invoke other skills. It works in Claude Code and Codex — no subagents, no `Skill` tool calls, no `!` command interpolation, no `$CLAUDE_PLUGIN_ROOT`.
|
|
12
12
|
|
|
@@ -33,7 +33,7 @@ Rules:
|
|
|
33
33
|
|
|
34
34
|
The skill uses two HTML-comment sentinels.
|
|
35
35
|
|
|
36
|
-
**Addressed sentinel**: `<!-- babysit-pr:addressed v1 -->`. Appended on its own line at the end of every reply the skill posts (both thread replies and the
|
|
36
|
+
**Addressed sentinel**: `<!-- babysit-pr:addressed v1 -->`. Appended on its own line at the end of every reply the skill posts (both thread replies and the CodeRabbit summary). This is how the skill knows, on re-runs, which threads and CodeRabbit review-body comments it already handled.
|
|
37
37
|
|
|
38
38
|
**Follow-up sentinel**: `<!-- babysit-pr:followup v1 -->`. Attached to replies that defer an out-of-scope comment as a tracked follow-up (see the Scope subsection and the Defer verdict in step 6). Grep `babysit-pr:followup` across PR conversation JSON to enumerate deferred items. This sentinel is additive — the post-reply scripts still append the `addressed` sentinel at the end, so a deferred thread is correctly machine-classified as addressed (the skill _has_ handled it — by deferring). Human reviewers and future sweeps distinguish deferred from resolved by looking for the follow-up sentinel.
|
|
39
39
|
|
|
@@ -50,7 +50,7 @@ The skill uses two HTML-comment sentinels.
|
|
|
50
50
|
|
|
51
51
|
The bot detection exists ONLY to downgrade the default for post-sentinel bot activity from `"active"` to `"uncertain"`. It NEVER suppresses bot comments or marks a thread `"addressed"` on its own — CodeRabbit's review content would be lost if it did.
|
|
52
52
|
|
|
53
|
-
For
|
|
53
|
+
For CodeRabbit review-body comments, the script emits a stable `fingerprint` per comment (sha256 of file + line + title + body, no timestamp). This includes CodeRabbit's Nitpick comments, Minor comments, and Outside diff range comments sections. Before posting a summary, search existing PR issue-comments for a prior babysit-pr sentinel comment that already contains those fingerprints; if every current fingerprint is already present in a prior sentinel comment, skip posting.
|
|
54
54
|
|
|
55
55
|
## One iteration
|
|
56
56
|
|
|
@@ -119,14 +119,14 @@ The output JSON has:
|
|
|
119
119
|
- `threads`: every unresolved review thread, with `threadId`, `replyToCommentDatabaseId`, `comments[]`, `lastBabysitSentinelAt`, `lastHumanCommentAt`, `lastBotCommentAt`, `postSentinelBotComments[]`, `postSentinelHumanComments[]`, and `activityState` (`"active"` / `"uncertain"` / `"addressed"`).
|
|
120
120
|
- `activeThreads`: threads where `activityState != "addressed"` — these need attention this iteration (active AND uncertain).
|
|
121
121
|
- `uncertainThreads`: just the uncertain subset. For each, read EVERY entry in `postSentinelBotComments` before deciding.
|
|
122
|
-
- `nitpickComments`: parsed CodeRabbit
|
|
122
|
+
- `nitpickComments`: parsed CodeRabbit review-body comments, each with a stable `fingerprint`. The field name is retained for compatibility, but it includes Nitpick comments, Minor comments, and Outside diff range comments.
|
|
123
123
|
- `totalActiveThreads`, `totalUncertainThreads`, `totalNitpicks`, `totalUnresolvedComments` for quick checks.
|
|
124
124
|
|
|
125
125
|
### Scope
|
|
126
126
|
|
|
127
|
-
This PR's review-feedback scope is strict by default. Steps 6 (threads) and 7 (
|
|
127
|
+
This PR's review-feedback scope is strict by default. Steps 6 (threads) and 7 (CodeRabbit review-body comments) classify each comment as in-scope or out-of-scope using this rule before choosing a verdict. Step 5 (CI) uses the broader CI-scope rule in that step, not this one — CI can legitimately fail on unchanged lines because the PR changed a contract or dependency path.
|
|
128
128
|
|
|
129
|
-
Build the changed-line set from `gh pr diff` once per iteration. Count changed diff lines on both sides: added lines in the new version, removed lines in the old version, and modified code represented by adjacent remove/add pairs. Do not count diff context lines. A reviewer comment or
|
|
129
|
+
Build the changed-line set from `gh pr diff` once per iteration. Count changed diff lines on both sides: added lines in the new version, removed lines in the old version, and modified code represented by adjacent remove/add pairs. Do not count diff context lines. A reviewer comment or CodeRabbit review-body comment is **in scope** when its anchor falls on a changed diff line on either side of the hunk. Deleted-line comments like "why remove this?" or "please add this back" are in scope by definition. For a range like `12-14`, any overlap with a changed diff line is in scope.
|
|
130
130
|
|
|
131
131
|
When matching review comments to hunks, use the anchor line provided by `unresolvedPrComments.sh`; it may be the current `line` or the script's fallback to `originalLine`. Compare that anchor against both new-side added ranges and old-side removed ranges.
|
|
132
132
|
|
|
@@ -199,19 +199,19 @@ For every thread in `activeThreads` (this includes both `"active"` and `"uncerta
|
|
|
199
199
|
- Does not meet the bar → **Defer** (new verdict). Record a one-line rationale and, if relevant, a pointer to where the concern lives.
|
|
200
200
|
- Disagree and Already-fixed can still apply to out-of-scope comments (e.g., reviewer asks for a refactor that's already landed on main, or misreads the code).
|
|
201
201
|
|
|
202
|
-
### 7. Assess
|
|
202
|
+
### 7. Assess CodeRabbit review-body comments
|
|
203
203
|
|
|
204
|
-
For every
|
|
204
|
+
For every parsed CodeRabbit review-body comment in `nitpickComments`:
|
|
205
205
|
|
|
206
206
|
- Check whether its `fingerprint` already appears in a prior babysit-pr sentinel comment on the PR. If yes, skip.
|
|
207
|
-
- **Classify scope** (in / out) using the Scope subsection. For CodeRabbit
|
|
207
|
+
- **Classify scope** (in / out) using the Scope subsection. For CodeRabbit ranges like `12-14`, any overlap with changed diff lines on either side of the hunk is in scope; no overlap is out of scope unless one of the explicit escape-hatch signals applies.
|
|
208
208
|
- Pick a verdict:
|
|
209
209
|
- In-scope → Agree / Disagree / Already fixed (as with threads). If Agree, apply the fix.
|
|
210
|
-
- Out-of-scope → apply the out-of-scope fix bar. Meets the bar → Agree and apply the fix, noting in the summary that it was fixed despite being out of scope. Does not meet the bar → **Defer**. A Deferred
|
|
210
|
+
- Out-of-scope → apply the out-of-scope fix bar. Meets the bar → Agree and apply the fix, noting in the summary that it was fixed despite being out of scope. Does not meet the bar → **Defer**. A Deferred CodeRabbit review-body comment does not get its own top-level comment; it goes into the summary under the **Deferred (out of scope)** heading (see step 9).
|
|
211
211
|
|
|
212
|
-
Deferred
|
|
212
|
+
Deferred CodeRabbit fingerprints still go into the fenced fingerprint block at the end of the summary alongside addressed ones, so future runs dedupe correctly — the comment is handled, just handled by deferring.
|
|
213
213
|
|
|
214
|
-
If no
|
|
214
|
+
If no CodeRabbit review-body comments remain after filtering, skip ONLY the top-level CodeRabbit summary comment in step 9. Still post thread replies for every non-Skip-reply thread from step 6.
|
|
215
215
|
|
|
216
216
|
### 8. Commit and push (if any edits)
|
|
217
217
|
|
|
@@ -256,18 +256,18 @@ For Defer replies, include the follow-up sentinel on its own line as shown. The
|
|
|
256
256
|
|
|
257
257
|
The script uses the `addPullRequestReviewThreadReply` GraphQL mutation. It does NOT resolve the thread.
|
|
258
258
|
|
|
259
|
-
If any
|
|
259
|
+
If any CodeRabbit review-body comments were assessed in step 7, post ONE top-level PR comment summarizing all of them:
|
|
260
260
|
|
|
261
261
|
```bash
|
|
262
262
|
bash scripts/postSentinelPrComment.sh "$PR_NUMBER" "$BODY"
|
|
263
263
|
```
|
|
264
264
|
|
|
265
|
-
The
|
|
265
|
+
The CodeRabbit summary body should:
|
|
266
266
|
|
|
267
267
|
- Group verdicts under **Agree / Disagree / Already fixed / Deferred (out of scope)** headings. Omit a heading if its list is empty.
|
|
268
|
-
- Under **Deferred (out of scope)**, list each deferred
|
|
268
|
+
- Under **Deferred (out of scope)**, list each deferred CodeRabbit review-body comment as a bullet, followed on its own line by `<!-- babysit-pr:followup v1 -->` so grep catches them individually.
|
|
269
269
|
- Include the commit URL for fixes.
|
|
270
|
-
- Include every current
|
|
270
|
+
- Include every current CodeRabbit review-body comment's `fingerprint` — addressed and deferred — in a fenced block at the end (one per line, before the sentinel) so future runs can dedupe. Deferred comments count as handled for dedupe purposes.
|
|
271
271
|
|
|
272
272
|
### 10. Summarize
|
|
273
273
|
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# _sentinel.sh — shared SENTINEL constant + append helper.
|
|
3
|
+
# Sourced by unresolvedPrComments.sh, postSentinelReply.sh, postSentinelPrComment.sh.
|
|
4
|
+
# Keeping the sentinel in one place prevents a version bump from silently
|
|
5
|
+
# diverging between the posting scripts and the reader's recency detector.
|
|
6
|
+
|
|
7
|
+
SENTINEL='<!-- babysit-pr:addressed v1 -->'
|
|
8
|
+
|
|
9
|
+
# Echo $1 with the sentinel appended on its own trailing paragraph, unless
|
|
10
|
+
# the body already ends with the sentinel.
|
|
11
|
+
ensure_sentinel() {
|
|
12
|
+
local body="$1"
|
|
13
|
+
case "$body" in
|
|
14
|
+
*"$SENTINEL") printf '%s' "$body" ;;
|
|
15
|
+
*) printf '%s\n\n%s' "$body" "$SENTINEL" ;;
|
|
16
|
+
esac
|
|
17
|
+
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# parseNitpicks.sh — Parse CodeRabbit
|
|
3
|
-
# Copied from plugins/core/skills/unresolved-pr-comments/scripts/parseNitpicks.sh
|
|
4
|
-
# with one addition: each emitted nitpick includes a stable `fingerprint` field
|
|
5
|
-
# (sha256 of file + normalized line range + title + body), so reposted reviews
|
|
6
|
-
# dedupe to the same fingerprint. Source review timestamps are kept as
|
|
7
|
-
# `createdAt` metadata but NOT included in the fingerprint.
|
|
2
|
+
# parseNitpicks.sh — Parse CodeRabbit review-body comments from PR review bodies.
|
|
8
3
|
#
|
|
9
|
-
#
|
|
4
|
+
# Each emitted comment includes a stable `fingerprint` field (sha256 of file +
|
|
5
|
+
# normalized line range + title + body), so reposted reviews dedupe to the same
|
|
6
|
+
# fingerprint. Source review timestamps are kept as `createdAt` metadata but
|
|
7
|
+
# NOT included in the fingerprint.
|
|
8
|
+
#
|
|
9
|
+
# Sourced by unresolvedPrComments.sh. Requires: perl with Digest::SHA + Encode.
|
|
10
10
|
|
|
11
11
|
extract_nitpick_comments() {
|
|
12
12
|
local reviews_json="$1"
|
|
@@ -27,7 +27,7 @@ my $latest_time = "";
|
|
|
27
27
|
for my $review (@$reviews) {
|
|
28
28
|
my $author = $review->{author}{login} // "";
|
|
29
29
|
my $body = $review->{body} // "";
|
|
30
|
-
next unless $author eq "coderabbitai" && $body
|
|
30
|
+
next unless $author eq "coderabbitai" && has_supported_sections($body);
|
|
31
31
|
my $created = $review->{createdAt} // "";
|
|
32
32
|
if ($created gt $latest_time) {
|
|
33
33
|
$latest_time = $created;
|
|
@@ -44,44 +44,62 @@ my $body = $latest_review->{body};
|
|
|
44
44
|
my $author = $latest_review->{author}{login} // "deleted-user";
|
|
45
45
|
my $created_at = $latest_review->{createdAt} // "";
|
|
46
46
|
|
|
47
|
-
my
|
|
48
|
-
unless (
|
|
47
|
+
my @sections = extract_review_body_comment_sections($body);
|
|
48
|
+
unless (@sections) {
|
|
49
49
|
print "[]";
|
|
50
50
|
exit 0;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
my @comments;
|
|
54
|
-
|
|
55
|
-
my $
|
|
56
|
-
my $
|
|
57
|
-
|
|
58
|
-
while ($
|
|
59
|
-
my $
|
|
60
|
-
my $
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
54
|
+
for my $section (@sections) {
|
|
55
|
+
my $section_content = $section->{content};
|
|
56
|
+
my $category = $section->{category};
|
|
57
|
+
|
|
58
|
+
while ($section_content =~ /<details>\s*<summary>([^<]+?)\s+\(\d+\)<\/summary>\s*<blockquote>([\s\S]*?)<\/blockquote>\s*<\/details>/g) {
|
|
59
|
+
my $raw_file_name = trim($1);
|
|
60
|
+
my $file_content = $2;
|
|
61
|
+
|
|
62
|
+
while ($file_content =~ /`(\d+(?:-\d+)?)`:\s*(?:_[^_]+_\s*\|\s*_[^_]+_\s*)?\*\*([^*]+)\*\*\s*([\s\S]*?)(?=---|\n`\d|<\/blockquote>|$)/g) {
|
|
63
|
+
my $line_range = $1;
|
|
64
|
+
my $title = trim($2);
|
|
65
|
+
my $clean_body = clean_comment_body(trim($3));
|
|
66
|
+
my $file_name = normalize_file_name($raw_file_name, $line_range);
|
|
67
|
+
|
|
68
|
+
# Fingerprint: file + normalized line + title + body (NO timestamp,
|
|
69
|
+
# NO author, NO category — reposted reviews must dedupe to the same
|
|
70
|
+
# fingerprint even if CodeRabbit relabels the section).
|
|
71
|
+
my $fingerprint_input = join("\n", $file_name, $line_range, $title, $clean_body);
|
|
72
|
+
my $fingerprint = substr(sha256_hex(encode_utf8($fingerprint_input)), 0, 16);
|
|
73
|
+
|
|
74
|
+
push @comments, {
|
|
75
|
+
author => $author,
|
|
76
|
+
body => "$title\n\n$clean_body",
|
|
77
|
+
category => $category,
|
|
78
|
+
createdAt => $created_at,
|
|
79
|
+
file => $file_name,
|
|
80
|
+
fingerprint => $fingerprint,
|
|
81
|
+
line => $line_range,
|
|
82
|
+
title => $title,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
77
85
|
}
|
|
78
86
|
}
|
|
79
87
|
|
|
80
88
|
print encode_json(\@comments);
|
|
81
89
|
|
|
82
|
-
sub
|
|
90
|
+
sub has_supported_sections {
|
|
91
|
+
my ($text) = @_;
|
|
92
|
+
$text = strip_markdown_blockquote_prefixes($text);
|
|
93
|
+
return $text =~ /<summary>\s*[^<]*(?:Nitpick comments|Minor comments|Outside diff range comments)\s*\(\d+\)<\/summary>\s*<blockquote>/i;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
sub extract_review_body_comment_sections {
|
|
83
97
|
my ($text) = @_;
|
|
84
|
-
|
|
98
|
+
$text = strip_markdown_blockquote_prefixes($text);
|
|
99
|
+
|
|
100
|
+
my @sections;
|
|
101
|
+
while ($text =~ /<summary>\s*[^<]*(Nitpick comments|Minor comments|Outside diff range comments)\s*\(\d+\)<\/summary>\s*<blockquote>/ig) {
|
|
102
|
+
my $category = section_category($1);
|
|
85
103
|
my $content_start = $+[0];
|
|
86
104
|
my $after = substr($text, $content_start);
|
|
87
105
|
|
|
@@ -96,11 +114,36 @@ sub extract_nitpick_section {
|
|
|
96
114
|
for my $tag (@tags) {
|
|
97
115
|
$depth += $tag->[1] ? 1 : -1;
|
|
98
116
|
if ($depth == 0) {
|
|
99
|
-
|
|
117
|
+
push @sections, {
|
|
118
|
+
category => $category,
|
|
119
|
+
content => substr($after, 0, $tag->[0]),
|
|
120
|
+
};
|
|
121
|
+
last;
|
|
100
122
|
}
|
|
101
123
|
}
|
|
102
124
|
}
|
|
103
|
-
return
|
|
125
|
+
return @sections;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
sub section_category {
|
|
129
|
+
my ($label) = @_;
|
|
130
|
+
return "nitpick" if $label =~ /Nitpick comments/i;
|
|
131
|
+
return "minor" if $label =~ /Minor comments/i;
|
|
132
|
+
return "outside-diff" if $label =~ /Outside diff range comments/i;
|
|
133
|
+
return "unknown";
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
sub normalize_file_name {
|
|
137
|
+
my ($file_name, $line_range) = @_;
|
|
138
|
+
my $suffix = "-" . $line_range;
|
|
139
|
+
$file_name =~ s/\Q$suffix\E$//;
|
|
140
|
+
return $file_name;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
sub strip_markdown_blockquote_prefixes {
|
|
144
|
+
my ($text) = @_;
|
|
145
|
+
$text =~ s/^[ \t]*>[ \t]?//mg;
|
|
146
|
+
return $text;
|
|
104
147
|
}
|
|
105
148
|
|
|
106
149
|
sub clean_comment_body {
|
|
@@ -124,9 +167,3 @@ sub trim {
|
|
|
124
167
|
}
|
|
125
168
|
'
|
|
126
169
|
}
|
|
127
|
-
|
|
128
|
-
# Extract code scanning alert number from comment body.
|
|
129
|
-
extract_code_scanning_alert_number() {
|
|
130
|
-
local body="$1"
|
|
131
|
-
printf '%s' "$body" | perl -ne 'print $1 if m{/code-scanning/(\d+)}'
|
|
132
|
-
}
|
|
@@ -8,7 +8,9 @@
|
|
|
8
8
|
|
|
9
9
|
set -euo pipefail
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
12
|
+
# shellcheck source=_sentinel.sh
|
|
13
|
+
source "${SCRIPT_DIR}/_sentinel.sh"
|
|
12
14
|
|
|
13
15
|
if [ $# -lt 2 ]; then
|
|
14
16
|
printf '{"error":"Usage: postSentinelPrComment.sh <pr-number> <body>"}\n' >&2
|
|
@@ -27,14 +29,7 @@ if [ -z "$BODY" ]; then
|
|
|
27
29
|
exit 2
|
|
28
30
|
fi
|
|
29
31
|
|
|
30
|
-
|
|
31
|
-
*"$SENTINEL") ;;
|
|
32
|
-
*)
|
|
33
|
-
BODY="${BODY}
|
|
34
|
-
|
|
35
|
-
${SENTINEL}"
|
|
36
|
-
;;
|
|
37
|
-
esac
|
|
32
|
+
BODY="$(ensure_sentinel "$BODY")"
|
|
38
33
|
|
|
39
34
|
repo_json="$(gh repo view --json owner,name 2>/dev/null)" || {
|
|
40
35
|
printf '{"error":"Could not determine repository."}\n' >&2
|
|
@@ -11,7 +11,9 @@
|
|
|
11
11
|
|
|
12
12
|
set -euo pipefail
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
15
|
+
# shellcheck source=_sentinel.sh
|
|
16
|
+
source "${SCRIPT_DIR}/_sentinel.sh"
|
|
15
17
|
|
|
16
18
|
if [ $# -lt 2 ]; then
|
|
17
19
|
printf '{"error":"Usage: postSentinelReply.sh <thread-id> <body>"}\n' >&2
|
|
@@ -30,15 +32,7 @@ if [ -z "$BODY" ]; then
|
|
|
30
32
|
exit 2
|
|
31
33
|
fi
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
case "$BODY" in
|
|
35
|
-
*"$SENTINEL") ;;
|
|
36
|
-
*)
|
|
37
|
-
BODY="${BODY}
|
|
38
|
-
|
|
39
|
-
${SENTINEL}"
|
|
40
|
-
;;
|
|
41
|
-
esac
|
|
35
|
+
BODY="$(ensure_sentinel "$BODY")"
|
|
42
36
|
|
|
43
37
|
MUTATION='
|
|
44
38
|
mutation($threadId: ID!, $body: String!) {
|
|
@@ -11,8 +11,8 @@ set -euo pipefail
|
|
|
11
11
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
12
12
|
# shellcheck source=parseNitpicks.sh
|
|
13
13
|
source "${SCRIPT_DIR}/parseNitpicks.sh"
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
# shellcheck source=_sentinel.sh
|
|
15
|
+
source "${SCRIPT_DIR}/_sentinel.sh"
|
|
16
16
|
|
|
17
17
|
exec 3>&1
|
|
18
18
|
|
|
@@ -283,7 +283,7 @@ main() {
|
|
|
283
283
|
]
|
|
284
284
|
')"
|
|
285
285
|
|
|
286
|
-
# Flattened unresolved_comments
|
|
286
|
+
# Flattened unresolved_comments — retained for backward compat with the prose summary.
|
|
287
287
|
# Includes comments from "active" AND "uncertain" threads so the agent never misses new feedback.
|
|
288
288
|
local all_unresolved
|
|
289
289
|
all_unresolved="$(printf '%s' "$threads_json" | jq '[
|
|
@@ -300,41 +300,46 @@ main() {
|
|
|
300
300
|
}
|
|
301
301
|
]')"
|
|
302
302
|
|
|
303
|
-
# Filter out fixed code-scanning alerts from github-advanced-security
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
"
|
|
320
|
-
local comment_body alert_number
|
|
321
|
-
comment_body="$(printf '%s' "$comment" | jq -r '.body')"
|
|
322
|
-
alert_number="$(extract_code_scanning_alert_number "$comment_body")"
|
|
323
|
-
if [ -n "$alert_number" ]; then
|
|
324
|
-
if is_code_scanning_alert_fixed "$owner" "$repo" "$alert_number"; then
|
|
325
|
-
keep=false
|
|
326
|
-
fi
|
|
327
|
-
fi
|
|
328
|
-
;;
|
|
329
|
-
esac
|
|
330
|
-
|
|
331
|
-
if [ "$keep" = true ]; then
|
|
332
|
-
unresolved_comments="$(printf '%s' "$unresolved_comments" | jq --argjson c "$comment" '. + [$c]')"
|
|
303
|
+
# Filter out fixed code-scanning alerts from github-advanced-security.
|
|
304
|
+
# Two-pass: collect unique alert numbers, query each once, then drop matching
|
|
305
|
+
# comments in a single jq pass. Avoids the O(N²) rebuild and duplicate gh api
|
|
306
|
+
# calls the naive per-comment loop would incur.
|
|
307
|
+
# github-advanced-security posts under either login depending on account type
|
|
308
|
+
# (app vs direct) — both forms match below.
|
|
309
|
+
local security_alerts
|
|
310
|
+
security_alerts="$(printf '%s' "$all_unresolved" | jq -r '
|
|
311
|
+
.[]
|
|
312
|
+
| select(.author == "github-advanced-security" or .author == "github-advanced-security[bot]")
|
|
313
|
+
| try (.body | capture("/code-scanning/(?<n>[0-9]+)") | .n)
|
|
314
|
+
' | sort -u)"
|
|
315
|
+
|
|
316
|
+
local fixed_alerts="" alert_number
|
|
317
|
+
for alert_number in $security_alerts; do
|
|
318
|
+
if is_code_scanning_alert_fixed "$owner" "$repo" "$alert_number"; then
|
|
319
|
+
fixed_alerts="${fixed_alerts} ${alert_number}"
|
|
333
320
|
fi
|
|
334
|
-
|
|
335
|
-
i=$((i + 1))
|
|
336
321
|
done
|
|
337
322
|
|
|
323
|
+
local unresolved_comments
|
|
324
|
+
if [ -z "$fixed_alerts" ]; then
|
|
325
|
+
unresolved_comments="$all_unresolved"
|
|
326
|
+
else
|
|
327
|
+
# capture() on a non-matching string produces ZERO outputs (not null, not an
|
|
328
|
+
# error). Without the `// null` guard below, `as $n` would bind to nothing
|
|
329
|
+
# and the map entry would silently collapse to empty — dropping
|
|
330
|
+
# github-advanced-security comments that reference no code-scanning URL.
|
|
331
|
+
unresolved_comments="$(printf '%s' "$all_unresolved" | jq --arg fixed "$fixed_alerts" '
|
|
332
|
+
($fixed | split(" ") | map(select(length > 0))) as $fixedSet
|
|
333
|
+
| map(
|
|
334
|
+
. as $c
|
|
335
|
+
| if ($c.author == "github-advanced-security" or $c.author == "github-advanced-security[bot]") then
|
|
336
|
+
(((try ($c.body | capture("/code-scanning/(?<n>[0-9]+)") | .n)) // null)) as $n
|
|
337
|
+
| if ($n != null and ($n | IN($fixedSet[]))) then empty else $c end
|
|
338
|
+
else $c end
|
|
339
|
+
)
|
|
340
|
+
')"
|
|
341
|
+
fi
|
|
342
|
+
|
|
338
343
|
# Nitpicks from coderabbit review bodies
|
|
339
344
|
local reviews_json
|
|
340
345
|
reviews_json="$(printf '%s' "$response" | jq '[.data.repository.pullRequest.reviews.nodes[]]')"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: unresolved-pr-comments
|
|
3
|
-
description: "Get unresolved review comments from a GitHub pull request. Use this skill when the user asks about PR feedback, review comments, unresolved threads, what reviewers said, CodeRabbit
|
|
3
|
+
description: "Get unresolved review comments from a GitHub pull request. Use this skill when the user asks about PR feedback, review comments, unresolved threads, what reviewers said, CodeRabbit review-body comments, or wants to address PR review feedback. Also use when the user says 'check my PR', 'what's left on my PR', or 'resolve comments'."
|
|
4
4
|
argument-hint: "[pr-number]"
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -26,7 +26,7 @@ Using the JSON output from the script:
|
|
|
26
26
|
|
|
27
27
|
1. **If error**: Display the error message and suggest the fix
|
|
28
28
|
2. **If no comments**: Report the PR has no pending feedback
|
|
29
|
-
3. **If comments exist**: Present a brief summary (e.g., "Found 3 unresolved comments and 5
|
|
29
|
+
3. **If comments exist**: Present a brief summary (e.g., "Found 3 unresolved comments and 5 CodeRabbit review-body comments")
|
|
30
30
|
|
|
31
31
|
Then, for EVERY comment (both `unresolvedComments` AND `nitpickComments`):
|
|
32
32
|
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# parseNitpicks.sh — Parse CodeRabbit
|
|
2
|
+
# parseNitpicks.sh — Parse CodeRabbit review-body comments from PR review bodies.
|
|
3
3
|
# Sourced by unresolvedPrComments.sh. Requires: jq, perl.
|
|
4
4
|
|
|
5
|
-
# Extract
|
|
6
|
-
# Outputs a JSON array of
|
|
5
|
+
# Extract CodeRabbit review-body comments from reviews JSON (passed via stdin).
|
|
6
|
+
# Outputs a JSON array of comment objects. The function name is retained for
|
|
7
|
+
# backward compatibility with existing skill scripts.
|
|
7
8
|
extract_nitpick_comments() {
|
|
8
9
|
local reviews_json="$1"
|
|
9
10
|
|
|
@@ -16,13 +17,13 @@ local $/;
|
|
|
16
17
|
my $reviews_json = <STDIN>;
|
|
17
18
|
my $reviews = decode_json($reviews_json);
|
|
18
19
|
|
|
19
|
-
# Find latest coderabbitai review with
|
|
20
|
+
# Find latest coderabbitai review with supported review-body comment sections.
|
|
20
21
|
my $latest_review;
|
|
21
22
|
my $latest_time = "";
|
|
22
23
|
for my $review (@$reviews) {
|
|
23
24
|
my $author = $review->{author}{login} // "";
|
|
24
25
|
my $body = $review->{body} // "";
|
|
25
|
-
next unless $author eq "coderabbitai" && $body
|
|
26
|
+
next unless $author eq "coderabbitai" && has_supported_sections($body);
|
|
26
27
|
my $created = $review->{createdAt} // "";
|
|
27
28
|
if ($created gt $latest_time) {
|
|
28
29
|
$latest_time = $created;
|
|
@@ -39,44 +40,58 @@ my $body = $latest_review->{body};
|
|
|
39
40
|
my $author = $latest_review->{author}{login} // "deleted-user";
|
|
40
41
|
my $created_at = $latest_review->{createdAt} // "";
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
unless (defined $nitpick_content) {
|
|
43
|
+
my @sections = extract_review_body_comment_sections($body);
|
|
44
|
+
unless (@sections) {
|
|
45
45
|
print "[]";
|
|
46
46
|
exit 0;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
# Extract file sections: <details><summary>filename (count)</summary><blockquote>...</blockquote></details>
|
|
50
49
|
my @comments;
|
|
51
|
-
|
|
52
|
-
my $
|
|
53
|
-
my $
|
|
54
|
-
|
|
55
|
-
# Extract
|
|
56
|
-
while ($
|
|
57
|
-
my $
|
|
58
|
-
my $
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
50
|
+
for my $section (@sections) {
|
|
51
|
+
my $section_content = $section->{content};
|
|
52
|
+
my $category = $section->{category};
|
|
53
|
+
|
|
54
|
+
# Extract file sections: <details><summary>filename (count)</summary><blockquote>...</blockquote></details>
|
|
55
|
+
while ($section_content =~ /<details>\s*<summary>([^<]+?)\s+\(\d+\)<\/summary>\s*<blockquote>([\s\S]*?)<\/blockquote>\s*<\/details>/g) {
|
|
56
|
+
my $raw_file_name = trim($1);
|
|
57
|
+
my $file_content = $2;
|
|
58
|
+
|
|
59
|
+
# Extract individual comments: `line-range`: severity metadata, **title**, body
|
|
60
|
+
while ($file_content =~ /`(\d+(?:-\d+)?)`:\s*(?:_[^_]+_\s*\|\s*_[^_]+_\s*)?\*\*([^*]+)\*\*\s*([\s\S]*?)(?=---|\n`\d|<\/blockquote>|$)/g) {
|
|
61
|
+
my $line_range = $1;
|
|
62
|
+
my $title = trim($2);
|
|
63
|
+
my $clean_body = clean_comment_body(trim($3));
|
|
64
|
+
my $file_name = normalize_file_name($raw_file_name, $line_range);
|
|
65
|
+
push @comments, {
|
|
66
|
+
author => $author,
|
|
67
|
+
body => "$title\n\n$clean_body",
|
|
68
|
+
category => $category,
|
|
69
|
+
createdAt => $created_at,
|
|
70
|
+
file => $file_name,
|
|
71
|
+
line => $line_range,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
67
74
|
}
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
print encode_json(\@comments);
|
|
71
78
|
|
|
72
|
-
sub
|
|
79
|
+
sub has_supported_sections {
|
|
73
80
|
my ($text) = @_;
|
|
74
|
-
|
|
75
|
-
|
|
81
|
+
$text = strip_markdown_blockquote_prefixes($text);
|
|
82
|
+
return $text =~ /<summary>\s*[^<]*(?:Nitpick comments|Minor comments|Outside diff range comments)\s*\(\d+\)<\/summary>\s*<blockquote>/i;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
sub extract_review_body_comment_sections {
|
|
86
|
+
my ($text) = @_;
|
|
87
|
+
$text = strip_markdown_blockquote_prefixes($text);
|
|
88
|
+
|
|
89
|
+
my @sections;
|
|
90
|
+
while ($text =~ /<summary>\s*[^<]*(Nitpick comments|Minor comments|Outside diff range comments)\s*\(\d+\)<\/summary>\s*<blockquote>/ig) {
|
|
91
|
+
my $category = section_category($1);
|
|
76
92
|
my $content_start = $+[0];
|
|
77
93
|
my $after = substr($text, $content_start);
|
|
78
94
|
|
|
79
|
-
# Find matching closing blockquote by tracking depth
|
|
80
95
|
my $depth = 1;
|
|
81
96
|
my @tags;
|
|
82
97
|
while ($after =~ /(<blockquote>|<\/blockquote>)/gi) {
|
|
@@ -88,11 +103,36 @@ sub extract_nitpick_section {
|
|
|
88
103
|
for my $tag (@tags) {
|
|
89
104
|
$depth += $tag->[1] ? 1 : -1;
|
|
90
105
|
if ($depth == 0) {
|
|
91
|
-
|
|
106
|
+
push @sections, {
|
|
107
|
+
category => $category,
|
|
108
|
+
content => substr($after, 0, $tag->[0]),
|
|
109
|
+
};
|
|
110
|
+
last;
|
|
92
111
|
}
|
|
93
112
|
}
|
|
94
113
|
}
|
|
95
|
-
return
|
|
114
|
+
return @sections;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
sub section_category {
|
|
118
|
+
my ($label) = @_;
|
|
119
|
+
return "nitpick" if $label =~ /Nitpick comments/i;
|
|
120
|
+
return "minor" if $label =~ /Minor comments/i;
|
|
121
|
+
return "outside-diff" if $label =~ /Outside diff range comments/i;
|
|
122
|
+
return "unknown";
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
sub normalize_file_name {
|
|
126
|
+
my ($file_name, $line_range) = @_;
|
|
127
|
+
my $suffix = "-" . $line_range;
|
|
128
|
+
$file_name =~ s/\Q$suffix\E$//;
|
|
129
|
+
return $file_name;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
sub strip_markdown_blockquote_prefixes {
|
|
133
|
+
my ($text) = @_;
|
|
134
|
+
$text =~ s/^[ \t]*>[ \t]?//mg;
|
|
135
|
+
return $text;
|
|
96
136
|
}
|
|
97
137
|
|
|
98
138
|
sub clean_comment_body {
|