@clipboard-health/ai-rules 2.15.4 → 2.15.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clipboard-health/ai-rules",
3
- "version": "2.15.4",
3
+ "version": "2.15.5",
4
4
  "description": "Pre-built AI agent rules for consistent coding standards.",
5
5
  "keywords": [
6
6
  "ai",
@@ -1,85 +0,0 @@
1
- ---
2
- name: fix-ci
3
- description: "Analyze and fix CI failures for a GitHub pull request. Use this when CI checks are failing, the build is red, or you need to diagnose GitHub Actions failures. Triggers on: 'fix CI', 'CI is failing', 'checks failed', 'build is broken', 'tests failing in CI', 'why is CI red', or any request to investigate and resolve PR check failures."
4
- argument-hint: "[pr-url]"
5
- ---
6
-
7
- # Fix CI Failures
8
-
9
- Diagnose and fix CI failures for a GitHub pull request by retrieving logs, identifying root causes, and applying targeted fixes.
10
-
11
- ## Arguments
12
-
13
- - `$ARGUMENTS` - GitHub PR URL (e.g., `https://github.com/owner/repo/pull/123`). If omitted, uses the PR for the current branch.
14
-
15
- ## Instructions
16
-
17
- ### Step 1: Get PR and Identify Failed Checks
18
-
19
- ```bash
20
- # If $ARGUMENTS contains a PR URL, extract the PR number from the path.
21
- # Otherwise, get the PR for the current branch:
22
- gh pr view --json number,url,headRefName,statusCheckRollup
23
- ```
24
-
25
- From `statusCheckRollup`, find checks where `conclusion` is `"failure"` or `state` is `"FAILURE"`. If no checks have failed, report that CI is green and stop.
26
-
27
- When multiple checks failed, prioritize **build/compile checks over test checks** — build errors are often the root cause of downstream test failures, so fixing them first may resolve multiple failures at once.
28
-
29
- ### Step 2: Retrieve Failed Job Logs
30
-
31
- Extract the run ID from the failed check's `detailsUrl` (the numeric suffix of the GitHub Actions URL, e.g., `.../actions/runs/123456789`).
32
-
33
- ```bash
34
- # List failed job IDs for the run
35
- gh run view $RUN_ID --json jobs --jq '.jobs[] | select(.conclusion == "failure") | .databaseId'
36
-
37
- # Get only failed step output (concise)
38
- gh run view --job $JOB_ID --log-failed
39
- ```
40
-
41
- Start with `--log-failed` because it shows only the output from failed steps, cutting through potentially thousands of lines of passing output. Only fall back to `--log` with targeted grep if `--log-failed` doesn't provide enough context.
42
-
43
- When parsing large log output, focus on:
44
-
45
- - **The first error message** — later failures often cascade from this
46
- - **Summary lines** like "Test Suites:", "FAILED", build error counts
47
- - **Stack traces** immediately after assertion or compilation errors
48
-
49
- ### Step 3: Diagnose Root Cause
50
-
51
- Read the relevant source files and tests to understand the failure in context.
52
-
53
- **Build/compilation errors** — type errors, missing imports, syntax issues. These block everything else.
54
- **Test failures** — read both the failing test and the code it exercises. The fix could be in either place; don't assume the test is wrong just because it's failing.
55
- **Lint/format errors** — usually auto-fixable with project formatting tools.
56
- **Infrastructure issues** — missing env vars, CI config problems, dependency resolution failures.
57
-
58
- ### Step 4: Present Findings and Proposed Fix
59
-
60
- Before changing code, present:
61
-
62
- 1. **Root cause** — what's failing and why
63
- 2. **Proposed fix** — which files to change and what the changes are
64
-
65
- Then ask the user whether to proceed. This confirmation step matters because CI failures can be ambiguous — what looks like a test bug might actually be the test correctly catching a real regression.
66
-
67
- ### Step 5: Apply Fix and Verify
68
-
69
- After approval, implement the changes. Then run the relevant checks locally (build, test, lint for the affected project/files) to verify the fix before pushing. Report the local results.
70
-
71
- ### Failure Patterns
72
-
73
- **Removed code but tests still reference it:** Delete or update the tests. Check for leftover references in shared test fixtures or configurations.
74
-
75
- **Missing mocks/providers after adding a dependency:** Add the mock to test module setup. Update existing mocks when signatures change.
76
-
77
- **Type errors from interface changes:** Fix in the changed files. If the type change is intentional, update downstream consumers too.
78
-
79
- **Snapshot mismatches:** Check if the change is expected (you modified rendering/output). If so, update snapshots. If not, investigate the regression.
80
-
81
- **Flaky/intermittent failures:** If the test passes locally, the failure looks timing-dependent, or it's unrelated to the PR's changes, flag this to the user rather than making speculative changes.
82
-
83
- ## Input
84
-
85
- Pull Request URL: $ARGUMENTS
@@ -1,135 +0,0 @@
1
- ---
2
- name: iterate-pr
3
- description: "Iteratively commit, push, and address PR feedback until CI passes and comments are resolved. Use this skill when the user wants to get a PR to a mergeable state, fix CI and address review comments in a loop, or says things like 'iterate on my PR', 'make this PR pass', 'fix everything on the PR', 'get this PR ready to merge', or 'keep going until CI is green'."
4
- argument-hint: "[max-iterations]"
5
- ---
6
-
7
- # Iterate PR
8
-
9
- Autonomously iterate on a pull request until CI checks pass and all comments are resolved. Each iteration runs in a fresh subagent.
10
-
11
- ## Arguments
12
-
13
- - `$ARGUMENTS` - Maximum iterations (default: 3)
14
-
15
- ## Instructions
16
-
17
- ### Step 1: Initialize
18
-
19
- Parse max iterations from `$ARGUMENTS` (default: 3). Set iteration counter to 0.
20
-
21
- ### Step 2: Check State
22
-
23
- Get the PR for the current branch:
24
-
25
- !`gh pr view --json number,url,headRefName,statusCheckRollup 2>/dev/null || echo "NO_PR"`
26
-
27
- **If no PR exists:** Proceed to Step 3 (first iteration will create one).
28
-
29
- **If PR exists:** Get unresolved comments data:
30
-
31
- !`bash "${CLAUDE_PLUGIN_ROOT:-.agents}/skills/unresolved-pr-comments/scripts/unresolvedPrComments.sh" 2>/dev/null`
32
-
33
- Parse the JSON output and evaluate exit conditions.
34
-
35
- **If any CI checks are still PENDING, QUEUED, or IN_PROGRESS:** Proceed to Step 3 regardless of comment count. Automated reviewers (e.g. CodeRabbit) may not have posted comments yet, so comment data is unreliable until all checks complete.
36
-
37
- **If statusCheckRollup is empty or null:** Treat CI as passing (some PRs have no required checks configured).
38
-
39
- **Exit with success** when ALL conditions are met:
40
-
41
- - All CI checks have completed (no PENDING, QUEUED, or IN_PROGRESS statuses in statusCheckRollup) and none have failed (or no checks exist)
42
- - No unresolved comments (totalUnresolvedComments == 0)
43
- - No nitpicks (totalNitpicks == 0)
44
-
45
- **JSON Structure for Exit Evaluation:**
46
-
47
- ```json
48
- {
49
- "totalUnresolvedComments": number,
50
- "totalNitpicks": number,
51
- "unresolvedComments": [...],
52
- "nitpickComments": [...]
53
- }
54
- ```
55
-
56
- Check `totalUnresolvedComments == 0` and `totalNitpicks == 0` for exit condition.
57
-
58
- Report: "PR is clean! All CI checks pass and no unresolved comments."
59
-
60
- **Exit with status report** when iteration counter >= max iterations:
61
-
62
- - List failing CI checks
63
- - List unresolved comment/nitpick counts
64
- - Suggest running `/iterate-pr` again to continue
65
-
66
- ### Step 3: Spawn Iteration Subagent
67
-
68
- Spawn a Task subagent with `subagent_type: "general-purpose"` using this prompt:
69
-
70
- > Handle one iteration of the PR feedback loop:
71
- >
72
- > 1. **Record Starting Commit**: !`git rev-parse HEAD` (save as `startCommit`)
73
- > 2. **Commit and Push**: Invoke `core:commit-push-pr` via the Skill tool
74
- > 3. **Get PR Number**: !`gh pr view --json number --jq '.number'`
75
- > 4. **Wait for CI**: !`rc=0; if command -v gtimeout >/dev/null 2>&1; then gtimeout 600 gh pr checks --watch || rc=$?; elif command -v timeout >/dev/null 2>&1; then timeout 600 gh pr checks --watch || rc=$?; else gh pr checks --watch || rc=$?; fi; case $rc in 0|1|8|124) ;; *) exit $rc;; esac` (10 minute timeout; exit codes: 0=pass, 1=fail, 8=pending, 124=timeout are expected and handled in next step; other codes like 4=auth error are re-raised; uses gtimeout on macOS, timeout on Linux, no timeout as fallback)
76
- > 5. **Check CI Status**: Run `gh pr checks --json name,state,bucket` and parse the output
77
- > - If any check has `bucket: "fail"`, invoke `core:fix-ci` via the Skill tool. Since you are running autonomously, do NOT wait for user approval — apply the fixes directly. Report what was fixed and exit.
78
- > 6. **Check Comments**: Run `bash "${CLAUDE_PLUGIN_ROOT:-.agents}/skills/unresolved-pr-comments/scripts/unresolvedPrComments.sh"` and parse the JSON output
79
- > - If unresolved comments or nitpicks exist:
80
- > 1. Group comments by file path and read each file once (not per-comment)
81
- > 2. If a file no longer exists, note the comment may be outdated and skip it
82
- > 3. Assess each comment with an explicit verdict:
83
- > - **Agree**: Explain why, then fix it
84
- > - **Disagree**: Explain why the current code is acceptable; do NOT change the code
85
- > - **Already fixed**: Note that the code already addresses this concern
86
- > 4. After assessing all comments, fix only those you agreed with and exit (next iteration will commit fixes)
87
- > 7. **Report**: Run `git rev-parse HEAD` and compare to `startCommit` to determine if commits were made. Include PR status, CI status, and comments addressed
88
-
89
- ### Step 4: Loop
90
-
91
- After the subagent completes:
92
-
93
- 1. Increment iteration counter
94
- 2. If no commits were made this iteration:
95
- - Get unresolved comments by running: `bash "${CLAUDE_PLUGIN_ROOT:-.agents}/skills/unresolved-pr-comments/scripts/unresolvedPrComments.sh"`
96
- - If unresolved comments remain, exit with: "Comments addressed, awaiting reviewer resolution. Run `/iterate-pr` after reviewer responds."
97
- 3. Report: "Iteration [N]/[max] complete. Checking state..."
98
- 4. Return to Step 2
99
-
100
- ## Examples
101
-
102
- **Successful completion:**
103
-
104
- ```text
105
- Iteration 1/3: No PR exists
106
- > commit-push-pr -> PR #347 created
107
- > CI failed (lint errors) -> fix-ci -> fixed missing semicolons
108
-
109
- Iteration 2/3: CI failures remain
110
- > commit-push-pr -> pushed fixes
111
- > CI passed, 2 review comments
112
- > unresolved-pr-comments -> addressed null check, const usage
113
-
114
- Iteration 3/3: Comments pending
115
- > commit-push-pr -> pushed fixes
116
- > CI passed, no unresolved comments
117
-
118
- PR is clean! All CI checks pass and no unresolved comments.
119
- ```
120
-
121
- **Awaiting reviewer resolution:**
122
-
123
- ```text
124
- Iteration 1/3: PR exists with comments
125
- > commit-push-pr -> pushed comment fixes
126
- > CI passed, 1 comment remains (disagreed with suggestion)
127
-
128
- Iteration 2/3: No commits made, comments remain
129
-
130
- Comments addressed, awaiting reviewer resolution. Run `/iterate-pr` after reviewer responds.
131
- ```
132
-
133
- ## Input
134
-
135
- Maximum iterations: $ARGUMENTS
@@ -1,123 +0,0 @@
1
- ---
2
- name: learn-from-session
3
- description: |
4
- Analyze the current session for agent efficiency, quality, and actionable improvements. Use this skill when the user says things like "learn from this session", "session retro", "what went well", "how did the agent do", "what should I improve", "review this session", "session review", or any request to reflect on how the current session went. Also use when the user wants to extract learnings, identify friction, or improve their workflow based on what just happened.
5
- ---
6
-
7
- You are reviewing the current Claude Code session. Your job is to surface the 2-3 most impactful findings — things that would actually change how the next session goes — not to produce an exhaustive report card.
8
-
9
- Be candid. A session where everything scores 4/5 but you have nothing concrete to suggest is a wasted review. Prioritize specificity over coverage: one sharp observation beats five generic ones.
10
-
11
- Analyze the session transcript and produce the following:
12
-
13
- ## Agent efficiency analysis
14
-
15
- Evaluate how well the agent used tools and how much human course-correction was needed.
16
-
17
- Score each dimension 1-5. Calibration: 1=actively harmful or completely wrong approach, 2=significant waste or frequent missteps, 3=adequate but with clear room for improvement, 4=good with minor issues, 5=genuinely impressive and hard to improve on. Reserve 5 for sessions that would make you say "I wish the agent always worked like this."
18
-
19
- - **Tool precision**: Did the agent use the right tools for each task, or did it flail between tools, run unnecessary reads, or use grep when it should have used targeted file reads?
20
- - **Iteration efficiency**: How many attempts did it take to get things right? Count tool retries, failed bash commands, and edit-then-re-edit cycles.
21
- - **Context utilization**: Did the agent leverage CLAUDE.md, AGENTS.md, and project conventions, or did it ignore available context and make assumptions?
22
- - **Autonomy level**: How often did the agent work without human intervention? Each rejection/abort/course-correction is a friction event.
23
- - **Autonomy span**: What was the longest streak of productive tool calls without human intervention?
24
-
25
- Provide a brief narrative (2-3 sentences) explaining the scores with specific examples from the session.
26
-
27
- ## Agent quality analysis
28
-
29
- Evaluate how well the agent followed project coding standards and CLAUDE.md rules.
30
-
31
- Use the same 1-5 calibration as above.
32
-
33
- - **CLAUDE.md compliance**: Did the agent follow the rules and conventions defined in CLAUDE.md and AGENTS.md? Flag specific violations.
34
- - **Code pattern adherence**: Did the generated code follow established project patterns (naming conventions, error handling, file organization, test structure)?
35
- - **Test coverage intent**: Did the agent write or update tests when modifying behavior? Did it run tests before declaring done?
36
- - **PR hygiene**: Is the commit history clean? Are changes scoped appropriately? Did the agent avoid touching unrelated files?
37
- - **Documentation awareness**: Did the agent update relevant docs, comments, or type definitions when changing interfaces?
38
-
39
- Provide a brief narrative (2-3 sentences) explaining the scores with specific examples.
40
-
41
- ## Session reflection
42
-
43
- Surface things that would save time if known at the start of the next session. The bar for inclusion: "would I tell a teammate about this before they start working on this codebase?" Skip categories with nothing worth noting.
44
-
45
- - **Bash commands**: Commands that were used, discovered, or would have been useful to know upfront.
46
- - **Code style patterns**: Patterns followed or discovered that aren't yet documented.
47
- - **Testing approaches**: Testing strategies that worked or should have been used.
48
- - **Environment/configuration quirks**: Gotchas, workarounds, or setup details encountered.
49
- - **Warnings**: Errors, deprecations, or edge cases that tripped up the agent.
50
-
51
- ## Actionable improvements
52
-
53
- This is the most important section. Produce recommendations in these categories, but only if genuinely actionable — a suggestion the user can apply in under 5 minutes. Skip any category with nothing worth doing.
54
-
55
- ### CLAUDE.md updates
56
-
57
- **Do not suggest rules that duplicate existing automated enforcement.** If a lint rule, pre-commit hook, CI check, or other tooling already catches an issue, documenting it in CLAUDE.md/AGENTS.md is redundant. Before suggesting a rule, check whether the session transcript shows the error was already caught and blocked by automated tooling (e.g. a pre-commit hook rejected the commit, a linter flagged the issue). If so, skip it — the tooling is already doing its job.
58
-
59
- For each suggestion, specify whether it belongs in:
60
-
61
- - **Team-shared** (checked into git, e.g. `./CLAUDE.md` or `./AGENTS.md`)
62
- - **Personal/local** (git-ignored, e.g. `~/.claude/CLAUDE.md`)
63
-
64
- Format each as a ready-to-paste diff:
65
-
66
- ```diff
67
- + [the addition - keep it brief]
68
- ```
69
-
70
- ### Hooks, skills, or agents
71
-
72
- Suggest new or revised hooks, skills, or agents that would have prevented issues or improved the workflow.
73
-
74
- ### Prompt technique
75
-
76
- One concrete thing the engineer could do differently in their prompts to get better results (be specific, not generic).
77
-
78
- ### Agent pattern note
79
-
80
- One observation about how the agent behaved that the team should know about — a strength to replicate or a weakness to work around.
81
-
82
- ## Structured data block
83
-
84
- After the human-readable review, emit a fenced JSON block that a scraper can parse:
85
-
86
- ```json
87
- {
88
- "session_review": {
89
- "version": "1.1",
90
- "timestamp": "<ISO 8601>",
91
- "efficiency": {
92
- "tool_precision": <1-5>,
93
- "iteration_efficiency": <1-5>,
94
- "context_utilization": <1-5>,
95
- "autonomy_level": <1-5>,
96
- "autonomy_span": <1-5>,
97
- "friction_events": <count of rejections/aborts/course-corrections>,
98
- "total_tool_calls": <count>,
99
- "failed_tool_calls": <count>
100
- },
101
- "quality": {
102
- "claude_md_compliance": <1-5>,
103
- "code_pattern_adherence": <1-5>,
104
- "test_coverage_intent": <1-5>,
105
- "pr_hygiene": <1-5>,
106
- "documentation_awareness": <1-5>,
107
- "violations": ["<brief description of each CLAUDE.md violation>"]
108
- },
109
- "improvements": {
110
- "claude_md_rules": ["<each suggested rule, ready to paste>"],
111
- "hooks_skills_agents": ["<each suggestion>"],
112
- "prompt_technique": "<the suggestion>",
113
- "agent_pattern_note": "<the observation>"
114
- }
115
- }
116
- }
117
- ```
118
-
119
- ## Presenting the review
120
-
121
- 1. Display the human-readable sections.
122
- 2. If there is an open PR for the current branch, offer to post the review as a PR comment (using `gh pr comment` with `--body-file`).
123
- 3. If the user has CLAUDE.md updates to apply, offer to apply them directly.
@@ -1,47 +0,0 @@
1
- ---
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 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
- argument-hint: "[pr-number]"
5
- ---
6
-
7
- # Unresolved PR Comments
8
-
9
- Fetch and analyze unresolved review comments from a GitHub pull request.
10
-
11
- ## Usage
12
-
13
- Run the script to fetch PR comment data:
14
-
15
- ```bash
16
- bash scripts/unresolvedPrComments.sh [pr-number]
17
- ```
18
-
19
- If no PR number is provided, it uses the PR associated with the current branch.
20
-
21
- Note: Limited to 100 review threads, 10 comments per thread, and 100 reviews.
22
-
23
- ## Processing Instructions
24
-
25
- Using the JSON output from the script:
26
-
27
- 1. **If error**: Display the error message and suggest the fix
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 CodeRabbit review-body comments")
30
-
31
- Then, for EVERY comment (both `unresolvedComments` AND `nitpickComments`):
32
-
33
- 1. Group comments by file path and read each file once (not per-comment)
34
- 2. If a file no longer exists, note that the comment may be outdated
35
- 3. Assess each comment against the current code. When multiple comments appear at the same file and line, they are part of the same review thread — read them together as a conversation
36
- 4. Group your assessment as follows:
37
-
38
- **Should address**
39
- - Description of the comment with file path(s)
40
- - Why it's a real issue (bug, a11y, UX, etc.)
41
-
42
- **Can ignore**
43
- - Description of the comment with file path(s)
44
- - Why: already fixed, not actionable, not worth addressing in this PR, etc.
45
-
46
- **Net**
47
- Summary of how many are worth fixing and what kind of issues they are. Offer to fix, but **do NOT start until the user confirms**.
@@ -1,165 +0,0 @@
1
- #!/usr/bin/env bash
2
- # parseNitpicks.sh — Parse CodeRabbit review-body comments from PR review bodies.
3
- # Sourced by unresolvedPrComments.sh. Requires: jq, perl.
4
-
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.
8
- extract_nitpick_comments() {
9
- local reviews_json="$1"
10
-
11
- printf '%s' "$reviews_json" | perl -e '
12
- use strict;
13
- use warnings;
14
- use JSON::PP;
15
-
16
- local $/;
17
- my $reviews_json = <STDIN>;
18
- my $reviews = decode_json($reviews_json);
19
-
20
- # Find latest coderabbitai review with supported review-body comment sections.
21
- my $latest_review;
22
- my $latest_time = "";
23
- for my $review (@$reviews) {
24
- my $author = $review->{author}{login} // "";
25
- my $body = $review->{body} // "";
26
- next unless $author eq "coderabbitai" && has_supported_sections($body);
27
- my $created = $review->{createdAt} // "";
28
- if ($created gt $latest_time) {
29
- $latest_time = $created;
30
- $latest_review = $review;
31
- }
32
- }
33
-
34
- unless ($latest_review) {
35
- print "[]";
36
- exit 0;
37
- }
38
-
39
- my $body = $latest_review->{body};
40
- my $author = $latest_review->{author}{login} // "deleted-user";
41
- my $created_at = $latest_review->{createdAt} // "";
42
-
43
- my @sections = extract_review_body_comment_sections($body);
44
- unless (@sections) {
45
- print "[]";
46
- exit 0;
47
- }
48
-
49
- my @comments;
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
- }
74
- }
75
- }
76
-
77
- print encode_json(\@comments);
78
-
79
- sub has_supported_sections {
80
- my ($text) = @_;
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);
92
- my $content_start = $+[0];
93
- my $after = substr($text, $content_start);
94
-
95
- my $depth = 1;
96
- my @tags;
97
- while ($after =~ /(<blockquote>|<\/blockquote>)/gi) {
98
- my $tag = $1;
99
- my $pos = $-[0];
100
- my $is_open = ($tag =~ /^<blockquote>/i) ? 1 : 0;
101
- push @tags, [$pos, $is_open];
102
- }
103
- for my $tag (@tags) {
104
- $depth += $tag->[1] ? 1 : -1;
105
- if ($depth == 0) {
106
- push @sections, {
107
- category => $category,
108
- content => substr($after, 0, $tag->[0]),
109
- };
110
- last;
111
- }
112
- }
113
- }
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;
136
- }
137
-
138
- sub clean_comment_body {
139
- my ($text) = @_;
140
- # Iteratively remove innermost <details> elements
141
- my $prev = "";
142
- while ($text ne $prev) {
143
- $prev = $text;
144
- $text =~ s/<details>(?:(?!<details>)[\s\S])*?<\/details>//g;
145
- }
146
- $text =~ s/</&lt;/g;
147
- $text =~ s/>/&gt;/g;
148
- return trim($text);
149
- }
150
-
151
- sub trim {
152
- my ($s) = @_;
153
- $s =~ s/^\s+//;
154
- $s =~ s/\s+$//;
155
- return $s;
156
- }
157
- '
158
- }
159
-
160
- # Extract code scanning alert number from comment body.
161
- # Outputs the alert number or empty string.
162
- extract_code_scanning_alert_number() {
163
- local body="$1"
164
- printf '%s' "$body" | perl -ne 'print $1 if m{/code-scanning/(\d+)}'
165
- }
@@ -1,246 +0,0 @@
1
- #!/usr/bin/env bash
2
- # unresolvedPrComments.sh — Fetch unresolved review comments from a GitHub PR.
3
- # Usage: bash unresolvedPrComments.sh [pr-number]
4
- # Outputs JSON with unresolved comments and CodeRabbit nitpicks.
5
- # Compatible with macOS bash 3.2. Requires: gh, jq, perl.
6
-
7
- set -euo pipefail
8
-
9
- SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
10
- # shellcheck source=parseNitpicks.sh
11
- source "${SCRIPT_DIR}/parseNitpicks.sh"
12
-
13
- # Save original stdout so output_error works inside $() command substitutions
14
- exec 3>&1
15
-
16
- # --- Helpers (inlined from ghClient.ts / prClient.ts) ---
17
-
18
- output_error() {
19
- printf '%s' "$1" | jq -Rsc '{ error: . }' >&3
20
- exit 1
21
- }
22
-
23
- validate_prerequisites() {
24
- if ! command -v jq >/dev/null 2>&1; then
25
- printf '{"error":"jq not found. Install from https://stedolan.github.io/jq"}\n' >&3
26
- exit 1
27
- fi
28
- if ! command -v gh >/dev/null 2>&1; then
29
- output_error "gh CLI not found. Install from https://cli.github.com"
30
- fi
31
- if ! command -v perl >/dev/null 2>&1; then
32
- output_error "perl not found."
33
- fi
34
- if ! gh api user --jq '.login' >/dev/null 2>&1; then
35
- output_error "Not authenticated with GitHub. Run: gh auth login"
36
- fi
37
- }
38
-
39
- get_pr_number() {
40
- local arg="${1:-}"
41
- if [ -n "$arg" ]; then
42
- if ! printf '%s' "$arg" | grep -qE '^[0-9]+$'; then
43
- output_error "Invalid PR number: ${arg}"
44
- fi
45
- printf '%s' "$arg"
46
- return
47
- fi
48
-
49
- local pr_json
50
- if ! pr_json="$(gh pr view --json number 2>/dev/null)"; then
51
- output_error "No PR found for current branch. Provide PR number as argument."
52
- fi
53
-
54
- local pr_num
55
- pr_num="$(printf '%s' "$pr_json" | jq -r '.number // empty')"
56
- if [ -z "$pr_num" ]; then
57
- output_error "No PR found for current branch. Provide PR number as argument."
58
- fi
59
- printf '%s' "$pr_num"
60
- }
61
-
62
- get_repo_info() {
63
- local repo_json
64
- if ! repo_json="$(gh repo view --json owner,name 2>/dev/null)"; then
65
- output_error "Could not determine repository. Are you in a git repo with a GitHub remote?"
66
- fi
67
-
68
- REPO_OWNER="$(printf '%s' "$repo_json" | jq -r '.owner.login // empty')"
69
- REPO_NAME="$(printf '%s' "$repo_json" | jq -r '.name // empty')"
70
-
71
- if [ -z "$REPO_OWNER" ] || [ -z "$REPO_NAME" ]; then
72
- output_error "Failed to parse repository info from gh CLI output."
73
- fi
74
- }
75
-
76
- # --- GraphQL ---
77
-
78
- # Pagination limits: 100 review threads, 10 comments per thread, 100 reviews.
79
- # Sufficient for typical PRs; data may be truncated on exceptionally active PRs.
80
- GRAPHQL_QUERY='
81
- query($owner: String!, $repo: String!, $pr: Int!) {
82
- repository(owner: $owner, name: $repo) {
83
- pullRequest(number: $pr) {
84
- title
85
- url
86
- reviewThreads(first: 100) {
87
- nodes {
88
- isResolved
89
- comments(first: 10) {
90
- nodes {
91
- body
92
- path
93
- line
94
- originalLine
95
- author { login }
96
- createdAt
97
- }
98
- }
99
- }
100
- }
101
- reviews(first: 100) {
102
- nodes {
103
- body
104
- author { login }
105
- createdAt
106
- }
107
- }
108
- }
109
- }
110
- }'
111
-
112
- execute_graphql_query() {
113
- local owner="$1" repo="$2" pr_number="$3"
114
- local result
115
- if ! result="$(gh api graphql \
116
- -f "query=${GRAPHQL_QUERY}" \
117
- -f "owner=${owner}" \
118
- -f "repo=${repo}" \
119
- -F "pr=${pr_number}" 2>&1)"; then
120
- output_error "GraphQL query failed: ${result}"
121
- fi
122
- printf '%s' "$result"
123
- }
124
-
125
- # --- Code scanning filter ---
126
-
127
- is_code_scanning_alert_fixed() {
128
- local owner="$1" repo="$2" alert_number="$3"
129
- local result
130
- if ! result="$(gh api "repos/${owner}/${repo}/code-scanning/alerts/${alert_number}" 2>/dev/null)"; then
131
- return 1
132
- fi
133
-
134
- local state
135
- state="$(printf '%s' "$result" | jq -r '.most_recent_instance.state // empty')"
136
- [ "$state" = "fixed" ]
137
- }
138
-
139
- # --- Main ---
140
-
141
- main() {
142
- validate_prerequisites
143
-
144
- local pr_number
145
- pr_number="$(get_pr_number "${1:-}")"
146
-
147
- get_repo_info
148
- local owner="$REPO_OWNER"
149
- local repo="$REPO_NAME"
150
-
151
- local response
152
- response="$(execute_graphql_query "$owner" "$repo" "$pr_number")"
153
-
154
- # Validate response
155
- if [ "$(printf '%s' "$response" | jq -r '.data.repository // empty')" = "" ]; then
156
- output_error "Repository ${owner}/${repo} not found or not accessible."
157
- fi
158
- if [ "$(printf '%s' "$response" | jq -r '.data.repository.pullRequest // empty')" = "" ]; then
159
- output_error "PR #${pr_number} not found or not accessible."
160
- fi
161
-
162
- local title url
163
- title="$(printf '%s' "$response" | jq -r '.data.repository.pullRequest.title')"
164
- url="$(printf '%s' "$response" | jq -r '.data.repository.pullRequest.url')"
165
-
166
- # Extract unresolved comments: filter unresolved threads, flatten comments
167
- local all_unresolved
168
- all_unresolved="$(printf '%s' "$response" | jq '[
169
- .data.repository.pullRequest.reviewThreads.nodes[]
170
- | select(.isResolved == false)
171
- | .comments.nodes[]
172
- | {
173
- author: (.author.login // "deleted-user"),
174
- body: .body,
175
- createdAt: .createdAt,
176
- file: .path,
177
- line: (.line // .originalLine)
178
- }
179
- ]')"
180
-
181
- # Filter out fixed code-scanning alerts from github-advanced-security
182
- local unresolved_comments="[]"
183
- local count
184
- count="$(printf '%s' "$all_unresolved" | jq 'length')"
185
-
186
- local i=0
187
- while [ "$i" -lt "$count" ]; do
188
- local comment
189
- comment="$(printf '%s' "$all_unresolved" | jq ".[$i]")"
190
- local comment_author
191
- comment_author="$(printf '%s' "$comment" | jq -r '.author')"
192
-
193
- local keep=true
194
- if [ "$comment_author" = "github-advanced-security" ]; then
195
- local comment_body alert_number
196
- comment_body="$(printf '%s' "$comment" | jq -r '.body')"
197
- alert_number="$(extract_code_scanning_alert_number "$comment_body")"
198
- if [ -n "$alert_number" ]; then
199
- if is_code_scanning_alert_fixed "$owner" "$repo" "$alert_number"; then
200
- keep=false
201
- fi
202
- fi
203
- fi
204
-
205
- if [ "$keep" = true ]; then
206
- unresolved_comments="$(printf '%s' "$unresolved_comments" | jq --argjson c "$comment" '. + [$c]')"
207
- fi
208
-
209
- i=$((i + 1))
210
- done
211
-
212
- # Extract nitpick comments from reviews
213
- local reviews_json
214
- reviews_json="$(printf '%s' "$response" | jq '[.data.repository.pullRequest.reviews.nodes[]]')"
215
- local nitpick_comments
216
- nitpick_comments="$(extract_nitpick_comments "$reviews_json")"
217
-
218
- # Build final output
219
- local total_unresolved total_nitpicks
220
- total_unresolved="$(printf '%s' "$unresolved_comments" | jq 'length')"
221
- total_nitpicks="$(printf '%s' "$nitpick_comments" | jq 'length')"
222
-
223
- jq -n \
224
- --argjson nitpickComments "$nitpick_comments" \
225
- --arg owner "$owner" \
226
- --argjson prNumber "$pr_number" \
227
- --arg repo "$repo" \
228
- --arg title "$title" \
229
- --argjson totalNitpicks "$total_nitpicks" \
230
- --argjson totalUnresolvedComments "$total_unresolved" \
231
- --argjson unresolvedComments "$unresolved_comments" \
232
- --arg url "$url" \
233
- '{
234
- nitpickComments: $nitpickComments,
235
- owner: $owner,
236
- prNumber: $prNumber,
237
- repo: $repo,
238
- title: $title,
239
- totalNitpicks: $totalNitpicks,
240
- totalUnresolvedComments: $totalUnresolvedComments,
241
- unresolvedComments: $unresolvedComments,
242
- url: $url
243
- }'
244
- }
245
-
246
- main "$@"