pr-shepherd 0.59.3 → 0.60.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bin/api.d.mts +1 -1
  3. package/bin/classify/standard/codecov-report.d.mts +1 -1
  4. package/bin/classify/standard/codecov-report.mjs +7 -2
  5. package/bin/classify/standard/coderabbit-notices.d.mts +4 -3
  6. package/bin/classify/standard/coderabbit-notices.mjs +29 -47
  7. package/bin/classify/standard/codex-notices.mjs +34 -7
  8. package/bin/classify/standard/dependabot-notices.mjs +5 -1
  9. package/bin/classify/standard/gemini-notices.d.mts +1 -1
  10. package/bin/classify/standard/gemini-notices.mjs +16 -5
  11. package/bin/classify/standard/kilo-no-issues.d.mts +1 -1
  12. package/bin/classify/standard/kilo-no-issues.mjs +37 -10
  13. package/bin/classify/standard/sourcery-notices.mjs +1 -1
  14. package/bin/cli/edited-comments-format.d.mts +14 -0
  15. package/bin/cli/edited-comments-format.mjs +39 -0
  16. package/bin/cli/fix-formatter.mjs +5 -2
  17. package/bin/cli/iterate-activity-formatter.d.mts +1 -1
  18. package/bin/cli/iterate-activity-formatter.mjs +4 -5
  19. package/bin/cli/iterate-check-names.d.mts +5 -0
  20. package/bin/cli/iterate-check-names.mjs +17 -0
  21. package/bin/cli/iterate-formatter.mjs +15 -19
  22. package/bin/cli/iterate-lean.mjs +19 -11
  23. package/bin/cli/iterate-verbose.mjs +7 -2
  24. package/bin/cli/list-formatters.d.mts +0 -3
  25. package/bin/cli/list-formatters.mjs +1 -5
  26. package/bin/commands/check-review-budget.d.mts +39 -0
  27. package/bin/commands/check-review-budget.mjs +78 -0
  28. package/bin/commands/check-wait-detectors.d.mts +6 -1
  29. package/bin/commands/check-wait-detectors.mjs +2 -1
  30. package/bin/commands/check.d.mts +2 -0
  31. package/bin/commands/check.mjs +46 -25
  32. package/bin/commands/iterate/check-instructions.d.mts +5 -0
  33. package/bin/commands/iterate/check-instructions.mjs +4 -0
  34. package/bin/commands/iterate/classify.d.mts +1 -1
  35. package/bin/commands/iterate/edited-comment-markers.d.mts +3 -0
  36. package/bin/commands/iterate/edited-comment-markers.mjs +10 -0
  37. package/bin/commands/iterate/fix-code.mjs +17 -1
  38. package/bin/commands/iterate/helpers.mjs +9 -5
  39. package/bin/commands/iterate/index.mjs +44 -17
  40. package/bin/commands/iterate/render.mjs +1 -4
  41. package/bin/commands/poll-rate-limit-cancel.mjs +1 -1
  42. package/bin/commands/poll-seen-options.d.mts +8 -0
  43. package/bin/commands/poll-seen-options.mjs +7 -0
  44. package/bin/commands/poll.mjs +7 -5
  45. package/bin/comments/review-output-budget.d.mts +32 -0
  46. package/bin/comments/review-output-budget.mjs +55 -0
  47. package/bin/comments/visible-comments.d.mts +3 -1
  48. package/bin/comments/visible-comments.mjs +10 -5
  49. package/bin/config/load.d.mts +6 -0
  50. package/bin/config/load.mjs +7 -0
  51. package/bin/config.json +2 -1
  52. package/bin/github/poll-summary-fingerprint.mjs +5 -1
  53. package/bin/github/poll-summary-review.mjs +2 -1
  54. package/bin/reporters/agent.d.mts +1 -3
  55. package/bin/reporters/agent.mjs +0 -1
  56. package/bin/types/escalate.d.mts +9 -0
  57. package/bin/types/iterate.d.mts +19 -1
  58. package/bin/types/report.d.mts +23 -4
  59. package/bin/util/cap-list.d.mts +7 -0
  60. package/bin/util/cap-list.mjs +13 -0
  61. package/package.json +2 -2
  62. package/plugins/pr-shepherd/.claude-plugin/plugin.json +1 -1
  63. package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
  64. package/plugins/pr-shepherd/.codex.mcp.json +1 -1
  65. package/plugins/pr-shepherd/.mcp.json +1 -1
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "pr-shepherd",
3
3
  "description": "Autonomous PR CI monitor and review-comment resolver for agentic coding tools",
4
- "version": "0.59.3",
4
+ "version": "0.60.1",
5
5
  "author": {
6
6
  "name": "Jonathan Ong",
7
7
  "email": "jonathanrichardong@gmail.com"
package/bin/api.d.mts CHANGED
@@ -18,7 +18,7 @@ export interface CreatePrShepherdOptions {
18
18
  }
19
19
  /** A positive PR number, GitHub pull-request URL, or owner/repo#number reference. */
20
20
  export type PrReference = number | string;
21
- type IterateOptions = Omit<IterateCommandOptions, "format" | "prNumber" | "targetRepository" | "persistSeen" | "fingerprintCache" | "stallDeadlineSink" | "deferQuotaWarning" | "quotaWarningMinimumPollIntervalMinutes">;
21
+ type IterateOptions = Omit<IterateCommandOptions, "format" | "prNumber" | "targetRepository" | "persistSeen" | "deferEditedCommentMarkers" | "fingerprintCache" | "stallDeadlineSink" | "deferQuotaWarning" | "quotaWarningMinimumPollIntervalMinutes">;
22
22
  export type SingleIterateInput = IterateOptions & {
23
23
  pr?: PrReference;
24
24
  prs?: never;
@@ -1,4 +1,4 @@
1
1
  import type { ClassifyRule } from "../types.mts";
2
- /** Codecov's coverage report comment. Coverage gates still report through their status checks. */
2
+ /** Codecov's coverage report and first-run welcome. Coverage gates still report through checks. */
3
3
  declare const rule: ClassifyRule;
4
4
  export default rule;
@@ -1,9 +1,14 @@
1
1
  import { isBotItem } from "./author.mjs";
2
- /** Codecov's coverage report comment. Coverage gates still report through their status checks. */
2
+ const WELCOME = /^## Welcome to \[Codecov\]\(https:\/\/codecov\.io[^\s)]*\) :tada:\n\nOnce you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests\.\n\nThanks for integrating Codecov - We've got you covered :open_umbrella:$/;
3
+ /** Codecov's coverage report and first-run welcome. Coverage gates still report through checks. */
3
4
  const rule = (item) => {
4
5
  if (!isBotItem(item, ["codecov"], ["pr-comment"]))
5
6
  return null;
6
- if (!/^#{1,6}\s*\[?Codecov\b/i.test(item.body.trimStart()))
7
+ const body = item.body.trim();
8
+ if (WELCOME.test(body)) {
9
+ return { autoResolve: true, suppress: true, reason: "Codecov welcome notice" };
10
+ }
11
+ if (!/^#{1,6}\s*\[?Codecov\b/i.test(body))
7
12
  return null;
8
13
  return { autoResolve: true, suppress: true, reason: "Codecov coverage report" };
9
14
  };
@@ -1,8 +1,9 @@
1
1
  import type { ClassifyRule } from "../types.mts";
2
2
  /**
3
- * CodeRabbit progress, rate-limit, review-skipped and draft notices, plus its "Action not
4
- * completed" command replies. If the comment is later edited into a PR walkthrough, its new
5
- * body no longer matches and Shepherd surfaces the edited minimized comment to the agent.
3
+ * CodeRabbit progress, rate-limit, review-skipped and draft notices, its status-only command
4
+ * replies, and its zero-findings, skipped-autofix, and failed-edit status comments. If the
5
+ * comment is later edited into a PR walkthrough, its new body no longer matches and Shepherd
6
+ * surfaces the edited minimized comment to the agent.
6
7
  */
7
8
  declare const rule: ClassifyRule;
8
9
  export default rule;
@@ -1,54 +1,33 @@
1
1
  import { isBotItem } from "./author.mjs";
2
2
  const NOTICE_ENVELOPE = /^<!-- This is an auto-generated comment: (rate limited|skip review|review in progress) by coderabbit.ai -->\n([\s\S]*?)\n<!-- end of auto-generated comment: \1 by coderabbit.ai -->$/;
3
- const COMMAND_REPLY = /^<!-- This is an auto-generated reply by CodeRabbit -->\n<!-- CodeRabbit review command invocation: v2:[a-f0-9]+ -->\n<details>\n<summary>⚠️ Action not completed<\/summary>\n\n(?:Review rate limited\.|Review skipped: \d+ files exceed the limit of \d+\.)\n\n(?:> Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits\. This command is applicable only when automatic reviews are paused\.|---\n\nYour included review limit is currently reached under our \[Fair Usage Limits Policy\]\(https:\/\/docs.coderabbit.ai\/management\/plans#fair-usage-limits-policy\)\. This review may still proceed through usage-based billing if eligible\. Your next included review will be available in \d+ minutes?\.)\n\n<\/details>$/;
4
- const FINISHING_TOUCH_BLOCK = `<!-- finishing_touch_checkbox_start -->
5
-
6
- <details>
7
- <summary><strong>✨ Finishing Touches</strong></summary>
8
- <dl>
9
- <dd>
10
-
11
- <details open>
12
- <summary><strong>📝 Generate docstrings</strong></summary>
13
- <dl>
14
- <dd>
15
-
16
- - [ ] <!-- {"checkboxId":"3e1879ae-f29b-4d0d-8e06-d12b7ba33d98"} --> Commit to this branch
17
- - [ ] <!-- {"checkboxId":"7962f53c-55bc-4827-bfbf-6a18da830691"} --> Create a new PR
18
-
19
- </dd>
20
- </dl>
21
- </details>
22
- <details open>
23
- <summary><strong>🧪 Generate unit tests (beta)</strong></summary>
24
- <dl>
25
- <dd>
26
-
27
- - [ ] <!-- {"checkboxId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} --> Commit to this branch
28
- - [ ] <!-- {"checkboxId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} --> Create a new PR
29
-
30
- </dd>
31
- </dl>
32
- </details>
33
-
34
- </dd>
35
- </dl>
36
- </details>
37
-
38
- <!-- finishing_touch_checkbox_end -->`;
39
- const AUTOPILOT_BLOCK = /\n<!-- autopilot:start -->\n- \[ \] <!-- \{"checkboxId":"2708ad07-9f24-4260-9c11-7dc76a49f2e3"\} --> <strong title="Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts">Autofix<\/strong> · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts\n<!-- autopilot:end -->$/;
3
+ /**
4
+ * Command replies that only report a known status, optionally followed by one of CodeRabbit's
5
+ * fixed explanations. The invocation id varies between releases; any other text stays visible.
6
+ */
7
+ const COMMAND_REPLY = /^<!-- This is an auto-generated reply by CodeRabbit -->\n<!-- CodeRabbit review command invocation: [^\n]* -->\n<details>\n<summary>(?:⚠️ Action not completed|✅ Action performed)<\/summary>\n\n(?:Review rate limited\.|Review skipped: \d+ files exceed the limit of \d+\.|(?:Full review|Review) finished\.|Pull request base or head changed\.|Pull request is closed\.)\n\n(?:(?:> Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits\. This command is applicable only when automatic reviews are paused\.|---\n\nYour included review limit is currently reached under our \[Fair Usage Limits Policy\]\(https:\/\/docs\.coderabbit\.ai\/[^\s)]*\)\. This review may still proceed through usage-based billing if eligible\. Your next included review will be available in \d+ minutes?\.)\n\n)?<\/details>$/;
8
+ /** Standalone status comments: zero-findings completion, skipped autofix, and a failed edit. */
9
+ const STATUS_COMMENTS = [
10
+ /^<!-- coderabbit-review-completion-marker -->\n\*\*Actionable comments posted: 0\*\*$/,
11
+ /^<!-- This is an auto-generated comment: autofix status by CodeRabbit -->\n> \[!NOTE\]\n> Autofix is a beta feature\.[^\n]*\n\nAutofix skipped\. No unresolved CodeRabbit review comments with fix instructions found\.\n<!-- autofix-run-id: [^\n]* -->$/,
12
+ /^> \[!CAUTION\]\n> Failed to replace \(edit\) comment\. This is likely due to insufficient permissions or the comment being deleted\.\n> \n> <details>\n> <summary>Error details<\/summary>\n> \n> ```\n> [^`\n]*\n> ```\n> \n> <\/details>$/,
13
+ ];
14
+ /** One "Finishing Touches" option group: a bold title and only checkbox lines with plain labels. */
15
+ const FINISHING_TOUCH_GROUP = "<details open>\\n<summary><strong>[^<\\n]+<\\/strong><\\/summary>\\n<dl>\\n<dd>\\n\\n(?:- \\[ \\] <!-- \\{[^\\n]*\\} --> [^<\\n]+\\n)+\\n<\\/dd>\\n<\\/dl>\\n<\\/details>\\n";
16
+ /** The finishing-touch checkbox UI, with any checkbox ids and any number of option groups. */
17
+ const FINISHING_TOUCH_BLOCK = new RegExp(`\\n\\n<!-- finishing_touch_checkbox_start -->\\n\\n<details>\\n<summary><strong>✨ Finishing Touches<\\/strong><\\/summary>\\n<dl>\\n<dd>\\n\\n(?:${FINISHING_TOUCH_GROUP})+\\n<\\/dd>\\n<\\/dl>\\n<\\/details>\\n\\n<!-- finishing_touch_checkbox_end -->$`);
18
+ /** The Autofix/Autopilot checkbox, with any checkbox id, one-word label, and description. */
19
+ const AUTOPILOT_BLOCK = /\n<!-- autopilot:start -->\n- \[ \] <!-- \{[^\n]*\} --> <strong title="[^"\n]*">\w+<\/strong> · [^<\n]+\n<!-- autopilot:end -->$/;
40
20
  /** Strips CodeRabbit's footer and header blocks so only the notice envelope remains. */
41
21
  function stripChrome(body) {
42
- const withoutFooter = body
22
+ return body
43
23
  .trim()
44
24
  .replace(/\n<!-- tips_start -->[\s\S]*?<!-- tips_end -->$/, "")
45
25
  .trim()
46
26
  .replace(AUTOPILOT_BLOCK, "")
47
- .trim();
48
- const withoutFinishingTouch = withoutFooter.endsWith(`\n\n${FINISHING_TOUCH_BLOCK}`)
49
- ? withoutFooter.slice(0, -FINISHING_TOUCH_BLOCK.length).trim()
50
- : withoutFooter;
51
- return withoutFinishingTouch
27
+ .trim()
28
+ .replace(FINISHING_TOUCH_BLOCK, "")
29
+ .trim()
30
+ .replace(/^<!-- coderabbit-ai-generated:\S* -->\s*/, "")
52
31
  .replace(/^<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\s*/, "")
53
32
  .replace(/^<!-- review_stack_entry_start -->[\s\S]*?<!-- review_stack_entry_end -->\s*/, "");
54
33
  }
@@ -57,21 +36,24 @@ function isQuotedNotice(quoted) {
57
36
  if (!onlyQuotedLines)
58
37
  return false;
59
38
  return (quoted.includes("> ## Review limit reached\n") ||
39
+ quoted.includes("> ## Rate limit exceeded\n") ||
60
40
  quoted.includes("> ## Draft PR not reviewed\n") ||
61
41
  quoted.includes("> ## Review skipped\n") ||
62
42
  quoted.includes("> Currently processing new changes in this PR. This may take a few minutes, please wait..."));
63
43
  }
64
44
  /**
65
- * CodeRabbit progress, rate-limit, review-skipped and draft notices, plus its "Action not
66
- * completed" command replies. If the comment is later edited into a PR walkthrough, its new
67
- * body no longer matches and Shepherd surfaces the edited minimized comment to the agent.
45
+ * CodeRabbit progress, rate-limit, review-skipped and draft notices, its status-only command
46
+ * replies, and its zero-findings, skipped-autofix, and failed-edit status comments. If the
47
+ * comment is later edited into a PR walkthrough, its new body no longer matches and Shepherd
48
+ * surfaces the edited minimized comment to the agent.
68
49
  */
69
50
  const rule = (item) => {
70
51
  if (!isBotItem(item, ["coderabbitai"], ["pr-comment"]))
71
52
  return null;
72
53
  const body = stripChrome(item.body);
73
54
  const quoted = body.match(NOTICE_ENVELOPE)?.[2]?.trim();
74
- if (!(quoted && isQuotedNotice(quoted)) && !COMMAND_REPLY.test(body))
55
+ const isStatus = COMMAND_REPLY.test(body) || STATUS_COMMENTS.some((notice) => notice.test(body));
56
+ if (!(quoted && isQuotedNotice(quoted)) && !isStatus)
75
57
  return null;
76
58
  return {
77
59
  autoResolve: true,
@@ -1,14 +1,37 @@
1
1
  import { isBotItem } from "./author.mjs";
2
2
  const QUOTA_NOTICES = [
3
3
  /^You have reached your Codex usage limits for security reviews\. Please try again later\.$/,
4
- /^You have reached your Codex usage limits for code reviews\. You can see your limits in the \[Codex usage dashboard\]\(https:\/\/chatgpt.com\/codex\/cloud\/settings\/usage\)\.(?:\nTo continue using code reviews, add credits to your account and enable them for code reviews in your \[settings\]\(https:\/\/chatgpt.com\/codex\/cloud\/settings\/code-review\)\.)?$/,
5
- /^Codex usage limits have been reached for code reviews\. Please check with the admins of this repo to increase the limits by adding credits\.\nRepo admins can enable using credits for code reviews in their \[settings\]\(https:\/\/chatgpt.com\/codex\/cloud\/settings\/code-review\)\.$/,
4
+ /^You have reached your Codex usage limits for code reviews\. You can see your limits in the \[Codex usage dashboard\]\(https:\/\/chatgpt\.com\/[^\s)]*\)\.(?:\nTo continue using code reviews, add credits to your account and enable them for code reviews in your \[settings\]\(https:\/\/chatgpt\.com\/[^\s)]*\)\.)?$/,
5
+ /^Codex usage limits have been reached for code reviews\. Please check with the admins of this repo to increase the limits by adding credits\.\nRepo admins can enable using credits for code reviews in their \[settings\]\(https:\/\/chatgpt\.com\/[^\s)]*\)\.$/,
6
6
  /^Codex usage limits have been reached\. Please try again later\.$/,
7
7
  ];
8
8
  const ABOUT_BLOCK = /\n\n<details> <summary>ℹ️ About Codex (?:in GitHub|security reviews in GitHub)<\/summary>[\s\S]*?<\/details>$/;
9
- const ACTIVITY_SUMMARY = /^<!-- codex-pull-request-review-summary -->\n(?:<!-- codex-review-state:v1 \{[^\n]*\} -->\n)?(?:<!-- codex-security-review:v1 \{[^\n]*\} -->\n)?\n?## Codex Review Summary\n\nThis comment shows the latest Codex review activity on this pull request\.\n\n\| Review \| Status \| Commit \| Review trigger \|\n\| --- \| --- \| --- \| --- \|\n(?:\|[^\n]+\|\n?)+$/;
10
- const CLEAN_REVIEW = /^Codex Review: Didn't find any major issues\. (?:Bravo\.|Breezy!|Hooray!|:\+1:)\n\n\*\*Reviewed commit:\*\* `[a-f0-9]+`$/;
11
- const CLEAN_SECURITY = /^### 🛡️ Codex Security Review(?: · _Automatically triggered_)?\n\nSecurity review completed\. No security issues were found in this pull request\.\n\n\*\*Reviewed commit:\*\* `[a-f0-9]+`\n\n\[View security finding report\]\(https:\/\/chatgpt.com\/codex\/cloud\/tasks\/[a-zA-Z0-9_]+\)\n\n_Only the user who started this review can view the report in Codex\._$/;
9
+ const ACTIVITY_SUMMARY = /^<!-- codex-pull-request-review-summary -->\n(?:<!-- codex-[a-z0-9-]+:[^\n]* -->\n)*\n?## Codex Review Summary\n\nThis comment shows the latest Codex review activity on this pull request\.\n\n\| Review \| Status \| Commit \| Review trigger \|\n\| --- \| --- \| --- \| --- \|\n(?:\|[^\n]+\|\n?)+$/;
10
+ /**
11
+ * Codex's rotating clean-review sign-offs, as observed. Prose is matched only from this list so a
12
+ * short piece of advice in that position stays visible; a new sign-off is shown until added here.
13
+ */
14
+ const SIGN_OFFS = [
15
+ "Already looking forward to the next diff.",
16
+ "Another round soon, please!",
17
+ "Bravo.",
18
+ "Breezy!",
19
+ "Can't wait for the next one!",
20
+ "Chef's kiss.",
21
+ "Delightful!",
22
+ "Hooray!",
23
+ "Keep it up!",
24
+ "Keep them coming!",
25
+ "More of your lovely PRs please.",
26
+ "Nice work!",
27
+ "Swish!",
28
+ "What shall we delve into next?",
29
+ "You're on a roll.",
30
+ ];
31
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
32
+ /** A clean review closes with a known sign-off, a `:shortcode:`, or a single emoji. */
33
+ const CLEAN_REVIEW = new RegExp(`^Codex Review: Didn't find any major issues\\. (?:${SIGN_OFFS.map(escapeRegExp).join("|")}|:[a-z0-9_+-]+:|\\p{Extended_Pictographic}\\uFE0F?)\\n\\n\\*\\*Reviewed commit:\\*\\* \`[a-f0-9]+\`$`, "u");
34
+ const CLEAN_SECURITY = /^### 🛡️ Codex Security Review(?: · _Automatically triggered_)?\n\nSecurity review completed\. No security issues were found in this pull request\.\n\n\*\*Reviewed commit:\*\* `[a-f0-9]+`\n\n\[View security finding report\]\(https:\/\/chatgpt\.com\/[^\s)]*\)\n\n_Only the user who started this review can view the report in Codex\._$/;
12
35
  /**
13
36
  * Codex quota notices, the review-activity status table, and no-findings notices on conversation
14
37
  * comments. Each pattern matches the whole comment, so a notice with any appended finding stays
@@ -21,8 +44,12 @@ const rule = (item) => {
21
44
  if (QUOTA_NOTICES.some((notice) => notice.test(body))) {
22
45
  return { autoResolve: true, suppress: true, reason: "Codex usage-limit notice" };
23
46
  }
24
- // Strip only the trailing About block; extra findings after it stay visible.
25
- const content = body.replace(ABOUT_BLOCK, "").trim();
47
+ // Strip only leading codex-* metadata markers and the trailing About block; extra findings stay
48
+ // visible. The activity table's own marker has no colon, so it is kept for ACTIVITY_SUMMARY.
49
+ const content = body
50
+ .replace(ABOUT_BLOCK, "")
51
+ .trim()
52
+ .replace(/^(?:<!-- codex-[a-z0-9-]+:[^\n]* -->\s*)+/, "");
26
53
  if (ACTIVITY_SUMMARY.test(content)) {
27
54
  return { autoResolve: true, suppress: true, reason: "Codex review-activity table" };
28
55
  }
@@ -1,7 +1,11 @@
1
1
  import { isBotItem } from "./author.mjs";
2
2
  const NOTICES = [
3
3
  /^Looks like this PR is already up-to-date with [^\s!]+! If you'd still like to recreate it from scratch, overwriting any edits, you can request `@dependabot recreate`\.$/,
4
- /^Looks like these dependencies are updatable in another way, so this is no longer needed\.$/,
4
+ /^Looks like (?:these dependencies are|\S+ is) updatable in another way, so this is no longer needed\.$/,
5
+ /^Superseded by #\d+\.$/,
6
+ /^OK, I won't notify you again about this release, but will get in touch when a new version is available\. If you'd rather skip all updates until the next major or minor version, let me know by commenting `@dependabot ignore this major version` or `@dependabot ignore this minor version`\. You can also ignore all major, minor, or patch releases for a dependency by adding an \[`ignore` condition\]\(https:\/\/docs\.github\.com\/[^\s)]*\) with the desired `update_types` to your config file\.\n\nIf you change your mind, just re-open this PR and I'll resolve any conflicts on it\.$/,
7
+ /^This pull request was built based on a group rule\. Closing it will not ignore any of these versions in future pull requests\.\n\nTo ignore these dependencies, configure \[ignore rules\]\(https:\/\/docs\.github\.com\/[^\s)]*\) in dependabot\.yml$/,
8
+ /^Looks like this PR has been edited by someone other than Dependabot\. That means Dependabot can't rebase it - sorry!\n\nIf you're happy for Dependabot to recreate it from scratch, overwriting any edits, you can request `@dependabot recreate`\.$/,
5
9
  ];
6
10
  /** Dependabot's replies that report no change to make. */
7
11
  const rule = (item) => {
@@ -1,4 +1,4 @@
1
1
  import type { ClassifyRule } from "../types.mts";
2
- /** Complete Gemini Code Assist quota and review-limit notices. */
2
+ /** Complete Gemini Code Assist quota, review-limit, and unsupported-file notices. */
3
3
  declare const rule: ClassifyRule;
4
4
  export default rule;
@@ -1,14 +1,25 @@
1
1
  import { isBotItem } from "./author.mjs";
2
- const NOTICES = [
2
+ const QUOTA_NOTICES = [
3
3
  "You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again!",
4
4
  "Review limit reached. I'll be able to review this pull request again tomorrow.",
5
5
  ];
6
- /** Complete Gemini Code Assist quota and review-limit notices. */
6
+ const UNSUPPORTED_NOTICES = [
7
+ "Gemini is unable to generate a review for this pull request due to the file types involved not being currently supported.",
8
+ "Gemini is unable to generate a summary for this pull request due to the file types involved not being currently supported.",
9
+ ];
10
+ /** A GitHub alert prefix such as `> [!WARNING]`, with the notice on the following quoted line. */
11
+ const ALERT_PREFIX = /^> \[!(?:NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]\n> /;
12
+ /** Complete Gemini Code Assist quota, review-limit, and unsupported-file notices. */
7
13
  const rule = (item) => {
8
14
  if (!isBotItem(item, ["gemini-code-assist"], ["pr-comment"]))
9
15
  return null;
10
- if (!NOTICES.includes(item.body.trim()))
11
- return null;
12
- return { autoResolve: true, suppress: true, reason: "Gemini quota or review-limit notice" };
16
+ const notice = item.body.trim().replace(ALERT_PREFIX, "");
17
+ if (QUOTA_NOTICES.includes(notice)) {
18
+ return { autoResolve: true, suppress: true, reason: "Gemini quota or review-limit notice" };
19
+ }
20
+ if (UNSUPPORTED_NOTICES.includes(notice)) {
21
+ return { autoResolve: true, suppress: true, reason: "Gemini unsupported-file-type notice" };
22
+ }
23
+ return null;
13
24
  };
14
25
  export default rule;
@@ -1,4 +1,4 @@
1
1
  import type { ClassifyRule } from "../types.mts";
2
- /** Complete Kilo Code no-issues status or report with a file list and no extra prose. */
2
+ /** Complete Kilo Code no-issues reports and its out-of-credits notice, with no extra findings. */
3
3
  declare const rule: ClassifyRule;
4
4
  export default rule;
@@ -1,20 +1,47 @@
1
1
  import { isBotItem } from "./author.mjs";
2
2
  const PLAIN_STATUS = /^Status: ✅ No Issues Found \| Recommendation: Merge$/;
3
3
  const REVIEW_REPORT = /^## Code Review Summary\n\n\*\*Status:\*\* No Issues Found \| \*\*Recommendation:\*\* Merge(?:\n\n### Files Reviewed \((\d+) files\)\n((?:- `[^`\n]+`(?:\n|$))+))?$/;
4
- /** Complete Kilo Code no-issues status or report with a file list and no extra prose. */
4
+ /**
5
+ * The marked report: a no-issues status, an optional plain overview (see `isPlainOverview`), the
6
+ * collapsed file list, and `kilo-*` usage footers. Any other status, such as "N Issues Found",
7
+ * does not match, and Kilo rewrites this same comment when a later push finds issues. A stray
8
+ * `EOF` line, left by some models before the footer, is tolerated.
9
+ */
10
+ const MARKED_REPORT = /^<!-- kilo-review -->\n\n?## Code Review Summary\n\n\*\*Status:\*\* No (?:New )?Issues Found \| \*\*Recommendation:\*\* Merge(?: \([^)\n]*\))?\n\n([\s\S]*?\n)?<details>\n<summary><b>Files Reviewed \([^)\n]*\)<\/b><\/summary>\n(?:(?!<details>)[\s\S])*?<\/details>\n+(?:EOF\n+)?---\n(?:<!-- kilo-[a-z-]+ -->\n<sub>[^\n]*<\/sub>(?:\n|$))+$/;
11
+ const BILLING = /^<!-- kilo-billing-notice -->\n\*\*Kilo Code Review could not run — your account is out of credits\.\*\*\n\n\[Add credits\]\(https:\/\/app\.kilo\.ai\/[^\s)]*\) or \[switch to a free model\]\(https:\/\/app\.kilo\.ai\/[^\s)]*\) to enable reviews on this change\.$/;
12
+ /**
13
+ * True when the overview is only prose and lists, optionally under `### Overview`. Any other
14
+ * heading, HTML block, or table could carry a finding, so it keeps the report visible.
15
+ */
16
+ function isPlainOverview(overview) {
17
+ return (overview ?? "")
18
+ .split("\n")
19
+ .every((line) => line === "### Overview" || !/^\s*(?:#|<|\|)/.test(line));
20
+ }
21
+ /** True for the marked report when its overview is plain. */
22
+ function isMarkedReport(body) {
23
+ const report = body.match(MARKED_REPORT);
24
+ return report != null && isPlainOverview(report[1]);
25
+ }
26
+ /** True for the unmarked report when its file count agrees with the listed files. */
27
+ function isConsistentReport(body) {
28
+ const report = body.match(REVIEW_REPORT);
29
+ if (!report)
30
+ return false;
31
+ if (report[1] === undefined)
32
+ return true;
33
+ return report[2].trimEnd().split("\n").length === Number(report[1]);
34
+ }
35
+ /** Complete Kilo Code no-issues reports and its out-of-credits notice, with no extra findings. */
5
36
  const rule = (item) => {
6
37
  if (!isBotItem(item, ["kilo-code-bot"], ["pr-comment"]))
7
38
  return null;
8
39
  const body = item.body.trim();
9
- if (!PLAIN_STATUS.test(body)) {
10
- const report = body.match(REVIEW_REPORT);
11
- if (!report)
12
- return null;
13
- if (report[1] !== undefined) {
14
- const listed = report[2].trimEnd().split("\n").length;
15
- if (listed !== Number(report[1]))
16
- return null;
17
- }
40
+ if (BILLING.test(body)) {
41
+ return { autoResolve: true, suppress: true, reason: "Kilo Code out-of-credits notice" };
42
+ }
43
+ if (!PLAIN_STATUS.test(body) && !isMarkedReport(body) && !isConsistentReport(body)) {
44
+ return null;
18
45
  }
19
46
  return { autoResolve: true, suppress: true, reason: "Kilo Code no-issues report" };
20
47
  };
@@ -1,5 +1,5 @@
1
1
  import { isBotItem } from "./author.mjs";
2
- const NO_ACCESS = /^Hi @[A-Za-z0-9-]+! 👋\n\nYour private repo does not have access to Sourcery\.\n\nPlease \[upgrade\]\(https:\/\/app\.sourcery\.ai\/login\?connection=github(?:&git_namespace_id=\d+)?&from_surface=github_bot&intent=private_repo_upsell\) to continue using Sourcery ✨$/;
2
+ const NO_ACCESS = /^Hi @[A-Za-z0-9-]+! 👋\n\nYour private repo does not have access to Sourcery\.\n\nPlease \[upgrade\]\(https:\/\/app\.sourcery\.ai\/[^\s)]*\) to continue using Sourcery ✨$/;
3
3
  const RATE_LIMIT = /^Sorry @[A-Za-z0-9-]+, you have reached your weekly rate limit for Sourcery\. Please try again later\.$/;
4
4
  /** Complete Sourcery private-repo upsell and weekly rate-limit notices. */
5
5
  const rule = (item) => {
@@ -0,0 +1,14 @@
1
+ import type { EditedComment } from "../types.mts";
2
+ /** Insert the edited-comments section before `## Instructions` (or append it). */
3
+ export declare function insertEditedCommentsSection(text: string, comments: readonly EditedComment[] | undefined, verbose: boolean): string;
4
+ /** JSON projection matching the text section: the agent comment plus its minimized status. */
5
+ export declare function projectEditedComments(comments: readonly EditedComment[]): {
6
+ id: string;
7
+ viewerCanMinimize?: boolean;
8
+ author: string;
9
+ authorType?: import("../types.mts").AuthorType;
10
+ authorAssociation?: import("../types.mts").CommentAuthorAssociation;
11
+ body: string;
12
+ url: string;
13
+ isMinimized?: boolean | undefined;
14
+ }[];
@@ -0,0 +1,39 @@
1
+ import { blockquote, renderAuthor } from "./list-formatters.mjs";
2
+ import { BODY_TRUNCATE_MAX_CHARS } from "./body-truncate.mjs";
3
+ import { toAgentComment } from "../reporters/agent.mjs";
4
+ const HEADING = "Edited comments";
5
+ function editedTag(c) {
6
+ return c.isMinimized ? "[status: minimized, edited]" : "[edited since first look]";
7
+ }
8
+ /** Markdown for `editedComments`; null when there are none. Edits are informational only. */
9
+ function formatEditedCommentsSection(comments, verbose) {
10
+ if (!comments?.length)
11
+ return null;
12
+ const cap = verbose ? undefined : BODY_TRUNCATE_MAX_CHARS;
13
+ const parts = [
14
+ `## ${HEADING} (${comments.length}) — informational; edits do not block readiness`,
15
+ ];
16
+ for (const c of comments) {
17
+ const heading = c.url ? `[commentId=${c.id}](${c.url})` : `\`commentId=${c.id}\``;
18
+ parts.push(`### ${heading} (${renderAuthor(c.author, c.authorType, c.authorAssociation)}) ${editedTag(c)}`);
19
+ parts.push(blockquote(c.body, cap, c.url));
20
+ }
21
+ return parts.join("\n\n");
22
+ }
23
+ /** Insert the edited-comments section before `## Instructions` (or append it). */
24
+ export function insertEditedCommentsSection(text, comments, verbose) {
25
+ const section = formatEditedCommentsSection(comments, verbose);
26
+ if (!section)
27
+ return text;
28
+ const index = text.lastIndexOf("## Instructions");
29
+ if (index === -1)
30
+ return `${text}\n\n${section}`;
31
+ return `${text.slice(0, index)}${section}\n\n${text.slice(index)}`;
32
+ }
33
+ /** JSON projection matching the text section: the agent comment plus its minimized status. */
34
+ export function projectEditedComments(comments) {
35
+ return comments.map((c) => ({
36
+ ...toAgentComment(c),
37
+ ...(c.isMinimized && { isMinimized: true }),
38
+ }));
39
+ }
@@ -13,6 +13,10 @@ export function formatFixCodeResult(header, result, opts = {}) {
13
13
  const verbose = opts.verbose ?? false;
14
14
  const topCap = verbose ? undefined : BODY_TRUNCATE_MAX_CHARS;
15
15
  const sections = [header];
16
+ if (result.fix.reviewItems) {
17
+ const { shown, total } = result.fix.reviewItems;
18
+ sections.push(`**showing** ${shown} of ${total} review items`);
19
+ }
16
20
  if (result.mergedBasePullRequests?.length) {
17
21
  sections.push("## Merged PRs matching the current base");
18
22
  sections.push(result.mergedBasePullRequests
@@ -50,9 +54,8 @@ export function formatFixCodeResult(header, result, opts = {}) {
50
54
  sections.push("## Actionable comments");
51
55
  for (const c of result.fix.actionableComments) {
52
56
  const heading = c.url ? `[commentId=${c.id}](${c.url})` : `\`commentId=${c.id}\``;
53
- const editedMarker = c.edited ? " [edited since first look]" : "";
54
57
  const authorizationMarker = c.viewerCanMinimize === false ? " [viewer cannot minimize]" : "";
55
- sections.push(`### ${heading} (${renderAuthor(c.author, c.authorType, c.authorAssociation)})${authorizationMarker}${editedMarker}`);
58
+ sections.push(`### ${heading} (${renderAuthor(c.author, c.authorType, c.authorAssociation)})${authorizationMarker}`);
56
59
  sections.push(blockquote(c.body, topCap, c.url));
57
60
  }
58
61
  }
@@ -1,2 +1,2 @@
1
1
  import type { IterateResult } from "../types.mts";
2
- export declare function formatActivityLine(result: IterateResult): string | null;
2
+ export declare function formatActivityLine(result: IterateResult, verbose?: boolean): string | null;
@@ -1,4 +1,5 @@
1
- export function formatActivityLine(result) {
1
+ import { formatNameList } from "./iterate-check-names.mjs";
2
+ export function formatActivityLine(result, verbose = false) {
2
3
  const activity = result.activity ?? {
3
4
  commitCount: 0,
4
5
  reviewRoundCount: 0,
@@ -17,10 +18,8 @@ export function formatActivityLine(result) {
17
18
  parts.push(`${activity.reviewItemsSinceLatestCommit.length} review items since latest commit`);
18
19
  }
19
20
  if (hasActiveChecks) {
20
- parts.push(`active: ${result
21
- .inProgressChecks.slice(0, 5)
22
- .map((check) => `\`${check.name}\``)
23
- .join(", ")}`);
21
+ const names = result.inProgressChecks.map((check) => check.name);
22
+ parts.push(`active: ${formatNameList(names, verbose)}`);
24
23
  }
25
24
  return `**activity** ${parts.join(" · ")}`;
26
25
  }
@@ -0,0 +1,5 @@
1
+ import type { IterateResult } from "../types.mts";
2
+ /** Backticked inline name list; lean output caps it and appends `+N more`. */
3
+ export declare function formatNameList(names: readonly string[], verbose: boolean): string;
4
+ /** `**ignored**` and `**superseded**` header lines, omitted when their lists are empty. */
5
+ export declare function formatSuppressedNameLines(result: IterateResult, verbose: boolean): string[];
@@ -0,0 +1,17 @@
1
+ import { capList, moreSuffix } from "../util/cap-list.mjs";
2
+ /** Backticked inline name list; lean output caps it and appends `+N more`. */
3
+ export function formatNameList(names, verbose) {
4
+ const { shown, hidden } = verbose ? { shown: names, hidden: 0 } : capList(names);
5
+ return shown.map((n) => "`" + n + "`").join(", ") + moreSuffix(hidden);
6
+ }
7
+ /** `**ignored**` and `**superseded**` header lines, omitted when their lists are empty. */
8
+ export function formatSuppressedNameLines(result, verbose) {
9
+ const lines = [];
10
+ if (result.ignoredNames && result.ignoredNames.length > 0) {
11
+ lines.push(`**ignored** ${formatNameList(result.ignoredNames, verbose)}`);
12
+ }
13
+ if (result.supersededNames && result.supersededNames.length > 0) {
14
+ lines.push(`**superseded** ${formatNameList(result.supersededNames, verbose)}`);
15
+ }
16
+ return lines;
17
+ }
@@ -1,3 +1,5 @@
1
+ /* eslint-disable max-lines */
2
+ import { insertEditedCommentsSection } from "./edited-comments-format.mjs";
1
3
  import { formatFixCodeResult } from "./fix-formatter.mjs";
2
4
  import { formatRelevantChecks } from "./iterate-checks-formatter.mjs";
3
5
  import { joinSections } from "../util/markdown.mjs";
@@ -6,6 +8,7 @@ import { formatMergeRequirementLines } from "../merge-status/requirements-format
6
8
  import { appendMergeQueueHeader, formatDeferredWorkLine, formatMergeAction, } from "./iterate-merge-formatter.mjs";
7
9
  import { formatApiUsage, formatQuotaWarning } from "./api-usage-formatter.mjs";
8
10
  import { formatActivityLine } from "./iterate-activity-formatter.mjs";
11
+ import { formatSuppressedNameLines } from "./iterate-check-names.mjs";
9
12
  import { branchStateSegment } from "./iterate-branch-segment.mjs";
10
13
  import { insertRuleAutoResolveSection } from "../commands/rule-auto-resolve-format.mjs";
11
14
  import { formatTransportEvidence } from "./transport-formatter.mjs";
@@ -41,7 +44,7 @@ export function formatIterateResult(result, opts) {
41
44
  instructions.includes(`--repo ${result.repo} `));
42
45
  return generated ? full.replace(repoSeg, "") : full;
43
46
  };
44
- const finish = (text) => dropRepeatedRepo(insertRuleAutoResolveSection(text, result.ruleAutoResolve));
47
+ const finish = (text) => dropRepeatedRepo(insertEditedCommentsSection(insertRuleAutoResolveSection(text, result.ruleAutoResolve), result.editedComments, verbose));
45
48
  const heading = `# PR #${result.pr} [${result.action.toUpperCase()}]`;
46
49
  const reviewDecisionSeg = result.mergeStatus === "BLOCKED" && result.reviewDecision
47
50
  ? ` · **reviewDecision** \`${result.reviewDecision}\``
@@ -57,7 +60,7 @@ export function formatIterateResult(result, opts) {
57
60
  let summaryLine;
58
61
  if (verbose) {
59
62
  const verboseBranch = branchSeg ? ` · ${branchSeg}` : "";
60
- summaryLine = `**summary** ${result.summary.passing} passing, ${result.summary.skipped} skipped, ${result.summary.filtered} filtered, ${result.summary.inProgress} inProgress, ${result.summary.superseded} superseded · **remainingSeconds** ${result.remainingSeconds} · **blockingBotReviewInProgress** ${result.blockingBotReviewInProgress} · **isDraft** ${result.isDraft} · **shouldCancel** ${result.shouldCancel}${verboseBranch}${readyDelaySuffix ? ` · **ready-delay** \`${readyDelaySuffix}\` (override)` : ""}`;
63
+ summaryLine = `**summary** ${result.summary.passing} passing, ${result.summary.skipped} skipped, ${result.summary.filtered} filtered, ${result.summary.inProgress} inProgress, ${result.summary.queued} queued, ${result.summary.superseded} superseded · **remainingSeconds** ${result.remainingSeconds} · **blockingBotReviewInProgress** ${result.blockingBotReviewInProgress} · **isDraft** ${result.isDraft} · **shouldCancel** ${result.shouldCancel}${verboseBranch}${readyDelaySuffix ? ` · **ready-delay** \`${readyDelaySuffix}\` (override)` : ""}`;
61
64
  }
62
65
  else {
63
66
  const counts = [];
@@ -69,6 +72,8 @@ export function formatIterateResult(result, opts) {
69
72
  counts.push(`${result.summary.filtered} filtered`);
70
73
  if (result.summary.inProgress > 0)
71
74
  counts.push(`${result.summary.inProgress} inProgress`);
75
+ if (result.summary.queued > 0)
76
+ counts.push(`${result.summary.queued} queued`);
72
77
  if (result.summary.superseded > 0)
73
78
  counts.push(`${result.summary.superseded} superseded`);
74
79
  const segs = counts.length > 0 ? [`**summary** ${counts.join(", ")}`] : [];
@@ -114,17 +119,10 @@ export function formatIterateResult(result, opts) {
114
119
  else if (requiredLine) {
115
120
  headerLines.push(requiredLine);
116
121
  }
117
- if (result.ignoredNames && result.ignoredNames.length > 0) {
118
- const names = result.ignoredNames.map((n) => "`" + n + "`").join(", ");
119
- headerLines.push(`**ignored** ${names}`);
120
- }
121
- if (result.supersededNames && result.supersededNames.length > 0) {
122
- const names = result.supersededNames.map((n) => "`" + n + "`").join(", ");
123
- headerLines.push(`**superseded** ${names}`);
124
- }
122
+ headerLines.push(...formatSuppressedNameLines(result, verbose));
125
123
  appendUnreportedLines(headerLines, result);
126
124
  appendMergeQueueHeader(headerLines, result);
127
- const activityLine = formatActivityLine(result);
125
+ const activityLine = formatActivityLine(result, verbose);
128
126
  if (activityLine)
129
127
  headerLines.push(activityLine);
130
128
  headerLines.push(...formatNextCheckLines(result.nextCheck));
@@ -176,14 +174,7 @@ export function formatIterateResult(result, opts) {
176
174
  else if (requiredLine) {
177
175
  cancelHeaderLines.push(requiredLine);
178
176
  }
179
- if (result.ignoredNames && result.ignoredNames.length > 0) {
180
- const ignoredStr = result.ignoredNames.map((n) => "`" + n + "`").join(", ");
181
- cancelHeaderLines.push(`**ignored** ${ignoredStr}`);
182
- }
183
- if (result.supersededNames && result.supersededNames.length > 0) {
184
- const supersededStr = result.supersededNames.map((n) => "`" + n + "`").join(", ");
185
- cancelHeaderLines.push(`**superseded** ${supersededStr}`);
186
- }
177
+ cancelHeaderLines.push(...formatSuppressedNameLines(result, verbose));
187
178
  appendUnreportedLines(cancelHeaderLines, result);
188
179
  appendMergeQueueHeader(cancelHeaderLines, result);
189
180
  if (activityLine)
@@ -198,6 +189,11 @@ export function formatIterateResult(result, opts) {
198
189
  case "escalate":
199
190
  return finish(joinSections([
200
191
  header,
192
+ ...(result.escalate.reviewItems
193
+ ? [
194
+ `**showing** ${result.escalate.reviewItems.shown} of ${result.escalate.reviewItems.total} review items`,
195
+ ]
196
+ : []),
201
197
  ...(apiUsage ? [apiUsage] : []),
202
198
  ...(verboseChecks ? [verboseChecks] : []),
203
199
  result.escalate.humanMessage,