@clipboard-health/ai-rules 2.20.5 → 2.20.7

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipboard-health/ai-rules",
3
- "version": "2.20.5",
3
+ "version": "2.20.7",
4
4
  "description": "Pre-built AI agent rules for consistent coding standards.",
5
5
  "keywords": [
6
6
  "ai",
@@ -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 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'."
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 CodeRabbit review-body comment. Threads stay open for human resolution — this skill only posts replies, it never resolves.
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 CodeRabbit summary). This is how the skill knows, on re-runs, which threads and CodeRabbit 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.
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 — CodeRabbit's review content would be lost if it did.
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 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.
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 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.
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 (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.
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 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.
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 CodeRabbit review-body comments
218
+ ### 7. Assess automated review-body comments
219
219
 
220
- For every parsed CodeRabbit review-body comment in `nitpickComments`:
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 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.
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 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).
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 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.
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 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.
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 CodeRabbit review-body comments were assessed in step 7, post ONE top-level PR comment summarizing all of them:
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 CodeRabbit summary body should:
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 CodeRabbit 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.
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 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.
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
- - Nitpicks summarized (or skipped because already covered), including the Deferred count: "Y nitpicks deferred as follow-ups".
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 nitpick fingerprint is covered by an existing sentinel comment (deferred nitpicks 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.
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 nitpicks.
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 nitpicks:
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 nitpick fix. Post Defer reply on the thread with the `babysit-pr:followup v1` sentinel above the `addressed` sentinel. Post the nitpick 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 nitpick deferred as follow-up" plus the `gh api graphql ... | grep babysit-pr:followup` one-liner.
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 CodeRabbit review-body comments from PR review bodies.
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 $latest_review;
26
- my $latest_time = "";
27
- for my $review (@$reviews) {
28
- my $author = $review->{author}{login} // "";
29
- my $body = $review->{body} // "";
30
- next unless $author eq "coderabbitai" && has_supported_sections($body);
31
- my $created = $review->{createdAt} // "";
32
- if ($created gt $latest_time) {
33
- $latest_time = $created;
34
- $latest_review = $review;
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
- unless ($latest_review) {
39
- print "[]";
40
- exit 0;
41
- }
42
-
43
- my $body = $latest_review->{body};
44
- my $author = $latest_review->{author}{login} // "deleted-user";
45
- my $created_at = $latest_review->{createdAt} // "";
46
-
47
- my @sections = extract_review_body_comment_sections($body);
48
- unless (@sections) {
49
- print "[]";
50
- exit 0;
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
- print encode_json(\@comments);
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 + nitpicks for babysit-pr.
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 nitpick fingerprints.
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
- # Nitpicks from coderabbit review bodies
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