claude-dev-env 1.34.1 → 1.36.0

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 (148) hide show
  1. package/agents/clean-coder.md +109 -1
  2. package/agents/docs-agent.md +1 -1
  3. package/agents/project-docs-analyzer.md +0 -1
  4. package/agents/skill-to-agent-converter.md +0 -1
  5. package/bin/install.mjs +28 -8
  6. package/bin/install.test.mjs +9 -1
  7. package/commands/initialize.md +0 -1
  8. package/commands/readability-review.md +4 -4
  9. package/commands/review-plan.md +2 -4
  10. package/commands/stubcheck.md +1 -2
  11. package/docs/CODE_RULES.md +3 -0
  12. package/docs/agents-md-alignment-plan.md +123 -0
  13. package/hooks/blocking/code_rules_enforcer.py +686 -60
  14. package/hooks/blocking/es_exe_path_rewriter.py +10 -4
  15. package/hooks/blocking/test_code_rules_enforcer.py +273 -39
  16. package/hooks/blocking/test_code_rules_enforcer_annotations.py +97 -0
  17. package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +106 -0
  18. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +173 -0
  19. package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +328 -0
  20. package/hooks/blocking/test_code_rules_enforcer_config_path.py +0 -20
  21. package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +33 -11
  22. package/hooks/blocking/test_code_rules_enforcer_existence_checks.py +0 -18
  23. package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +291 -0
  24. package/hooks/blocking/test_code_rules_enforcer_inline_literal_collections.py +155 -0
  25. package/hooks/blocking/test_code_rules_enforcer_loop_variable_naming.py +194 -0
  26. package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +49 -13
  27. package/hooks/blocking/test_code_rules_enforcer_skip_decorators.py +0 -26
  28. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +234 -0
  29. package/hooks/blocking/test_code_rules_enforcer_sys_path_insert.py +157 -0
  30. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +244 -0
  31. package/hooks/blocking/test_es_exe_path_rewriter.py +81 -3
  32. package/hooks/blocking/test_windows_rmtree_blocker.py +120 -8
  33. package/hooks/blocking/windows_rmtree_blocker.py +23 -6
  34. package/hooks/config/banned_identifiers_constants.py +24 -0
  35. package/hooks/config/hardcoded_user_path_constants.py +12 -0
  36. package/hooks/config/hook_log_extractor_constants.py +1 -1
  37. package/hooks/config/pre_tool_use_stdin.py +48 -0
  38. package/hooks/config/setup_project_paths_constants.py +4 -0
  39. package/hooks/config/stuttering_check_config.py +14 -0
  40. package/hooks/config/stuttering_import_binding_constants.py +11 -0
  41. package/hooks/config/sys_path_insert_constants.py +4 -0
  42. package/hooks/config/test_banned_identifiers_constants.py +48 -0
  43. package/hooks/config/test_hardcoded_user_path_constants.py +78 -0
  44. package/hooks/config/test_hook_log_extractor_constants.py +3 -3
  45. package/hooks/config/test_pre_tool_use_stdin.py +80 -0
  46. package/hooks/config/unused_module_import_constants.py +7 -0
  47. package/hooks/config/windows_rmtree_blocker_constants.py +3 -0
  48. package/hooks/diagnostic/hook_log_stop_wrapper.py +7 -4
  49. package/hooks/git-hooks/config.py +3 -3
  50. package/hooks/git-hooks/test_gate_utils.py +10 -10
  51. package/hooks/mypy.ini +2 -0
  52. package/package.json +1 -1
  53. package/rules/gh-paginate.md +125 -0
  54. package/skills/bugteam/CONSTRAINTS.md +12 -6
  55. package/skills/bugteam/PROMPTS.md +0 -39
  56. package/skills/bugteam/SKILL.md +93 -125
  57. package/skills/bugteam/SKILL_EVALS.md +25 -23
  58. package/skills/bugteam/reference/README.md +2 -0
  59. package/skills/bugteam/reference/audit-and-teammates.md +2 -2
  60. package/skills/bugteam/reference/copilot-gap-analysis.md +12 -0
  61. package/skills/bugteam/reference/teardown-publish-permissions.md +1 -1
  62. package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +113 -0
  63. package/skills/bugteam/reference/workflow-path-b-task-harness.md +48 -0
  64. package/skills/bugteam/test_skill_additions.py +13 -4
  65. package/skills/bugteam/test_team_lifecycle.py +94 -0
  66. package/skills/findbugs/SKILL.md +3 -3
  67. package/skills/fixbugs/SKILL.md +4 -4
  68. package/skills/monitor-open-prs/SKILL.md +32 -2
  69. package/skills/monitor-open-prs/test_team_lifecycle.py +46 -0
  70. package/skills/pr-converge/SKILL.md +576 -95
  71. package/skills/pr-converge/scripts/README.md +145 -0
  72. package/skills/pr-converge/scripts/caller-window-pid.ps1 +86 -0
  73. package/skills/pr-converge/scripts/check_pr_mergeability.py +79 -0
  74. package/skills/pr-converge/scripts/config/pr_converge_constants.py +65 -0
  75. package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +176 -0
  76. package/skills/pr-converge/scripts/cursor-agents-continue-caller.cmd +9 -0
  77. package/skills/pr-converge/scripts/cursor-agents-continue-stop-others.ps1 +16 -0
  78. package/skills/pr-converge/scripts/cursor-agents-continue.ahk +172 -0
  79. package/skills/pr-converge/scripts/cursor-agents-continue.cmd +2 -0
  80. package/skills/pr-converge/scripts/evict_cached_config_modules.py +20 -0
  81. package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +110 -0
  82. package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +103 -0
  83. package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +112 -0
  84. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +121 -0
  85. package/skills/pr-converge/scripts/mark_pr_ready.py +54 -0
  86. package/skills/pr-converge/scripts/open_followup_copilot_pr.py +136 -0
  87. package/skills/pr-converge/scripts/post-bugbot-run.helpers.ps1 +49 -0
  88. package/skills/pr-converge/scripts/post-bugbot-run.ps1 +33 -0
  89. package/skills/pr-converge/scripts/reply_to_inline_comment.py +84 -0
  90. package/skills/pr-converge/scripts/request_copilot_review.py +71 -0
  91. package/skills/pr-converge/scripts/resolve_pr_head.py +58 -0
  92. package/skills/pr-converge/scripts/review_field_helpers.py +43 -0
  93. package/skills/pr-converge/scripts/test_check_pr_mergeability.py +126 -0
  94. package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +22 -0
  95. package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +342 -0
  96. package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +220 -0
  97. package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +372 -0
  98. package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +280 -0
  99. package/skills/pr-converge/scripts/test_mark_pr_ready.py +69 -0
  100. package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +236 -0
  101. package/skills/pr-converge/scripts/test_post_bugbot_run.py +195 -0
  102. package/skills/pr-converge/scripts/test_reply_to_inline_comment.py +159 -0
  103. package/skills/pr-converge/scripts/test_request_copilot_review.py +101 -0
  104. package/skills/pr-converge/scripts/test_resolve_pr_head.py +79 -0
  105. package/skills/pr-converge/scripts/test_review_field_helpers.py +80 -0
  106. package/skills/pr-converge/scripts/test_trigger_bugbot.py +139 -0
  107. package/skills/pr-converge/scripts/test_view_pr_context.py +111 -0
  108. package/skills/pr-converge/scripts/trigger_bugbot.py +77 -0
  109. package/skills/pr-converge/scripts/view_pr_context.py +47 -0
  110. package/skills/pr-converge/test_team_lifecycle.py +47 -0
  111. package/skills/pr-converge/workflows/ahk-auto-continue-loop.md +108 -0
  112. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +37 -0
  113. package/skills/qbug/SKILL.md +4 -4
  114. package/skills/qbug/test_qbug_skill_post_fix_audit.py +2 -2
  115. package/skills/resume-review/SKILL.md +261 -0
  116. package/agents/agent-writer.md +0 -157
  117. package/agents/config-centralizer.md +0 -686
  118. package/agents/config-extraction-agent.md +0 -225
  119. package/agents/doc-orchestrator.md +0 -47
  120. package/agents/docx-agent.md +0 -211
  121. package/agents/magic-value-eliminator-agent.md +0 -72
  122. package/agents/mandatory-agent-workflow-agent.md +0 -88
  123. package/agents/parallel-workflow-coordinator.md +0 -779
  124. package/agents/pdf-agent.md +0 -302
  125. package/agents/project-context-loader.md +0 -238
  126. package/agents/readability-review-agent.md +0 -76
  127. package/agents/refactoring-specialist.md +0 -69
  128. package/agents/right-sized-engineer.md +0 -129
  129. package/agents/session-continuity-manager.md +0 -53
  130. package/agents/stub-detector-agent.md +0 -140
  131. package/agents/tdd-test-writer.md +0 -62
  132. package/agents/test-data-builder.md +0 -68
  133. package/agents/tooling-builder.md +0 -78
  134. package/agents/validation-expert.md +0 -71
  135. package/agents/xlsx-agent.md +0 -169
  136. package/skills/bugteam/scripts/README.md +0 -58
  137. package/skills/bugteam/scripts/_claude_permissions_common.py +0 -219
  138. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +0 -633
  139. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +0 -260
  140. package/skills/bugteam/scripts/bugteam_preflight.py +0 -201
  141. package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +0 -17
  142. package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -109
  143. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -135
  144. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +0 -271
  145. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -267
  146. package/skills/bugteam/scripts/test_bugteam_preflight.py +0 -189
  147. package/skills/bugteam/scripts/test_claude_permissions_common.py +0 -44
  148. /package/skills/{bugteam → pr-converge}/scripts/config/__init__.py +0 -0
@@ -0,0 +1,172 @@
1
+ ; cursor-agents-continue.ahk
2
+ ; Sends "continue" + Enter to a Cursor Agents window every 5 minutes when enabled.
3
+ ; Toggle with Ctrl+Alt+A. Optional first CLI arg overrides the target window title substring.
4
+ ; A small colored pill on the upper-right corner of the rightmost monitor shows ON/OFF state.
5
+
6
+ #Requires AutoHotkey v2.0
7
+ #SingleInstance Force
8
+
9
+ SetTitleMatchMode 2
10
+
11
+ DEFAULT_TARGET_WINDOW_TITLE := "Cursor Agents"
12
+ SEND_INTERVAL_MILLISECONDS := 300000
13
+ ACTIVATION_TIMEOUT_SECONDS := 2
14
+ FOCUS_SETTLE_SLEEP_MS := 150
15
+ POST_TEXT_SLEEP_MS := 50
16
+ CONTINUE_PHRASE := "continue"
17
+
18
+ INDICATOR_DESIGN_WIDTH_PX := 230
19
+ INDICATOR_DESIGN_HEIGHT_PX := 28
20
+ INDICATOR_DESIGN_RIGHT_OFFSET_PX := 16
21
+ INDICATOR_DESIGN_TOP_OFFSET_PX := 8
22
+ INDICATOR_TEXT_DESIGN_PADDING_PX := 4
23
+ INDICATOR_TEXT_DESIGN_HEIGHT_PX := 20
24
+
25
+ INDICATOR_FONT_NAME := "Segoe UI"
26
+ INDICATOR_FONT_POINT_SIZE := 9
27
+ INDICATOR_FONT_OPTIONS := "s" INDICATOR_FONT_POINT_SIZE " cWhite Bold"
28
+
29
+ COLOR_OFF_BACKGROUND := "800000"
30
+ COLOR_ON_BACKGROUND := "006400"
31
+ LABEL_OFF := "AGENTS: OFF (Ctrl+Alt+A)"
32
+ LABEL_ON := "AGENTS: ON (Ctrl+Alt+A)"
33
+
34
+ DPI_REFERENCE := 96
35
+
36
+ AUTO_START_FLAG := "--start-on"
37
+
38
+ INT32_MINIMUM_VALUE := -2147483648
39
+
40
+ terminate_other_script_instances() {
41
+ stop_script := A_ScriptDir "\cursor-agents-continue-stop-others.ps1"
42
+ if !FileExist(stop_script)
43
+ return
44
+ RunWait('pwsh -NoProfile -NoLogo -ExecutionPolicy Bypass -File "' stop_script '" -KeepProcessId ' ProcessExist(), , "Hide")
45
+ }
46
+
47
+ terminate_other_script_instances()
48
+
49
+ target_window_title := resolve_target_window_title_from_arguments()
50
+ should_auto_start := has_auto_start_flag()
51
+ is_enabled := false
52
+
53
+ indicator := unset
54
+ indicator_label := unset
55
+ build_indicator()
56
+ render_indicator(false)
57
+ if (should_auto_start)
58
+ apply_enabled_state(true)
59
+
60
+ ^!a:: {
61
+ global is_enabled
62
+ apply_enabled_state(!is_enabled)
63
+ }
64
+
65
+ apply_enabled_state(next_enabled) {
66
+ global is_enabled
67
+ is_enabled := next_enabled
68
+ render_indicator(is_enabled)
69
+ if (is_enabled) {
70
+ send_continue_to_target()
71
+ SetTimer(send_continue_to_target, SEND_INTERVAL_MILLISECONDS)
72
+ return
73
+ }
74
+ SetTimer(send_continue_to_target, 0)
75
+ }
76
+
77
+ has_auto_start_flag() {
78
+ each_arg_index := 1
79
+ while (each_arg_index <= A_Args.Length) {
80
+ if (A_Args[each_arg_index] = AUTO_START_FLAG)
81
+ return true
82
+ each_arg_index++
83
+ }
84
+ return false
85
+ }
86
+
87
+ resolve_target_window_title_from_arguments() {
88
+ candidate := ""
89
+ each_arg_index := 1
90
+ while (each_arg_index <= A_Args.Length) {
91
+ arg_value := A_Args[each_arg_index]
92
+ if (arg_value = AUTO_START_FLAG) {
93
+ each_arg_index++
94
+ continue
95
+ }
96
+ if (candidate = "") {
97
+ candidate := arg_value
98
+ }
99
+ each_arg_index++
100
+ }
101
+ if (candidate = "")
102
+ return DEFAULT_TARGET_WINDOW_TITLE
103
+ return IsInteger(candidate) ? "ahk_pid " candidate : candidate
104
+ }
105
+
106
+
107
+ build_indicator() {
108
+ global indicator, indicator_label
109
+ dpi_scale_factor := A_ScreenDPI / DPI_REFERENCE
110
+ scaled_width := Round(INDICATOR_DESIGN_WIDTH_PX * dpi_scale_factor)
111
+ scaled_height := Round(INDICATOR_DESIGN_HEIGHT_PX * dpi_scale_factor)
112
+ scaled_right_offset := Round(INDICATOR_DESIGN_RIGHT_OFFSET_PX * dpi_scale_factor)
113
+ scaled_top_offset := Round(INDICATOR_DESIGN_TOP_OFFSET_PX * dpi_scale_factor)
114
+ scaled_text_padding := Round(INDICATOR_TEXT_DESIGN_PADDING_PX * dpi_scale_factor)
115
+ scaled_text_height := Round(INDICATOR_TEXT_DESIGN_HEIGHT_PX * dpi_scale_factor)
116
+ scaled_text_width := scaled_width - (scaled_text_padding * 2)
117
+
118
+ rightmost_monitor_bounds := find_rightmost_monitor_bounds()
119
+
120
+ indicator := Gui("-Caption +AlwaysOnTop +ToolWindow -DPIScale")
121
+ indicator.BackColor := COLOR_OFF_BACKGROUND
122
+ indicator.SetFont(INDICATOR_FONT_OPTIONS, INDICATOR_FONT_NAME)
123
+ indicator_label := indicator.Add(
124
+ "Text",
125
+ "x" scaled_text_padding " y" scaled_text_padding
126
+ . " w" scaled_text_width " h" scaled_text_height
127
+ . " Center BackgroundTrans",
128
+ LABEL_OFF
129
+ )
130
+
131
+ indicator_x := rightmost_monitor_bounds.right_edge - scaled_width - scaled_right_offset
132
+ indicator_y := rightmost_monitor_bounds.top_edge + scaled_top_offset
133
+ indicator.Show("x" indicator_x " y" indicator_y " w" scaled_width " h" scaled_height " NoActivate")
134
+ }
135
+
136
+ render_indicator(is_currently_enabled) {
137
+ global indicator, indicator_label
138
+ indicator.BackColor := is_currently_enabled ? COLOR_ON_BACKGROUND : COLOR_OFF_BACKGROUND
139
+ indicator_label.Text := is_currently_enabled ? LABEL_ON : LABEL_OFF
140
+ WinRedraw("ahk_id " indicator.Hwnd)
141
+ }
142
+
143
+ send_continue_to_target() {
144
+ global target_window_title
145
+ if (!WinExist(target_window_title))
146
+ return
147
+ try {
148
+ WinActivate(target_window_title)
149
+ if (!WinWaitActive(target_window_title, , ACTIVATION_TIMEOUT_SECONDS))
150
+ return
151
+ } catch TargetError {
152
+ return
153
+ }
154
+ Sleep FOCUS_SETTLE_SLEEP_MS
155
+ SendText CONTINUE_PHRASE
156
+ Sleep POST_TEXT_SLEEP_MS
157
+ Send "{Enter}"
158
+ }
159
+
160
+ find_rightmost_monitor_bounds() {
161
+ monitor_count := MonitorGetCount()
162
+ rightmost_edge_px := INT32_MINIMUM_VALUE
163
+ rightmost_monitor_top_px := 0
164
+ loop monitor_count {
165
+ MonitorGet(A_Index, &monitor_left, &monitor_top, &monitor_right, &monitor_bottom)
166
+ if (monitor_right > rightmost_edge_px) {
167
+ rightmost_edge_px := monitor_right
168
+ rightmost_monitor_top_px := monitor_top
169
+ }
170
+ }
171
+ return { right_edge: rightmost_edge_px, top_edge: rightmost_monitor_top_px }
172
+ }
@@ -0,0 +1,2 @@
1
+ @echo off
2
+ start "" "C:\Program Files\AutoHotkey\v2\AutoHotkey64.exe" "%~dp0cursor-agents-continue.ahk" %*
@@ -0,0 +1,20 @@
1
+ """Evict cached ``config`` package bindings before local ``config`` imports.
2
+
3
+ Mirrors ``_evict_config_module`` in the repository root ``conftest.py``: stale
4
+ ``config`` or ``config.*`` entries from other packages must not satisfy
5
+ ``from config.…`` in these scripts after ``sys.path`` inserts the script directory.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib
11
+ import sys
12
+
13
+
14
+ def evict_cached_config_modules() -> None:
15
+ for each_cached_module_name in list(sys.modules):
16
+ is_config_root = each_cached_module_name == "config"
17
+ is_config_submodule = each_cached_module_name.startswith("config.")
18
+ if is_config_root or is_config_submodule:
19
+ sys.modules.pop(each_cached_module_name, None)
20
+ importlib.invalidate_caches()
@@ -0,0 +1,110 @@
1
+ """Fetch unaddressed Cursor Bugbot inline comments for the latest Bugbot review on a commit.
2
+
3
+ Uses ``fetch_bugbot_reviews`` to find the newest submitted Bugbot review whose ``commit_id`` matches the caller
4
+ ``current_head``, then returns only ``cursor[bot]`` inline comments whose ``pull_request_review_id`` matches that
5
+ review. This avoids misclassifying a PR when Bugbot posts more than one review on the same SHA: older inline threads
6
+ stay anchored to the earlier review id even when they share the same commit id.
7
+
8
+ Wraps the gh CLI invocation required by the gh-paginate rule for the comments list:
9
+ ``gh api`` on ``repos/{owner}/{repo}/pulls/{number}/comments`` with ``--paginate --slurp`` and external JSON handling.
10
+ """
11
+
12
+ import argparse
13
+ import json
14
+ import subprocess
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ if str(Path(__file__).resolve().parent) not in sys.path:
19
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
20
+
21
+ from evict_cached_config_modules import evict_cached_config_modules
22
+
23
+ evict_cached_config_modules()
24
+
25
+ from config.pr_converge_constants import (
26
+ CURSOR_BOT_LOGIN,
27
+ GH_INLINE_COMMENTS_PATH_TEMPLATE,
28
+ )
29
+ from fetch_bugbot_reviews import fetch_bugbot_reviews
30
+ from review_field_helpers import body_of, login_of
31
+
32
+
33
+ def fetch_bugbot_inline_comments(
34
+ *,
35
+ owner: str,
36
+ repo: str,
37
+ number: int,
38
+ current_head: str,
39
+ ) -> list[dict[str, object]]:
40
+ """Return cursor[bot] inline comments for the latest Bugbot review on ``current_head``.
41
+
42
+ Each entry contains comment_id, commit_id, path, line, and body.
43
+ """
44
+ all_bugbot_reviews = fetch_bugbot_reviews(owner=owner, repo=repo, number=number)
45
+ latest_bugbot_review_for_head = next(
46
+ (
47
+ each_review
48
+ for each_review in all_bugbot_reviews
49
+ if each_review.get("commit_id") == current_head
50
+ ),
51
+ None,
52
+ )
53
+ if latest_bugbot_review_for_head is None:
54
+ return []
55
+ target_pull_request_review_id = latest_bugbot_review_for_head["review_id"]
56
+ comments_endpoint = GH_INLINE_COMMENTS_PATH_TEMPLATE.format(
57
+ owner=owner, repo=repo, number=number
58
+ )
59
+ gh_command: list[str] = [
60
+ "gh",
61
+ "api",
62
+ comments_endpoint,
63
+ "--paginate",
64
+ "--slurp",
65
+ ]
66
+ completed = subprocess.run(
67
+ gh_command,
68
+ capture_output=True,
69
+ check=True,
70
+ text=True,
71
+ encoding="utf-8",
72
+ errors="replace",
73
+ )
74
+ pages: list[list[dict[str, object]]] = json.loads(completed.stdout)
75
+ all_flat_comments = [each_comment for each_page in pages for each_comment in each_page]
76
+ return [
77
+ {
78
+ "comment_id": each_comment["id"],
79
+ "commit_id": each_comment.get("commit_id"),
80
+ "path": each_comment.get("path"),
81
+ "line": each_comment.get("line"),
82
+ "body": body_of(each_comment),
83
+ }
84
+ for each_comment in all_flat_comments
85
+ if login_of(each_comment) == CURSOR_BOT_LOGIN
86
+ and each_comment.get("commit_id") == current_head
87
+ and each_comment.get("pull_request_review_id") == target_pull_request_review_id
88
+ ]
89
+
90
+
91
+ def main() -> int:
92
+ parser = argparse.ArgumentParser(description=__doc__)
93
+ parser.add_argument("--owner", required=True)
94
+ parser.add_argument("--repo", required=True)
95
+ parser.add_argument("--number", required=True, type=int)
96
+ parser.add_argument("--commit", required=True, dest="current_head")
97
+ parsed_arguments = parser.parse_args()
98
+ all_comments = fetch_bugbot_inline_comments(
99
+ owner=parsed_arguments.owner,
100
+ repo=parsed_arguments.repo,
101
+ number=parsed_arguments.number,
102
+ current_head=parsed_arguments.current_head,
103
+ )
104
+ json.dump(all_comments, sys.stdout)
105
+ sys.stdout.write("\n")
106
+ return 0
107
+
108
+
109
+ if __name__ == "__main__":
110
+ sys.exit(main())
@@ -0,0 +1,103 @@
1
+ """Fetch Cursor Bugbot reviews newest-first, classified as dirty or clean.
2
+
3
+ Wraps the gh CLI invocation required by the gh-paginate rule:
4
+ `gh api '...?per_page=100' --paginate --slurp` piped through external Python
5
+ JSON handling (instead of `gh --jq`, which runs per-page and breaks cross-page
6
+ operations like sort/reverse — see GitHub CLI #10459).
7
+ """
8
+
9
+ import argparse
10
+ import json
11
+ import re
12
+ import subprocess
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ if str(Path(__file__).resolve().parent) not in sys.path:
17
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
18
+
19
+ from evict_cached_config_modules import evict_cached_config_modules
20
+
21
+ evict_cached_config_modules()
22
+
23
+ from config.pr_converge_constants import (
24
+ BUGBOT_DIRTY_BODY_REGEX,
25
+ CURSOR_BOT_LOGIN,
26
+ GH_REVIEWS_PATH_TEMPLATE,
27
+ )
28
+ from review_field_helpers import body_of, login_of, submitted_at_of
29
+
30
+
31
+ def fetch_bugbot_reviews(
32
+ *,
33
+ owner: str,
34
+ repo: str,
35
+ number: int,
36
+ ) -> list[dict[str, object]]:
37
+ """Return Cursor Bugbot reviews newest-first, each with a clean/dirty classification.
38
+
39
+ Each entry contains review_id, commit_id, submitted_at, body, and classification.
40
+ """
41
+ reviews_endpoint = GH_REVIEWS_PATH_TEMPLATE.format(
42
+ owner=owner, repo=repo, number=number
43
+ )
44
+ gh_command: list[str] = [
45
+ "gh",
46
+ "api",
47
+ reviews_endpoint,
48
+ "--paginate",
49
+ "--slurp",
50
+ ]
51
+ completed = subprocess.run(
52
+ gh_command,
53
+ capture_output=True,
54
+ check=True,
55
+ text=True,
56
+ encoding="utf-8",
57
+ errors="replace",
58
+ )
59
+ pages: list[list[dict[str, object]]] = json.loads(completed.stdout)
60
+ all_flat_reviews = [each_review for each_page in pages for each_review in each_page]
61
+ all_bugbot_reviews = [
62
+ each_review
63
+ for each_review in all_flat_reviews
64
+ if login_of(each_review) == CURSOR_BOT_LOGIN
65
+ and each_review.get("submitted_at") is not None
66
+ and each_review.get("id") is not None
67
+ ]
68
+ all_bugbot_reviews.sort(
69
+ key=lambda each_review: submitted_at_of(each_review), reverse=True
70
+ )
71
+ dirty_pattern = re.compile(BUGBOT_DIRTY_BODY_REGEX)
72
+ return [
73
+ {
74
+ "review_id": each_review["id"],
75
+ "commit_id": each_review.get("commit_id"),
76
+ "submitted_at": each_review["submitted_at"],
77
+ "body": body_of(each_review),
78
+ "classification": (
79
+ "dirty"
80
+ if dirty_pattern.search(body_of(each_review))
81
+ else "clean"
82
+ ),
83
+ }
84
+ for each_review in all_bugbot_reviews
85
+ ]
86
+
87
+
88
+ def main() -> int:
89
+ parser = argparse.ArgumentParser(description=__doc__)
90
+ parser.add_argument("--owner", required=True)
91
+ parser.add_argument("--repo", required=True)
92
+ parser.add_argument("--number", required=True, type=int)
93
+ parsed_arguments = parser.parse_args()
94
+ all_reviews = fetch_bugbot_reviews(
95
+ owner=parsed_arguments.owner, repo=parsed_arguments.repo, number=parsed_arguments.number
96
+ )
97
+ json.dump(all_reviews, sys.stdout)
98
+ sys.stdout.write("\n")
99
+ return 0
100
+
101
+
102
+ if __name__ == "__main__":
103
+ sys.exit(main())
@@ -0,0 +1,112 @@
1
+ """Fetch unaddressed Copilot inline comments for the latest Copilot review on a commit.
2
+
3
+ Uses ``fetch_copilot_reviews`` to find the newest submitted Copilot review whose ``commit_id`` matches the caller
4
+ ``current_head``, then returns only ``copilot-pull-request-reviewer[bot]`` inline comments whose
5
+ ``pull_request_review_id`` matches that review. This avoids misclassifying a PR when Copilot posts more than one review
6
+ on the same SHA: older inline threads stay anchored to the earlier review id even when they share the same commit id.
7
+
8
+ Wraps the gh CLI invocation required by the gh-paginate rule for the comments list:
9
+ ``gh api`` on ``repos/{owner}/{repo}/pulls/{number}/comments`` with ``--paginate --slurp`` and external JSON handling.
10
+ """
11
+
12
+ import argparse
13
+ import json
14
+ import subprocess
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ if str(Path(__file__).resolve().parent) not in sys.path:
19
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
20
+
21
+ from evict_cached_config_modules import evict_cached_config_modules
22
+
23
+ evict_cached_config_modules()
24
+
25
+ from config.pr_converge_constants import (
26
+ COPILOT_REVIEWER_LOGIN,
27
+ GH_INLINE_COMMENTS_PATH_TEMPLATE,
28
+ )
29
+ from fetch_copilot_reviews import fetch_copilot_reviews
30
+ from review_field_helpers import body_of, login_of
31
+
32
+
33
+ def fetch_copilot_inline_comments(
34
+ *,
35
+ owner: str,
36
+ repo: str,
37
+ number: int,
38
+ current_head: str,
39
+ ) -> list[dict[str, object]]:
40
+ """Return Copilot inline comments for the latest Copilot review on ``current_head``.
41
+
42
+ Each entry contains comment_id, commit_id, path, line, and body.
43
+ """
44
+ all_copilot_reviews = fetch_copilot_reviews(owner=owner, repo=repo, number=number)
45
+ latest_copilot_review_for_head = next(
46
+ (
47
+ each_review
48
+ for each_review in all_copilot_reviews
49
+ if each_review.get("commit_id") == current_head
50
+ ),
51
+ None,
52
+ )
53
+ if latest_copilot_review_for_head is None:
54
+ return []
55
+ target_pull_request_review_id = latest_copilot_review_for_head["review_id"]
56
+ comments_endpoint = GH_INLINE_COMMENTS_PATH_TEMPLATE.format(
57
+ owner=owner, repo=repo, number=number
58
+ )
59
+ gh_command: list[str] = [
60
+ "gh",
61
+ "api",
62
+ comments_endpoint,
63
+ "--paginate",
64
+ "--slurp",
65
+ ]
66
+ completed = subprocess.run(
67
+ gh_command,
68
+ capture_output=True,
69
+ check=True,
70
+ text=True,
71
+ encoding="utf-8",
72
+ errors="replace",
73
+ )
74
+ pages: list[list[dict[str, object]]] = json.loads(completed.stdout)
75
+ all_flat_comments = [
76
+ each_comment for each_page in pages for each_comment in each_page
77
+ ]
78
+ return [
79
+ {
80
+ "comment_id": each_comment["id"],
81
+ "commit_id": each_comment.get("commit_id"),
82
+ "path": each_comment.get("path"),
83
+ "line": each_comment.get("line"),
84
+ "body": body_of(each_comment),
85
+ }
86
+ for each_comment in all_flat_comments
87
+ if login_of(each_comment) == COPILOT_REVIEWER_LOGIN
88
+ and each_comment.get("commit_id") == current_head
89
+ and each_comment.get("pull_request_review_id") == target_pull_request_review_id
90
+ ]
91
+
92
+
93
+ def main() -> int:
94
+ parser = argparse.ArgumentParser(description=__doc__)
95
+ parser.add_argument("--owner", required=True)
96
+ parser.add_argument("--repo", required=True)
97
+ parser.add_argument("--number", required=True, type=int)
98
+ parser.add_argument("--commit", required=True, dest="current_head")
99
+ parsed_arguments = parser.parse_args()
100
+ all_comments = fetch_copilot_inline_comments(
101
+ owner=parsed_arguments.owner,
102
+ repo=parsed_arguments.repo,
103
+ number=parsed_arguments.number,
104
+ current_head=parsed_arguments.current_head,
105
+ )
106
+ json.dump(all_comments, sys.stdout)
107
+ sys.stdout.write("\n")
108
+ return 0
109
+
110
+
111
+ if __name__ == "__main__":
112
+ sys.exit(main())
@@ -0,0 +1,121 @@
1
+ """Fetch GitHub Copilot reviewer reviews newest-first, classified as dirty or clean.
2
+
3
+ Wraps the gh CLI invocation required by the gh-paginate rule:
4
+ ``gh api '...?per_page=100' --paginate --slurp`` piped through external Python
5
+ JSON handling (instead of ``gh --jq``, which runs per-page and breaks cross-page
6
+ operations like sort/reverse - see GitHub CLI #10459).
7
+
8
+ Classification follows the review's ``state`` field:
9
+ - ``APPROVED`` -> ``"clean"``
10
+ - ``CHANGES_REQUESTED`` -> ``"dirty"``
11
+ - ``COMMENTED`` with non-empty body -> ``"dirty"`` (Copilot uses COMMENTED + body
12
+ to flag findings without a hard block)
13
+ - everything else -> ``"clean"`` (no actionable findings on PR)
14
+ """
15
+
16
+ import argparse
17
+ import json
18
+ import subprocess
19
+ import sys
20
+ from pathlib import Path
21
+
22
+ if str(Path(__file__).resolve().parent) not in sys.path:
23
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
24
+
25
+ from evict_cached_config_modules import evict_cached_config_modules
26
+
27
+ evict_cached_config_modules()
28
+
29
+ from config.pr_converge_constants import (
30
+ ALL_COPILOT_DIRTY_REVIEW_STATES,
31
+ COPILOT_CLEAN_REVIEW_STATE,
32
+ COPILOT_REVIEWER_LOGIN,
33
+ COPILOT_SOFT_DIRTY_REVIEW_STATE,
34
+ GH_REVIEWS_PATH_TEMPLATE,
35
+ )
36
+ from review_field_helpers import body_of, login_of, state_of, submitted_at_of
37
+
38
+
39
+ def fetch_copilot_reviews(
40
+ *,
41
+ owner: str,
42
+ repo: str,
43
+ number: int,
44
+ ) -> list[dict[str, object]]:
45
+ """Return Copilot reviews newest-first, each with a clean/dirty classification.
46
+
47
+ Each entry contains review_id, commit_id, submitted_at, state, body, and classification.
48
+ """
49
+ reviews_endpoint = GH_REVIEWS_PATH_TEMPLATE.format(
50
+ owner=owner, repo=repo, number=number
51
+ )
52
+ gh_command: list[str] = [
53
+ "gh",
54
+ "api",
55
+ reviews_endpoint,
56
+ "--paginate",
57
+ "--slurp",
58
+ ]
59
+ completed = subprocess.run(
60
+ gh_command,
61
+ capture_output=True,
62
+ check=True,
63
+ text=True,
64
+ encoding="utf-8",
65
+ errors="replace",
66
+ )
67
+ pages: list[list[dict[str, object]]] = json.loads(completed.stdout)
68
+ all_flat_reviews = [each_review for each_page in pages for each_review in each_page]
69
+ all_copilot_reviews = [
70
+ each_review
71
+ for each_review in all_flat_reviews
72
+ if login_of(each_review) == COPILOT_REVIEWER_LOGIN
73
+ and each_review.get("submitted_at") is not None
74
+ and each_review.get("id") is not None
75
+ ]
76
+ all_copilot_reviews.sort(
77
+ key=lambda each_review: submitted_at_of(each_review), reverse=True
78
+ )
79
+ return [
80
+ {
81
+ "review_id": each_review["id"],
82
+ "commit_id": each_review.get("commit_id"),
83
+ "submitted_at": each_review["submitted_at"],
84
+ "state": state_of(each_review),
85
+ "body": body_of(each_review),
86
+ "classification": _classify_review(each_review),
87
+ }
88
+ for each_review in all_copilot_reviews
89
+ ]
90
+
91
+
92
+ def _classify_review(field_by_key: dict[str, object]) -> str:
93
+ review_state = state_of(field_by_key)
94
+ if review_state == COPILOT_CLEAN_REVIEW_STATE:
95
+ return "clean"
96
+ if review_state not in ALL_COPILOT_DIRTY_REVIEW_STATES:
97
+ return "clean"
98
+ state_requires_body = review_state == COPILOT_SOFT_DIRTY_REVIEW_STATE
99
+ if state_requires_body and not body_of(field_by_key):
100
+ return "clean"
101
+ return "dirty"
102
+
103
+
104
+ def main() -> int:
105
+ parser = argparse.ArgumentParser(description=__doc__)
106
+ parser.add_argument("--owner", required=True)
107
+ parser.add_argument("--repo", required=True)
108
+ parser.add_argument("--number", required=True, type=int)
109
+ parsed_arguments = parser.parse_args()
110
+ all_reviews = fetch_copilot_reviews(
111
+ owner=parsed_arguments.owner,
112
+ repo=parsed_arguments.repo,
113
+ number=parsed_arguments.number,
114
+ )
115
+ json.dump(all_reviews, sys.stdout)
116
+ sys.stdout.write("\n")
117
+ return 0
118
+
119
+
120
+ if __name__ == "__main__":
121
+ sys.exit(main())
@@ -0,0 +1,54 @@
1
+ """Mark a draft PR as ready for review.
2
+
3
+ Convergence action invoked by pr-converge when both bugbot and bugteam are
4
+ clean against the same HEAD.
5
+ """
6
+
7
+ import argparse
8
+ import subprocess
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ if str(Path(__file__).resolve().parent) not in sys.path:
13
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
14
+
15
+ from evict_cached_config_modules import evict_cached_config_modules
16
+
17
+ evict_cached_config_modules()
18
+
19
+ from config.pr_converge_constants import GH_REPO_ARG_TEMPLATE
20
+
21
+
22
+ def mark_pr_ready(*, owner: str, repo: str, number: int) -> None:
23
+ """Run `gh pr ready <number> --repo <owner>/<repo>`."""
24
+ repo_arg = GH_REPO_ARG_TEMPLATE.format(owner=owner, repo=repo)
25
+ gh_command: list[str] = [
26
+ "gh",
27
+ "pr",
28
+ "ready",
29
+ str(number),
30
+ "--repo",
31
+ repo_arg,
32
+ ]
33
+ subprocess.run(
34
+ gh_command,
35
+ capture_output=True,
36
+ check=True,
37
+ text=True,
38
+ encoding="utf-8",
39
+ errors="replace",
40
+ )
41
+
42
+
43
+ def main() -> int:
44
+ parser = argparse.ArgumentParser(description=__doc__)
45
+ parser.add_argument("--owner", required=True)
46
+ parser.add_argument("--repo", required=True)
47
+ parser.add_argument("--number", required=True, type=int)
48
+ parsed_arguments = parser.parse_args()
49
+ mark_pr_ready(owner=parsed_arguments.owner, repo=parsed_arguments.repo, number=parsed_arguments.number)
50
+ return 0
51
+
52
+
53
+ if __name__ == "__main__":
54
+ sys.exit(main())