@clipboard-health/ai-rules 2.20.4 → 2.20.6
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
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
|
|
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 automated review-body comments with sentinel-tagged comments. Runs one pass against the current branch's PR; pass a PR number or URL to `gh pr checkout` that PR first. Use when the user says 'babysit my PR', 'babysit PR 482', 'watch my PR', 'keep my PR moving', or 'respond to comments'."
|
|
4
4
|
argument-hint: "[pr-number-or-url]"
|
|
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
|
|
9
|
+
Watch one PR through CI, auto-fix high-confidence failures, and leave a paper-trail reply on every active review thread and automated 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
|
|
|
@@ -27,7 +27,7 @@ This skill always runs exactly one pass. It never waits or repeats internally. F
|
|
|
27
27
|
|
|
28
28
|
The skill uses two HTML-comment sentinels.
|
|
29
29
|
|
|
30
|
-
**Addressed sentinel**: `<!-- babysit-pr:addressed v1 core@3.4.1 -->`. The `core@<X.Y.Z>` suffix records which plugin version produced the reply. Appended on its own line at the end of every reply the skill posts (both thread replies and the
|
|
30
|
+
**Addressed sentinel**: `<!-- babysit-pr:addressed v1 core@3.4.1 -->`. The `core@<X.Y.Z>` suffix records which plugin version produced the reply. Appended on its own line at the end of every reply the skill posts (both thread replies and the review-body summary). This is how the skill knows, on re-runs, which threads and automated review-body comments it already handled. Dedupe matches by the version-agnostic prefix `<!-- babysit-pr:addressed v1` followed by a single space, so pre-versioning sentinels left by earlier plugin versions are still recognized. Grep `babysit-pr:addressed v1` (without `-->`) to find sentinels regardless of version; grep `babysit-pr:addressed v1 core@3.4.1` to find ones from a specific version.
|
|
31
31
|
|
|
32
32
|
**Follow-up sentinel**: `<!-- babysit-pr:followup v1 core@3.4.1 -->`. 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.
|
|
33
33
|
|
|
@@ -42,9 +42,9 @@ The skill uses two HTML-comment sentinels.
|
|
|
42
42
|
|
|
43
43
|
**Bot detection** uses two signals (union): GraphQL `author.__typename == "Bot"` (primary — catches every GitHub-tagged bot, including ones not on our allowlist), plus a name allowlist (for bots that post via a User-type service account). An unknown bot never falls through to human classification, so a new review bot won't cause an infinite re-reply loop.
|
|
44
44
|
|
|
45
|
-
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 —
|
|
45
|
+
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 — review-bot content would be lost if it did.
|
|
46
46
|
|
|
47
|
-
For
|
|
47
|
+
For automated 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, plus Mendral `Needs attention` review bodies that include a file/line anchor. 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.
|
|
48
48
|
|
|
49
49
|
## One iteration
|
|
50
50
|
|
|
@@ -135,14 +135,14 @@ The output JSON has:
|
|
|
135
135
|
- `threads`: every unresolved review thread, with `threadId`, `replyToCommentDatabaseId`, `comments[]`, `lastBabysitSentinelAt`, `lastHumanCommentAt`, `lastBotCommentAt`, `postSentinelBotComments[]`, `postSentinelHumanComments[]`, and `activityState` (`"active"` / `"uncertain"` / `"addressed"`).
|
|
136
136
|
- `activeThreads`: threads where `activityState != "addressed"` — these need attention this iteration (active AND uncertain).
|
|
137
137
|
- `uncertainThreads`: just the uncertain subset. For each, read EVERY entry in `postSentinelBotComments` before deciding.
|
|
138
|
-
- `nitpickComments`: parsed
|
|
138
|
+
- `nitpickComments`: parsed automated review-body comments, each with a stable `fingerprint`. The field name is retained for compatibility; it includes CodeRabbit Nitpick, Minor, and Outside diff range comments, plus Mendral `Needs attention` review bodies that include a file/line anchor.
|
|
139
139
|
- `totalActiveThreads`, `totalUncertainThreads`, `totalNitpicks`, `totalUnresolvedComments` for quick checks.
|
|
140
140
|
|
|
141
141
|
### Scope
|
|
142
142
|
|
|
143
|
-
This PR's review-feedback scope is strict by default. Steps 6 (threads) and 7 (
|
|
143
|
+
This PR's review-feedback scope is strict by default. Steps 6 (threads) and 7 (automated 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.
|
|
144
144
|
|
|
145
|
-
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
|
|
145
|
+
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 automated 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.
|
|
146
146
|
|
|
147
147
|
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.
|
|
148
148
|
|
|
@@ -215,19 +215,19 @@ For every thread in `activeThreads` (this includes both `"active"` and `"uncerta
|
|
|
215
215
|
- Does not meet the bar → **Defer** (new verdict). Record a one-line rationale and, if relevant, a pointer to where the concern lives.
|
|
216
216
|
- 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).
|
|
217
217
|
|
|
218
|
-
### 7. Assess
|
|
218
|
+
### 7. Assess automated review-body comments
|
|
219
219
|
|
|
220
|
-
For every parsed
|
|
220
|
+
For every parsed automated review-body comment in `nitpickComments`:
|
|
221
221
|
|
|
222
222
|
- Check whether its `fingerprint` already appears in a prior babysit-pr sentinel comment on the PR. If yes, skip.
|
|
223
|
-
- **Classify scope** (in / out) using the Scope subsection. For
|
|
223
|
+
- **Classify scope** (in / out) using the Scope subsection. For 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.
|
|
224
224
|
- Pick a verdict:
|
|
225
225
|
- In-scope → Agree / Disagree / Already fixed (as with threads). If Agree, apply the fix.
|
|
226
|
-
- 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
|
|
226
|
+
- 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 automated 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).
|
|
227
227
|
|
|
228
|
-
Deferred
|
|
228
|
+
Deferred review-body 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.
|
|
229
229
|
|
|
230
|
-
If no
|
|
230
|
+
If no automated review-body comments remain after filtering, skip ONLY the top-level review-body summary comment in step 9. Still post thread replies for every non-Skip-reply thread from step 6.
|
|
231
231
|
|
|
232
232
|
### 8. Commit and push (if any edits)
|
|
233
233
|
|
|
@@ -272,18 +272,18 @@ For Defer replies, include the follow-up sentinel on its own line as shown. The
|
|
|
272
272
|
|
|
273
273
|
The script uses the `addPullRequestReviewThreadReply` GraphQL mutation. It does NOT resolve the thread.
|
|
274
274
|
|
|
275
|
-
If any
|
|
275
|
+
If any automated review-body comments were assessed in step 7, post ONE top-level PR comment summarizing all of them:
|
|
276
276
|
|
|
277
277
|
```bash
|
|
278
278
|
bash scripts/postSentinelPrComment.sh "$PR_NUMBER" "$BODY"
|
|
279
279
|
```
|
|
280
280
|
|
|
281
|
-
The
|
|
281
|
+
The review-body summary should:
|
|
282
282
|
|
|
283
283
|
- Group verdicts under **Agree / Disagree / Already fixed / Deferred (out of scope)** headings. Omit a heading if its list is empty.
|
|
284
|
-
- Under **Deferred (out of scope)**, list each deferred
|
|
284
|
+
- Under **Deferred (out of scope)**, list each deferred review-body comment as a bullet, followed on its own line by `<!-- babysit-pr:followup v1 core@3.4.1 -->` so grep catches them individually.
|
|
285
285
|
- Include the commit URL for fixes.
|
|
286
|
-
- Include every current
|
|
286
|
+
- Include every current 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.
|
|
287
287
|
|
|
288
288
|
### 10. Summarize
|
|
289
289
|
|
|
@@ -293,7 +293,7 @@ Report:
|
|
|
293
293
|
- Merge conflict status if relevant (resolved or aborted with reason).
|
|
294
294
|
- CI checks fixed / still failing / skipped-with-diagnosis.
|
|
295
295
|
- Review threads replied to, grouped by verdict (including any Defer count: "X threads deferred as follow-ups").
|
|
296
|
-
-
|
|
296
|
+
- Review-body comments summarized (or skipped because already covered), including the Deferred count: "Y review-body comments deferred as follow-ups".
|
|
297
297
|
- Threads left active because of bot-acknowledgement uncertainty (flag by thread URL).
|
|
298
298
|
- The stop condition triggered for this pass (clean / progressing / stuck).
|
|
299
299
|
|
|
@@ -309,7 +309,7 @@ Do not rely only on `gh pr view --json comments,reviews` — that view can miss
|
|
|
309
309
|
|
|
310
310
|
After the single pass completes, pick exactly one outcome:
|
|
311
311
|
|
|
312
|
-
- **Exit clean** — all CI checks passed AND every thread in `activeThreads` was either marked Skip-reply during step 6's inspection or has already received a fresh sentinel reply in this pass (Agree / Disagree / Already-fixed / **Defer** all count — a Defer reply is a sentinel reply), AND every current
|
|
312
|
+
- **Exit clean** — all CI checks passed AND every thread in `activeThreads` was either marked Skip-reply during step 6's inspection or has already received a fresh sentinel reply in this pass (Agree / Disagree / Already-fixed / **Defer** all count — a Defer reply is a sentinel reply), AND every current review-body fingerprint is covered by an existing sentinel comment (deferred review-body comments count; they're in the summary's fingerprint block). Do not use raw `totalActiveThreads` from the script output — it is pre-inspection and will stay non-zero for Skip-reply cases. A PR with Deferred threads is still clean from babysit's perspective: the skill has done what it can without widening scope. Report success and stop.
|
|
313
313
|
- **Exit progressing** — pass made commits, posted new replies, or both, and the PR is not yet clean (CI is still pending, a new CI run was triggered by this pass's commits, or more work remains). There is real work still in flight that another run would pick up. Report what was done and what is pending, and tell the user to re-run `/babysit-pr` once CI settles, or to wrap the call with `/loop <cadence> /babysit-pr` (or a shell `while true; do ...; done`) for automatic re-runs.
|
|
314
314
|
- **Exit stuck** — pass made no commits and posted no new replies, and the PR is still not clean. Nothing actionable happened this pass. Use this whenever progress is blocked on something outside the skill's scope, including:
|
|
315
315
|
- Merge conflict in step 2 that exceeded the high-confidence resolution bar.
|
|
@@ -338,7 +338,7 @@ User: `babysit my PR`
|
|
|
338
338
|
- No PR arg → operate on the current branch.
|
|
339
339
|
- Preflight OK, PR #482 found.
|
|
340
340
|
- `gh pr checks --watch` times out at 600s — two checks still pending.
|
|
341
|
-
- `unresolvedPrComments.sh` returns 0 active threads, 0
|
|
341
|
+
- `unresolvedPrComments.sh` returns 0 active threads, 0 review-body comments.
|
|
342
342
|
- No commits, no replies posted, CI state unchanged vs. start.
|
|
343
343
|
- Outcome: **stuck**. Report: "CI still running after 10 min; no comments to address. Re-run `/babysit-pr` once CI settles, or wrap with `/loop 2m /babysit-pr`."
|
|
344
344
|
|
|
@@ -357,7 +357,7 @@ User: `babysit PR 482`
|
|
|
357
357
|
User: `babysit my PR`
|
|
358
358
|
|
|
359
359
|
- Preflight OK, PR #612 found, CI green.
|
|
360
|
-
- `unresolvedPrComments.sh` returns 1 active thread and 2
|
|
360
|
+
- `unresolvedPrComments.sh` returns 1 active thread and 2 review-body comments:
|
|
361
361
|
- Thread on `src/users.ts:82` (unchanged, not touched by diff) — reviewer: "while you're here, this helper could be memoized".
|
|
362
362
|
- Nitpick on `src/orders.ts:45-47` — anchor overlaps a changed line; CodeRabbit says the error message should use backticks. In scope.
|
|
363
363
|
- Nitpick on `src/unrelated.ts:10` — file not touched by the PR. Out of scope, no escape-hatch signal.
|
|
@@ -365,8 +365,8 @@ User: `babysit my PR`
|
|
|
365
365
|
- Thread is on an unchanged line; reviewer doesn't tie it to this PR's changes; doesn't meet the fix bar (not a crash, not a bug, not trivial). → **Defer**.
|
|
366
366
|
- First nitpick is in-scope → **Agree**, apply backtick fix.
|
|
367
367
|
- Second nitpick is out-of-scope, not a correctness bug, not a one-liner → **Defer** (goes under the Deferred (out of scope) heading in the summary).
|
|
368
|
-
- Commit `f00dbabe` for the in-scope
|
|
369
|
-
- Summary reports: "1 thread deferred as follow-up, 1
|
|
368
|
+
- Commit `f00dbabe` for the in-scope review-body fix. Post Defer reply on the thread with the `babysit-pr:followup v1` sentinel above the `addressed` sentinel. Post the review-body summary with Agree (1) and Deferred (out of scope) (1) headings; both fingerprints listed in the fenced block.
|
|
369
|
+
- Summary reports: "1 thread deferred as follow-up, 1 review-body comment deferred as follow-up" plus the `gh api graphql ... | grep babysit-pr:followup` one-liner.
|
|
370
370
|
- **Exit clean** — Defer replies count as fresh sentinel replies; all fingerprints are covered.
|
|
371
371
|
|
|
372
372
|
## Input
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# parseNitpicks.sh — Parse
|
|
2
|
+
# parseNitpicks.sh — Parse bot review-body comments from PR review bodies.
|
|
3
3
|
#
|
|
4
4
|
# Each emitted comment includes a stable `fingerprint` field (sha256 of file +
|
|
5
5
|
# normalized line range + title + body), so reposted reviews dedupe to the same
|
|
@@ -22,75 +22,132 @@ local $/;
|
|
|
22
22
|
my $reviews_json = <STDIN>;
|
|
23
23
|
my $reviews = decode_json($reviews_json);
|
|
24
24
|
|
|
25
|
-
my
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
25
|
+
my @comments = (
|
|
26
|
+
extract_coderabbit_comments($reviews),
|
|
27
|
+
extract_mendral_comments($reviews),
|
|
28
|
+
);
|
|
29
|
+
print encode_json(\@comments);
|
|
30
|
+
|
|
31
|
+
sub extract_coderabbit_comments {
|
|
32
|
+
my ($reviews) = @_;
|
|
33
|
+
|
|
34
|
+
my $latest_review;
|
|
35
|
+
my $latest_time = "";
|
|
36
|
+
for my $review (@$reviews) {
|
|
37
|
+
my $author = $review->{author}{login} // "";
|
|
38
|
+
my $body = $review->{body} // "";
|
|
39
|
+
next unless $author eq "coderabbitai" && has_supported_sections($body);
|
|
40
|
+
my $created = $review->{createdAt} // "";
|
|
41
|
+
if ($created gt $latest_time) {
|
|
42
|
+
$latest_time = $created;
|
|
43
|
+
$latest_review = $review;
|
|
44
|
+
}
|
|
35
45
|
}
|
|
46
|
+
|
|
47
|
+
return () unless $latest_review;
|
|
48
|
+
|
|
49
|
+
my $body = $latest_review->{body};
|
|
50
|
+
my $author = $latest_review->{author}{login} // "deleted-user";
|
|
51
|
+
my $created_at = $latest_review->{createdAt} // "";
|
|
52
|
+
|
|
53
|
+
my @sections = extract_review_body_comment_sections($body);
|
|
54
|
+
return () unless @sections;
|
|
55
|
+
|
|
56
|
+
my @comments;
|
|
57
|
+
for my $section (@sections) {
|
|
58
|
+
my $section_content = $section->{content};
|
|
59
|
+
my $category = $section->{category};
|
|
60
|
+
|
|
61
|
+
while ($section_content =~ /<details>\s*<summary>([^<]+?)\s+\(\d+\)<\/summary>\s*<blockquote>([\s\S]*?)<\/blockquote>\s*<\/details>/g) {
|
|
62
|
+
my $raw_file_name = trim($1);
|
|
63
|
+
my $file_content = $2;
|
|
64
|
+
|
|
65
|
+
# Category prefix is optional. CodeRabbit emits 0–N `_…_` tags
|
|
66
|
+
# separated by `|` (e.g. `_⚠️ Potential issue_ | _🟠 Major_ | _⚡ Quick win_`
|
|
67
|
+
# or just `_💤 Low value_` on lower-confidence findings). The previous
|
|
68
|
+
# regex required exactly two tags and silently dropped one-tag and
|
|
69
|
+
# three-tag variants.
|
|
70
|
+
while ($file_content =~ /`(\d+(?:-\d+)?)`:\s*(?:_[^_]+_(?:\s*\|\s*_[^_]+_)*\s*)?\*\*([^*]+)\*\*\s*([\s\S]*?)(?=---|\n`\d|<\/blockquote>|$)/g) {
|
|
71
|
+
my $line_range = $1;
|
|
72
|
+
my $title = trim($2);
|
|
73
|
+
my $clean_body = clean_comment_body(trim($3));
|
|
74
|
+
my $file_name = normalize_file_name($raw_file_name, $line_range);
|
|
75
|
+
|
|
76
|
+
push @comments, review_body_comment(
|
|
77
|
+
$author,
|
|
78
|
+
$created_at,
|
|
79
|
+
$file_name,
|
|
80
|
+
$line_range,
|
|
81
|
+
$title,
|
|
82
|
+
$clean_body,
|
|
83
|
+
$category,
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return @comments;
|
|
36
90
|
}
|
|
37
91
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
my $
|
|
44
|
-
my $author = $
|
|
45
|
-
my $
|
|
46
|
-
|
|
47
|
-
my
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
my @comments;
|
|
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
|
-
# Category prefix is optional. CodeRabbit emits 0–N `_…_` tags
|
|
63
|
-
# separated by `|` (e.g. `_⚠️ Potential issue_ | _🟠 Major_ | _⚡ Quick win_`
|
|
64
|
-
# or just `_💤 Low value_` on lower-confidence findings). The previous
|
|
65
|
-
# regex required exactly two tags and silently dropped one-tag and
|
|
66
|
-
# three-tag variants.
|
|
67
|
-
while ($file_content =~ /`(\d+(?:-\d+)?)`:\s*(?:_[^_]+_(?:\s*\|\s*_[^_]+_)*\s*)?\*\*([^*]+)\*\*\s*([\s\S]*?)(?=---|\n`\d|<\/blockquote>|$)/g) {
|
|
68
|
-
my $line_range = $1;
|
|
69
|
-
my $title = trim($2);
|
|
70
|
-
my $clean_body = clean_comment_body(trim($3));
|
|
71
|
-
my $file_name = normalize_file_name($raw_file_name, $line_range);
|
|
72
|
-
|
|
73
|
-
# Fingerprint: file + normalized line + title + body (NO timestamp,
|
|
74
|
-
# NO author, NO category — reposted reviews must dedupe to the same
|
|
75
|
-
# fingerprint even if CodeRabbit relabels the section).
|
|
76
|
-
my $fingerprint_input = join("\n", $file_name, $line_range, $title, $clean_body);
|
|
77
|
-
my $fingerprint = substr(sha256_hex(encode_utf8($fingerprint_input)), 0, 16);
|
|
78
|
-
|
|
79
|
-
push @comments, {
|
|
80
|
-
author => $author,
|
|
81
|
-
body => "$title\n\n$clean_body",
|
|
82
|
-
category => $category,
|
|
83
|
-
createdAt => $created_at,
|
|
84
|
-
file => $file_name,
|
|
85
|
-
fingerprint => $fingerprint,
|
|
86
|
-
line => $line_range,
|
|
87
|
-
title => $title,
|
|
88
|
-
};
|
|
92
|
+
sub extract_mendral_comments {
|
|
93
|
+
my ($reviews) = @_;
|
|
94
|
+
|
|
95
|
+
my $latest_review;
|
|
96
|
+
my $latest_time = "";
|
|
97
|
+
for my $review (@$reviews) {
|
|
98
|
+
my $author = $review->{author}{login} // "";
|
|
99
|
+
my $body = $review->{body} // "";
|
|
100
|
+
next unless ($author eq "mendral-app" || $author eq "mendral-app[bot]") && is_actionable_mendral_review($body);
|
|
101
|
+
my $created = $review->{createdAt} // "";
|
|
102
|
+
if ($created gt $latest_time) {
|
|
103
|
+
$latest_time = $created;
|
|
104
|
+
$latest_review = $review;
|
|
89
105
|
}
|
|
90
106
|
}
|
|
107
|
+
|
|
108
|
+
return () unless $latest_review;
|
|
109
|
+
|
|
110
|
+
my $body = $latest_review->{body} // "";
|
|
111
|
+
my $title = mendral_title($body);
|
|
112
|
+
return () unless $title;
|
|
113
|
+
|
|
114
|
+
my $clean_body = clean_mendral_body($body);
|
|
115
|
+
return () unless $clean_body ne "";
|
|
116
|
+
|
|
117
|
+
my ($file_name, $line_range) = extract_first_file_line_reference($clean_body);
|
|
118
|
+
return () unless $file_name && $line_range;
|
|
119
|
+
|
|
120
|
+
return review_body_comment(
|
|
121
|
+
$latest_review->{author}{login} // "deleted-user",
|
|
122
|
+
$latest_review->{createdAt} // "",
|
|
123
|
+
$file_name,
|
|
124
|
+
$line_range,
|
|
125
|
+
$title,
|
|
126
|
+
$clean_body,
|
|
127
|
+
"mendral",
|
|
128
|
+
);
|
|
91
129
|
}
|
|
92
130
|
|
|
93
|
-
|
|
131
|
+
sub review_body_comment {
|
|
132
|
+
my ($author, $created_at, $file_name, $line_range, $title, $clean_body, $category) = @_;
|
|
133
|
+
|
|
134
|
+
# Fingerprint: file + normalized line + title + body (NO timestamp,
|
|
135
|
+
# NO author, NO category — reposted reviews must dedupe to the same
|
|
136
|
+
# fingerprint even if a review bot relabels the section).
|
|
137
|
+
my $fingerprint_input = join("\n", $file_name, $line_range, $title, $clean_body);
|
|
138
|
+
my $fingerprint = substr(sha256_hex(encode_utf8($fingerprint_input)), 0, 16);
|
|
139
|
+
|
|
140
|
+
return {
|
|
141
|
+
author => $author,
|
|
142
|
+
body => "$title\n\n$clean_body",
|
|
143
|
+
category => $category,
|
|
144
|
+
createdAt => $created_at,
|
|
145
|
+
file => $file_name,
|
|
146
|
+
fingerprint => $fingerprint,
|
|
147
|
+
line => $line_range,
|
|
148
|
+
title => $title,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
94
151
|
|
|
95
152
|
sub has_supported_sections {
|
|
96
153
|
my ($text) = @_;
|
|
@@ -98,6 +155,47 @@ sub has_supported_sections {
|
|
|
98
155
|
return $text =~ /<summary>\s*[^<]*(?:Nitpick comments|Minor comments|Outside diff range comments)\s*\(\d+\)<\/summary>\s*<blockquote>/i;
|
|
99
156
|
}
|
|
100
157
|
|
|
158
|
+
sub is_actionable_mendral_review {
|
|
159
|
+
my ($text) = @_;
|
|
160
|
+
my $title = mendral_title($text);
|
|
161
|
+
return defined $title && $title =~ /^(?:needs attention|changes requested|needs changes)$/i;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
sub mendral_title {
|
|
165
|
+
my ($text) = @_;
|
|
166
|
+
$text = strip_markdown_blockquote_prefixes($text);
|
|
167
|
+
return $1 if $text =~ /^\s*\*\*([^*]+)\*\*/m;
|
|
168
|
+
return undef;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
sub clean_mendral_body {
|
|
172
|
+
my ($text) = @_;
|
|
173
|
+
$text = strip_markdown_blockquote_prefixes($text);
|
|
174
|
+
$text =~ s/^\s*\*\*[^*]+\*\*\s*//;
|
|
175
|
+
$text =~ s/<details>[\s\S]*$//;
|
|
176
|
+
$text =~ s/<sub>[\s\S]*?<\/sub>//g;
|
|
177
|
+
$text =~ s/<!--[\s\S]*?-->//g;
|
|
178
|
+
return trim($text);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
sub extract_first_file_line_reference {
|
|
182
|
+
my ($text) = @_;
|
|
183
|
+
$text =~ s/\x{2013}|\x{2014}/-/g;
|
|
184
|
+
|
|
185
|
+
if ($text =~ /`([^`\n]+\/[^`\n]+\.[A-Za-z0-9]+)`[^\n]{0,120}?\blines?\s+(\d+(?:\s*(?:-|to)\s*\d+)?)/i) {
|
|
186
|
+
return ($1, normalize_line_range($2));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return (undef, undef);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
sub normalize_line_range {
|
|
193
|
+
my ($line_range) = @_;
|
|
194
|
+
$line_range = trim($line_range);
|
|
195
|
+
return "$1-$2" if $line_range =~ /^(\d+)\s*(?:-|to)\s*(\d+)$/i;
|
|
196
|
+
return $line_range;
|
|
197
|
+
}
|
|
198
|
+
|
|
101
199
|
sub extract_review_body_comment_sections {
|
|
102
200
|
my ($text) = @_;
|
|
103
201
|
$text = strip_markdown_blockquote_prefixes($text);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env bash
|
|
2
|
-
# unresolvedPrComments.sh — Fetch review threads +
|
|
2
|
+
# unresolvedPrComments.sh — Fetch review threads + review-body comments for babysit-pr.
|
|
3
3
|
# Extended from plugins/core/skills/unresolved-pr-comments/scripts/unresolvedPrComments.sh.
|
|
4
|
-
# Adds: thread IDs, per-thread sentinel recency state, stable
|
|
4
|
+
# Adds: thread IDs, per-thread sentinel recency state, stable review-body fingerprints.
|
|
5
5
|
#
|
|
6
6
|
# Usage: bash unresolvedPrComments.sh [pr-number]
|
|
7
7
|
# Compatible with macOS bash 3.2. Requires: gh, jq (>= 1.5), perl with Digest::SHA.
|
|
@@ -190,7 +190,7 @@ main() {
|
|
|
190
190
|
# entry in postSentinelBotComments and treat as active unless EVERY one
|
|
191
191
|
# is confidently a non-actionable acknowledgement
|
|
192
192
|
# "addressed" — our sentinel is the newest relevant activity on this thread
|
|
193
|
-
local bots_json='["coderabbitai","coderabbitai[bot]","dependabot","dependabot[bot]","github-actions","github-actions[bot]","github-advanced-security","github-advanced-security[bot]","renovate","renovate[bot]","renovate-bot","pre-commit-ci","pre-commit-ci[bot]","codecov","codecov[bot]","sonarcloud","sonarcloud[bot]"]'
|
|
193
|
+
local bots_json='["coderabbitai","coderabbitai[bot]","mendral-app","mendral-app[bot]","dependabot","dependabot[bot]","github-actions","github-actions[bot]","github-advanced-security","github-advanced-security[bot]","renovate","renovate[bot]","renovate-bot","pre-commit-ci","pre-commit-ci[bot]","codecov","codecov[bot]","sonarcloud","sonarcloud[bot]"]'
|
|
194
194
|
local threads_json
|
|
195
195
|
threads_json="$(printf '%s' "$response" | jq --arg sentinel_prefix "$SENTINEL_PREFIX" --argjson bots "$bots_json" '
|
|
196
196
|
# Exact login equality via IN($bots[]) — do NOT use `inside($bots)`, which
|
|
@@ -342,7 +342,8 @@ main() {
|
|
|
342
342
|
')"
|
|
343
343
|
fi
|
|
344
344
|
|
|
345
|
-
#
|
|
345
|
+
# Automated review-body comments. The legacy function/field names stay for
|
|
346
|
+
# compatibility with callers that already consume nitpickComments.
|
|
346
347
|
local reviews_json
|
|
347
348
|
reviews_json="$(printf '%s' "$response" | jq '[.data.repository.pullRequest.reviews.nodes[]]')"
|
|
348
349
|
local nitpick_comments
|