claude-dev-env 1.35.0 → 1.36.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 (115) hide show
  1. package/agents/clean-coder.md +109 -1
  2. package/bin/install.mjs +28 -8
  3. package/bin/install.test.mjs +9 -1
  4. package/docs/CODE_RULES.md +3 -0
  5. package/docs/agents-md-alignment-plan.md +123 -0
  6. package/hooks/blocking/code_rules_enforcer.py +451 -39
  7. package/hooks/blocking/es_exe_path_rewriter.py +10 -4
  8. package/hooks/blocking/test_code_rules_enforcer.py +182 -0
  9. package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +106 -0
  10. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +173 -0
  11. package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +191 -0
  12. package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +40 -0
  13. package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +291 -0
  14. package/hooks/blocking/test_code_rules_enforcer_loop_variable_naming.py +87 -3
  15. package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +49 -0
  16. package/hooks/blocking/test_code_rules_enforcer_sys_path_insert.py +157 -0
  17. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +244 -0
  18. package/hooks/blocking/test_es_exe_path_rewriter.py +81 -3
  19. package/hooks/blocking/test_windows_rmtree_blocker.py +120 -8
  20. package/hooks/blocking/windows_rmtree_blocker.py +23 -6
  21. package/hooks/config/banned_identifiers_constants.py +24 -0
  22. package/hooks/config/hardcoded_user_path_constants.py +12 -0
  23. package/hooks/config/hook_log_extractor_constants.py +1 -1
  24. package/hooks/config/pre_tool_use_stdin.py +48 -0
  25. package/hooks/config/setup_project_paths_constants.py +4 -0
  26. package/hooks/config/stuttering_check_config.py +14 -0
  27. package/hooks/config/stuttering_import_binding_constants.py +11 -0
  28. package/hooks/config/sys_path_insert_constants.py +4 -0
  29. package/hooks/config/test_banned_identifiers_constants.py +48 -0
  30. package/hooks/config/test_hardcoded_user_path_constants.py +78 -0
  31. package/hooks/config/test_hook_log_extractor_constants.py +3 -3
  32. package/hooks/config/test_pre_tool_use_stdin.py +80 -0
  33. package/hooks/config/unused_module_import_constants.py +7 -0
  34. package/hooks/config/windows_rmtree_blocker_constants.py +3 -0
  35. package/hooks/diagnostic/hook_log_stop_wrapper.py +7 -4
  36. package/hooks/git-hooks/config.py +3 -3
  37. package/hooks/git-hooks/test_gate_utils.py +10 -10
  38. package/hooks/mypy.ini +2 -0
  39. package/package.json +1 -1
  40. package/rules/gh-paginate.md +125 -0
  41. package/skills/bugteam/CONSTRAINTS.md +12 -6
  42. package/skills/bugteam/SKILL.md +364 -154
  43. package/skills/bugteam/SKILL_EVALS.md +25 -23
  44. package/skills/bugteam/reference/README.md +2 -0
  45. package/skills/bugteam/reference/audit-and-teammates.md +2 -2
  46. package/skills/bugteam/reference/teardown-publish-permissions.md +1 -1
  47. package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +113 -0
  48. package/skills/bugteam/reference/workflow-path-b-task-harness.md +48 -0
  49. package/skills/bugteam/scripts/reflow_skill_md.py +298 -0
  50. package/skills/bugteam/test_skill_additions.py +13 -4
  51. package/skills/bugteam/test_team_lifecycle.py +103 -0
  52. package/skills/findbugs/SKILL.md +3 -3
  53. package/skills/fixbugs/SKILL.md +4 -4
  54. package/skills/monitor-open-prs/SKILL.md +32 -2
  55. package/skills/monitor-open-prs/test_team_lifecycle.py +46 -0
  56. package/skills/pr-converge/SKILL.md +1206 -131
  57. package/skills/pr-converge/scripts/README.md +145 -0
  58. package/skills/pr-converge/scripts/caller-window-pid.ps1 +86 -0
  59. package/skills/pr-converge/scripts/check_pr_mergeability.py +79 -0
  60. package/skills/pr-converge/scripts/config/pr_converge_constants.py +65 -0
  61. package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +176 -0
  62. package/skills/pr-converge/scripts/cursor-agents-continue-caller.cmd +9 -0
  63. package/skills/pr-converge/scripts/cursor-agents-continue-stop-others.ps1 +16 -0
  64. package/skills/pr-converge/scripts/cursor-agents-continue.ahk +172 -0
  65. package/skills/pr-converge/scripts/cursor-agents-continue.cmd +2 -0
  66. package/skills/pr-converge/scripts/evict_cached_config_modules.py +20 -0
  67. package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +110 -0
  68. package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +103 -0
  69. package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +112 -0
  70. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +121 -0
  71. package/skills/pr-converge/scripts/mark_pr_ready.py +54 -0
  72. package/skills/pr-converge/scripts/open_followup_copilot_pr.py +136 -0
  73. package/skills/pr-converge/scripts/post-bugbot-run.helpers.ps1 +49 -0
  74. package/skills/pr-converge/scripts/post-bugbot-run.ps1 +33 -0
  75. package/skills/pr-converge/scripts/reflow_skill_md.py +288 -0
  76. package/skills/pr-converge/scripts/reply_to_inline_comment.py +84 -0
  77. package/skills/pr-converge/scripts/request_copilot_review.py +71 -0
  78. package/skills/pr-converge/scripts/resolve_pr_head.py +58 -0
  79. package/skills/pr-converge/scripts/review_field_helpers.py +43 -0
  80. package/skills/pr-converge/scripts/test_check_pr_mergeability.py +126 -0
  81. package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +22 -0
  82. package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +342 -0
  83. package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +220 -0
  84. package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +372 -0
  85. package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +280 -0
  86. package/skills/pr-converge/scripts/test_mark_pr_ready.py +69 -0
  87. package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +236 -0
  88. package/skills/pr-converge/scripts/test_post_bugbot_run.py +195 -0
  89. package/skills/pr-converge/scripts/test_reply_to_inline_comment.py +159 -0
  90. package/skills/pr-converge/scripts/test_request_copilot_review.py +101 -0
  91. package/skills/pr-converge/scripts/test_resolve_pr_head.py +79 -0
  92. package/skills/pr-converge/scripts/test_review_field_helpers.py +80 -0
  93. package/skills/pr-converge/scripts/test_trigger_bugbot.py +139 -0
  94. package/skills/pr-converge/scripts/test_view_pr_context.py +111 -0
  95. package/skills/pr-converge/scripts/trigger_bugbot.py +77 -0
  96. package/skills/pr-converge/scripts/view_pr_context.py +47 -0
  97. package/skills/pr-converge/test_team_lifecycle.py +56 -0
  98. package/skills/pr-converge/workflows/ahk-auto-continue-loop.md +108 -0
  99. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +37 -0
  100. package/skills/qbug/SKILL.md +4 -4
  101. package/skills/qbug/test_qbug_skill_post_fix_audit.py +2 -2
  102. package/skills/resume-review/SKILL.md +261 -0
  103. package/skills/bugteam/scripts/README.md +0 -58
  104. package/skills/bugteam/scripts/_claude_permissions_common.py +0 -219
  105. package/skills/bugteam/scripts/bugteam_code_rules_gate.py +0 -633
  106. package/skills/bugteam/scripts/bugteam_fix_hookspath.py +0 -260
  107. package/skills/bugteam/scripts/bugteam_preflight.py +0 -201
  108. package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +0 -17
  109. package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -109
  110. package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -135
  111. package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +0 -271
  112. package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -267
  113. package/skills/bugteam/scripts/test_bugteam_preflight.py +0 -189
  114. package/skills/bugteam/scripts/test_claude_permissions_common.py +0 -44
  115. /package/skills/{bugteam → pr-converge}/scripts/config/__init__.py +0 -0
@@ -0,0 +1,145 @@
1
+ # pr-converge scripts
2
+
3
+ Thin Python wrappers around the gh CLI calls the skill makes per tick. Centralizing them lets the skill body reference one script path per action and keeps the gh-paginate and gh-body-file rules enforced in one place.
4
+
5
+ Scripts that target a specific repository are invoked as `python "${CLAUDE_SKILL_DIR}/scripts/<name>.py" --owner OWNER --repo REPO --number NUMBER ...`. `view_pr_context.py` relies on `gh`'s default repository context and takes no `--owner` / `--repo` / `--number` flags. Scripts return non-zero on subprocess failure and surface gh's stderr through `subprocess.CalledProcessError`.
6
+
7
+ ## Scripts
8
+
9
+ ### `view_pr_context.py`
10
+
11
+ Returns the per-tick PR context as JSON.
12
+
13
+ ```bash
14
+ python "${CLAUDE_SKILL_DIR}/scripts/view_pr_context.py"
15
+ ```
16
+
17
+ Output: `{"number", "url", "headRefOid", "baseRefName", "headRefName", "isDraft"}`. Wraps `gh pr view --json number,url,headRefOid,baseRefName,headRefName,isDraft`.
18
+
19
+ ### `fetch_bugbot_reviews.py`
20
+
21
+ Fetches every Cursor Bugbot review on the PR newest-first, classified per body content.
22
+
23
+ ```bash
24
+ python "${CLAUDE_SKILL_DIR}/scripts/fetch_bugbot_reviews.py" \
25
+ --owner <OWNER> --repo <REPO> --number <NUMBER>
26
+ ```
27
+
28
+ Output: JSON array of `{review_id, commit_id, submitted_at, body, classification}` where `classification` is `"dirty"` (body matches `Cursor Bugbot has reviewed your changes and found <N> potential issue`) or `"clean"`. Uses `--paginate --slurp` and flattens pages in Python — required by `../../../rules/gh-paginate.md` because `gh --paginate --jq` runs the filter per-page (gh CLI #10459).
29
+
30
+ ### `fetch_bugbot_inline_comments.py`
31
+
32
+ Fetches unaddressed Cursor Bugbot inline comments for the **newest submitted Bugbot review** on the requested ``--commit`` SHA (matches ``pull_request_review_id`` to the review returned by ``fetch_bugbot_reviews.py`` so stale inline threads from an older Bugbot review on the same SHA are ignored).
33
+
34
+ ```bash
35
+ python "${CLAUDE_SKILL_DIR}/scripts/fetch_bugbot_inline_comments.py" \
36
+ --owner <OWNER> --repo <REPO> --number <NUMBER> --commit <CURRENT_HEAD>
37
+ ```
38
+
39
+ Output: JSON array of `{comment_id, commit_id, path, line, body}`. Uses the same `--paginate --slurp` pattern as `fetch_bugbot_reviews.py`.
40
+
41
+ ### `resolve_pr_head.py`
42
+
43
+ Returns the current HEAD SHA of the PR. Wraps the single-object endpoint `repos/<owner>/<repo>/pulls/<number>` which is not paginated, so `gh`'s built-in `--jq .head.sha` is safe here (see "Single-object endpoints" in `../../../rules/gh-paginate.md`).
44
+
45
+ ```bash
46
+ python "${CLAUDE_SKILL_DIR}/scripts/resolve_pr_head.py" \
47
+ --owner <OWNER> --repo <REPO> --number <NUMBER>
48
+ ```
49
+
50
+ Output: the SHA on stdout, trailing newline.
51
+
52
+ ### `trigger_bugbot.py`
53
+
54
+ Posts the literal `bugbot run` re-trigger comment via `gh pr comment --body-file` (per `../../../rules/gh-body-file.md` — passing the body inline can corrupt backticks). Writes and removes the temp body file internally.
55
+
56
+ ```bash
57
+ python "${CLAUDE_SKILL_DIR}/scripts/trigger_bugbot.py" \
58
+ --owner <OWNER> --repo <REPO> --number <NUMBER>
59
+ ```
60
+
61
+ Output: the comment URL from gh on stdout.
62
+
63
+ ### `mark_pr_ready.py`
64
+
65
+ Marks a draft PR as ready for review. Convergence action invoked when both bugbot and bugteam are clean against the same HEAD.
66
+
67
+ ```bash
68
+ python "${CLAUDE_SKILL_DIR}/scripts/mark_pr_ready.py" \
69
+ --owner <OWNER> --repo <REPO> --number <NUMBER>
70
+ ```
71
+
72
+ ### `reply_to_inline_comment.py`
73
+
74
+ Posts an inline reply to a PR review comment. Reply body is sourced from a caller-supplied file via `gh api ... -F body=@<path>` (per `../../../rules/gh-body-file.md`).
75
+
76
+ ```bash
77
+ python "${CLAUDE_SKILL_DIR}/scripts/reply_to_inline_comment.py" \
78
+ --owner <OWNER> --repo <REPO> --number <NUMBER> \
79
+ --comment-id <COMMENT_ID> --body-file <PATH_TO_REPLY_MD>
80
+ ```
81
+
82
+ Output: the new reply id from gh's JSON response, on stdout.
83
+
84
+ ### `check_pr_mergeability.py`
85
+
86
+ Returns the mergeability state of the current PR as JSON. Wraps `gh pr view --json mergeable,mergeStateStatus,headRefOid` (single-object endpoint — no pagination needed). Used by the convergence gate to detect base-branch conflicts (`mergeStateStatus == "DIRTY"` / `mergeable == "CONFLICTING"`) before flipping the PR ready.
87
+
88
+ ```bash
89
+ python "${CLAUDE_SKILL_DIR}/scripts/check_pr_mergeability.py"
90
+ ```
91
+
92
+ Output: `{"mergeable", "mergeStateStatus", "headRefOid"}`.
93
+
94
+ ### `fetch_copilot_reviews.py`
95
+
96
+ Fetches every Copilot reviewer (`copilot-pull-request-reviewer[bot]`) review on the PR newest-first, classified per the review's `state` field.
97
+
98
+ ```bash
99
+ python "${CLAUDE_SKILL_DIR}/scripts/fetch_copilot_reviews.py" \
100
+ --owner <OWNER> --repo <REPO> --number <NUMBER>
101
+ ```
102
+
103
+ Output: JSON array of `{review_id, commit_id, submitted_at, state, body, classification}` where `classification` is `"clean"` for `state == "APPROVED"`, `"dirty"` for `state == "CHANGES_REQUESTED"`, and `"dirty"` for `state == "COMMENTED"` with a non-empty body. Uses `--paginate --slurp` and flattens pages in Python — required by `../../../rules/gh-paginate.md`.
104
+
105
+ ### `fetch_copilot_inline_comments.py`
106
+
107
+ Fetches unaddressed Copilot inline comments for the **newest submitted Copilot review** on the requested ``--commit`` SHA (matches ``pull_request_review_id`` to the review returned by ``fetch_copilot_reviews.py`` so stale inline threads from an older Copilot review on the same SHA are ignored).
108
+
109
+ ```bash
110
+ python "${CLAUDE_SKILL_DIR}/scripts/fetch_copilot_inline_comments.py" \
111
+ --owner <OWNER> --repo <REPO> --number <NUMBER> --commit <CURRENT_HEAD>
112
+ ```
113
+
114
+ Output: JSON array of `{comment_id, commit_id, path, line, body}`. Uses the same `--paginate --slurp` pattern as `fetch_copilot_reviews.py`.
115
+
116
+ ### `request_copilot_review.py`
117
+
118
+ Requests a Copilot review on the current PR via `gh api -X POST repos/{owner}/{repo}/pulls/{number}/requested_reviewers -f 'reviewers[]=copilot-pull-request-reviewer[bot]'`. The `[bot]` suffix is load-bearing per `../../copilot-review/SKILL.md` — `Copilot`, `copilot`, and `github-copilot` all silently no-op.
119
+
120
+ ```bash
121
+ python "${CLAUDE_SKILL_DIR}/scripts/request_copilot_review.py" \
122
+ --owner <OWNER> --repo <REPO> --number <NUMBER>
123
+ ```
124
+
125
+ Output: none on success (gh's stdout is suppressed); `subprocess.CalledProcessError` on failure.
126
+
127
+ ### `open_followup_copilot_pr.py`
128
+
129
+ Opens a follow-up draft PR addressing Copilot findings from the parent PR. Subprocess sequence: resolve parent's `baseRefName` → `git fetch origin <head_sha>` → `git switch -c <new_branch> <head_sha>` → `git push -u origin <new_branch>` → `gh pr create --draft --base <base_ref> --head <new_branch> --title <...> --body-file <findings_file>` (per `../../../rules/gh-body-file.md`). Branch name format: `chore/copilot-followup-{parent_number}-{short_sha}`.
130
+
131
+ ```bash
132
+ python "${CLAUDE_SKILL_DIR}/scripts/open_followup_copilot_pr.py" \
133
+ --owner <OWNER> --repo <REPO> --parent-number <PARENT_NUMBER> \
134
+ --head <HEAD_SHA> --findings-file <PATH_TO_FINDINGS_MD>
135
+ ```
136
+
137
+ Output: the new PR URL on stdout, trimmed.
138
+
139
+ ## Tests
140
+
141
+ Each script has a sibling `test_<name>.py`. Run them all with:
142
+
143
+ ```bash
144
+ python -m pytest packages/claude-dev-env/skills/pr-converge/scripts/ -v
145
+ ```
@@ -0,0 +1,86 @@
1
+ [CmdletBinding()]
2
+ param()
3
+
4
+ $DESKTOP_SHELL_TERMINATOR_NAME = 'explorer'
5
+ $MAXIMUM_PARENT_WALK_DEPTH = 24
6
+
7
+ if (-not ('CallerWindowPid.Win32ForegroundWindowQuery' -as [type])) {
8
+ Add-Type -Namespace 'CallerWindowPid' -Name 'Win32ForegroundWindowQuery' -MemberDefinition @'
9
+ [System.Runtime.InteropServices.DllImport("user32.dll")]
10
+ public static extern System.IntPtr GetForegroundWindow();
11
+ [System.Runtime.InteropServices.DllImport("user32.dll")]
12
+ public static extern uint GetWindowThreadProcessId(System.IntPtr hWnd, out uint processId);
13
+ '@
14
+ }
15
+
16
+ function Get-ParentProcessId {
17
+ param([int]$ChildProcessId)
18
+ try {
19
+ return (Get-CimInstance -ClassName Win32_Process -Filter "ProcessId=$ChildProcessId" -ErrorAction Stop).ParentProcessId
20
+ } catch {
21
+ return 0
22
+ }
23
+ }
24
+
25
+ function Resolve-NearestGuiAncestorPid {
26
+ param([int]$StartingProcessId)
27
+
28
+ $visited_process_ids = @{}
29
+ $current_process_id = $StartingProcessId
30
+ $walk_depth = 0
31
+
32
+ while ($walk_depth -lt $MAXIMUM_PARENT_WALK_DEPTH) {
33
+ $walk_depth++
34
+ if ($visited_process_ids.ContainsKey($current_process_id)) {
35
+ return $null
36
+ }
37
+ $visited_process_ids[$current_process_id] = $true
38
+
39
+ $parent_process_id = Get-ParentProcessId -ChildProcessId $current_process_id
40
+ if (-not $parent_process_id -or $parent_process_id -eq 0) {
41
+ return $null
42
+ }
43
+
44
+ try {
45
+ $parent_process = Get-Process -Id $parent_process_id -ErrorAction Stop
46
+ } catch {
47
+ return $null
48
+ }
49
+
50
+ if ($parent_process.ProcessName -eq $DESKTOP_SHELL_TERMINATOR_NAME) {
51
+ return $null
52
+ }
53
+
54
+ if ($parent_process.MainWindowHandle -ne [IntPtr]::Zero) {
55
+ return $parent_process.Id
56
+ }
57
+
58
+ $current_process_id = $parent_process_id
59
+ }
60
+
61
+ return $null
62
+ }
63
+
64
+ function Resolve-ForegroundWindowPid {
65
+ $foreground_window_handle = [CallerWindowPid.Win32ForegroundWindowQuery]::GetForegroundWindow()
66
+ if ($foreground_window_handle -eq [IntPtr]::Zero) {
67
+ return $null
68
+ }
69
+ $foreground_window_pid = [uint32]0
70
+ [void][CallerWindowPid.Win32ForegroundWindowQuery]::GetWindowThreadProcessId($foreground_window_handle, [ref]$foreground_window_pid)
71
+ if ($foreground_window_pid -eq 0) {
72
+ return $null
73
+ }
74
+ return [int]$foreground_window_pid
75
+ }
76
+
77
+ $resolved_pid = Resolve-NearestGuiAncestorPid -StartingProcessId $PID
78
+ if ($null -eq $resolved_pid) {
79
+ $resolved_pid = Resolve-ForegroundWindowPid
80
+ }
81
+ if ($null -eq $resolved_pid) {
82
+ Write-Error "Could not resolve a GUI process from PID $PID or the foreground window."
83
+ exit 1
84
+ }
85
+
86
+ Write-Output $resolved_pid
@@ -0,0 +1,79 @@
1
+ """Resolve the per-tick mergeability state of an explicitly-targeted PR.
2
+
3
+ Wraps ``gh pr view <number> --repo <owner>/<repo> --json mergeable,mergeStateStatus,headRefOid``
4
+ so the skill body emits one script invocation. Single-object endpoint - no
5
+ pagination. Explicit ``--owner``/``--repo``/``--number`` targeting matches every
6
+ sibling convergence-gate script (``fetch_*_reviews.py``,
7
+ ``fetch_*_inline_comments.py``, ``request_copilot_review.py``,
8
+ ``mark_pr_ready.py``); under multi-PR orchestration or after
9
+ ``open_followup_copilot_pr.py`` switches the checkout, the gate is guaranteed
10
+ to query the intended PR rather than whichever PR the current git context
11
+ points at.
12
+
13
+ The returned dict gates pr-converge's mark-ready step against PRs whose base
14
+ branch state is DIRTY (conflicts) or otherwise non-CLEAN.
15
+ """
16
+
17
+ import argparse
18
+ import json
19
+ import subprocess
20
+ import sys
21
+ from pathlib import Path
22
+
23
+ if str(Path(__file__).resolve().parent) not in sys.path:
24
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
25
+
26
+ from evict_cached_config_modules import evict_cached_config_modules
27
+
28
+ evict_cached_config_modules()
29
+
30
+ from config.pr_converge_constants import GH_REPO_ARG_TEMPLATE, MERGEABILITY_FIELDS
31
+
32
+
33
+ def check_pr_mergeability(
34
+ *,
35
+ owner: str,
36
+ repo: str,
37
+ number: int,
38
+ ) -> dict[str, object]:
39
+ """Return ``{mergeable, mergeStateStatus, headRefOid}`` from ``gh pr view`` for the targeted PR."""
40
+ repo_arg = GH_REPO_ARG_TEMPLATE.format(owner=owner, repo=repo)
41
+ gh_command: list[str] = [
42
+ "gh",
43
+ "pr",
44
+ "view",
45
+ str(number),
46
+ "--repo",
47
+ repo_arg,
48
+ "--json",
49
+ MERGEABILITY_FIELDS,
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
+ return json.loads(completed.stdout)
60
+
61
+
62
+ def main() -> int:
63
+ parser = argparse.ArgumentParser(description=__doc__)
64
+ parser.add_argument("--owner", required=True)
65
+ parser.add_argument("--repo", required=True)
66
+ parser.add_argument("--number", required=True, type=int)
67
+ parsed_arguments = parser.parse_args()
68
+ mergeability_state = check_pr_mergeability(
69
+ owner=parsed_arguments.owner,
70
+ repo=parsed_arguments.repo,
71
+ number=parsed_arguments.number,
72
+ )
73
+ json.dump(mergeability_state, sys.stdout)
74
+ sys.stdout.write("\n")
75
+ return 0
76
+
77
+
78
+ if __name__ == "__main__":
79
+ sys.exit(main())
@@ -0,0 +1,65 @@
1
+ """Configuration constants for the pr-converge skill scripts.
2
+
3
+ Path templates accept ``str.format(**kwargs)`` substitution; bugbot strings
4
+ match the literal phrasing the Cursor Bugbot reviewer emits.
5
+ """
6
+
7
+ CURSOR_BOT_LOGIN: str = "cursor[bot]"
8
+
9
+ COPILOT_REVIEWER_LOGIN: str = "copilot-pull-request-reviewer[bot]"
10
+
11
+ COPILOT_REVIEWER_REQUEST_ID: str = COPILOT_REVIEWER_LOGIN
12
+
13
+ COPILOT_CLEAN_REVIEW_STATE: str = "APPROVED"
14
+
15
+ ALL_COPILOT_DIRTY_REVIEW_STATES: tuple[str, ...] = ("CHANGES_REQUESTED", "COMMENTED")
16
+
17
+ COPILOT_SOFT_DIRTY_REVIEW_STATE: str = "COMMENTED"
18
+
19
+ BUGBOT_DIRTY_BODY_REGEX: str = (
20
+ r"Cursor Bugbot has reviewed your changes and found \d+ potential issue"
21
+ )
22
+
23
+ GH_REVIEWS_PATH_TEMPLATE: str = (
24
+ "repos/{owner}/{repo}/pulls/{number}/reviews?per_page=100"
25
+ )
26
+
27
+ GH_INLINE_COMMENTS_PATH_TEMPLATE: str = (
28
+ "repos/{owner}/{repo}/pulls/{number}/comments?per_page=100"
29
+ )
30
+
31
+ GH_PR_OBJECT_PATH_TEMPLATE: str = "repos/{owner}/{repo}/pulls/{number}"
32
+
33
+ GH_INLINE_COMMENT_REPLY_PATH_TEMPLATE: str = (
34
+ "repos/{owner}/{repo}/pulls/{number}/comments/{comment_id}/replies"
35
+ )
36
+
37
+ GH_REQUESTED_REVIEWERS_PATH_TEMPLATE: str = (
38
+ "repos/{owner}/{repo}/pulls/{number}/requested_reviewers"
39
+ )
40
+
41
+ GH_REQUESTED_REVIEWERS_FIELD_TEMPLATE: str = "reviewers[]={reviewer_id}"
42
+
43
+ BUGBOT_RUN_TRIGGER_PHRASE: str = "bugbot run\n"
44
+
45
+ BUGBOT_RUN_TEMPFILE_SUFFIX: str = ".md"
46
+
47
+ BUGBOT_RUN_TEMPFILE_PREFIX: str = "pr-converge-bugbot-run-"
48
+
49
+ PR_CONTEXT_FIELDS: str = "number,url,headRefOid,baseRefName,headRefName,isDraft"
50
+
51
+ MERGEABILITY_FIELDS: str = "mergeable,mergeStateStatus,headRefOid"
52
+
53
+ GH_FIELD_BODY_AT_PREFIX: str = "body=@"
54
+
55
+ GH_REPO_ARG_TEMPLATE: str = "{owner}/{repo}"
56
+
57
+ PR_BASE_REF_FIELDS: str = "baseRefName"
58
+
59
+ COPILOT_FOLLOWUP_BRANCH_TEMPLATE: str = "chore/copilot-followup-{parent_number}-{short_sha}"
60
+
61
+ COPILOT_FOLLOWUP_PR_TITLE_TEMPLATE: str = (
62
+ "chore: address Copilot findings from PR #{parent_number}"
63
+ )
64
+
65
+ COPILOT_FOLLOWUP_SHORT_SHA_LENGTH: int = 8
@@ -0,0 +1,176 @@
1
+ """Tests for pr_converge_constants.
2
+
3
+ Verifies that path templates accept the documented format substitutions
4
+ (owner, repo, number, comment_id) and the bugbot regex matches dirty review
5
+ bodies but not clean ones.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib.util
11
+ import re
12
+ from pathlib import Path
13
+ from types import ModuleType
14
+
15
+
16
+ def _load_module() -> ModuleType:
17
+ module_path = Path(__file__).parent / "pr_converge_constants.py"
18
+ spec = importlib.util.spec_from_file_location("pr_converge_constants", module_path)
19
+ assert spec is not None
20
+ assert spec.loader is not None
21
+ module = importlib.util.module_from_spec(spec)
22
+ spec.loader.exec_module(module)
23
+ return module
24
+
25
+
26
+ pr_converge_constants_module = _load_module()
27
+
28
+
29
+ def test_reviews_path_template_accepts_owner_repo_number() -> None:
30
+ rendered = pr_converge_constants_module.GH_REVIEWS_PATH_TEMPLATE.format(
31
+ owner="acme", repo="widget", number=42
32
+ )
33
+ assert rendered == "repos/acme/widget/pulls/42/reviews?per_page=100"
34
+
35
+
36
+ def test_inline_comments_path_template_accepts_owner_repo_number() -> None:
37
+ rendered = pr_converge_constants_module.GH_INLINE_COMMENTS_PATH_TEMPLATE.format(
38
+ owner="acme", repo="widget", number=42
39
+ )
40
+ assert rendered == "repos/acme/widget/pulls/42/comments?per_page=100"
41
+
42
+
43
+ def test_pr_object_path_template_accepts_owner_repo_number() -> None:
44
+ rendered = pr_converge_constants_module.GH_PR_OBJECT_PATH_TEMPLATE.format(
45
+ owner="acme", repo="widget", number=42
46
+ )
47
+ assert rendered == "repos/acme/widget/pulls/42"
48
+
49
+
50
+ def test_inline_comment_reply_path_template_accepts_all_substitutions() -> None:
51
+ rendered = (
52
+ pr_converge_constants_module.GH_INLINE_COMMENT_REPLY_PATH_TEMPLATE.format(
53
+ owner="acme", repo="widget", number=42, comment_id=12345
54
+ )
55
+ )
56
+ assert rendered == "repos/acme/widget/pulls/42/comments/12345/replies"
57
+
58
+
59
+ def test_bugbot_dirty_body_regex_distinguishes_findings_from_clean_bodies() -> None:
60
+ dirty_body = "Cursor Bugbot has reviewed your changes and found 3 potential issues."
61
+ clean_body = "Bugbot reviewed your changes and found no new issues!"
62
+ compiled_pattern = re.compile(pr_converge_constants_module.BUGBOT_DIRTY_BODY_REGEX)
63
+ dirty_match = compiled_pattern.search(dirty_body)
64
+ assert dirty_match is not None
65
+ assert "found 3 potential issue" in dirty_match.group(0)
66
+ assert compiled_pattern.search(clean_body) is None
67
+
68
+
69
+ def test_cursor_bot_login_matches_github_login_string() -> None:
70
+ assert pr_converge_constants_module.CURSOR_BOT_LOGIN == "cursor[bot]"
71
+
72
+
73
+ def test_bugbot_run_trigger_phrase_ends_with_newline() -> None:
74
+ assert pr_converge_constants_module.BUGBOT_RUN_TRIGGER_PHRASE == "bugbot run\n"
75
+
76
+
77
+ def test_pr_context_fields_lists_documented_field_names() -> None:
78
+ fields_arg = pr_converge_constants_module.PR_CONTEXT_FIELDS
79
+ for required_field in (
80
+ "number",
81
+ "url",
82
+ "headRefOid",
83
+ "baseRefName",
84
+ "headRefName",
85
+ "isDraft",
86
+ ):
87
+ assert required_field in fields_arg
88
+
89
+
90
+ def test_gh_field_body_at_prefix_matches_gh_field_from_file_form() -> None:
91
+ assert pr_converge_constants_module.GH_FIELD_BODY_AT_PREFIX == "body=@"
92
+
93
+
94
+ def test_gh_repo_arg_template_renders_owner_slash_repo() -> None:
95
+ rendered = pr_converge_constants_module.GH_REPO_ARG_TEMPLATE.format(
96
+ owner="acme", repo="widget"
97
+ )
98
+ assert rendered == "acme/widget"
99
+
100
+
101
+ def test_copilot_reviewer_login_carries_bot_suffix() -> None:
102
+ assert (
103
+ pr_converge_constants_module.COPILOT_REVIEWER_LOGIN
104
+ == "copilot-pull-request-reviewer[bot]"
105
+ )
106
+
107
+
108
+ def test_copilot_reviewer_request_id_reuses_login_constant() -> None:
109
+ request_id = pr_converge_constants_module.COPILOT_REVIEWER_REQUEST_ID
110
+ login = pr_converge_constants_module.COPILOT_REVIEWER_LOGIN
111
+ assert request_id == login
112
+ assert request_id is login
113
+
114
+
115
+ def test_copilot_clean_review_state_is_approved() -> None:
116
+ assert pr_converge_constants_module.COPILOT_CLEAN_REVIEW_STATE == "APPROVED"
117
+
118
+
119
+ def test_copilot_dirty_review_states_lists_changes_requested_and_commented() -> None:
120
+ dirty_states = pr_converge_constants_module.ALL_COPILOT_DIRTY_REVIEW_STATES
121
+ assert "CHANGES_REQUESTED" in dirty_states
122
+ assert "COMMENTED" in dirty_states
123
+
124
+
125
+ def test_copilot_soft_dirty_review_state_is_commented() -> None:
126
+ assert pr_converge_constants_module.COPILOT_SOFT_DIRTY_REVIEW_STATE == "COMMENTED"
127
+
128
+
129
+ def test_mergeability_fields_lists_required_field_names() -> None:
130
+ fields_arg = pr_converge_constants_module.MERGEABILITY_FIELDS
131
+ for required_field in ("mergeable", "mergeStateStatus", "headRefOid"):
132
+ assert required_field in fields_arg
133
+
134
+
135
+ def test_requested_reviewers_path_template_accepts_owner_repo_number() -> None:
136
+ rendered = (
137
+ pr_converge_constants_module.GH_REQUESTED_REVIEWERS_PATH_TEMPLATE.format(
138
+ owner="acme", repo="widget", number=42
139
+ )
140
+ )
141
+ assert rendered == "repos/acme/widget/pulls/42/requested_reviewers"
142
+
143
+
144
+ def test_requested_reviewers_field_template_accepts_reviewer_id() -> None:
145
+ rendered = (
146
+ pr_converge_constants_module.GH_REQUESTED_REVIEWERS_FIELD_TEMPLATE.format(
147
+ reviewer_id="copilot-pull-request-reviewer[bot]"
148
+ )
149
+ )
150
+ assert rendered == "reviewers[]=copilot-pull-request-reviewer[bot]"
151
+
152
+
153
+ def test_pr_base_ref_fields_lists_base_ref_name() -> None:
154
+ assert "baseRefName" in pr_converge_constants_module.PR_BASE_REF_FIELDS
155
+
156
+
157
+ def test_copilot_followup_branch_template_renders_parent_number_and_sha() -> None:
158
+ rendered = (
159
+ pr_converge_constants_module.COPILOT_FOLLOWUP_BRANCH_TEMPLATE.format(
160
+ parent_number=312, short_sha="abc12345"
161
+ )
162
+ )
163
+ assert rendered == "chore/copilot-followup-312-abc12345"
164
+
165
+
166
+ def test_copilot_followup_pr_title_template_renders_parent_number() -> None:
167
+ rendered = (
168
+ pr_converge_constants_module.COPILOT_FOLLOWUP_PR_TITLE_TEMPLATE.format(
169
+ parent_number=312
170
+ )
171
+ )
172
+ assert rendered == "chore: address Copilot findings from PR #312"
173
+
174
+
175
+ def test_copilot_followup_short_sha_length_is_eight() -> None:
176
+ assert pr_converge_constants_module.COPILOT_FOLLOWUP_SHORT_SHA_LENGTH == 8
@@ -0,0 +1,9 @@
1
+ @echo off
2
+ setlocal
3
+ for /f "delims=" %%P in ('pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0caller-window-pid.ps1"') do set CALLER_PID=%%P
4
+ if "%CALLER_PID%"=="" (
5
+ echo [cursor-agents-continue-caller] Failed to resolve caller PID.
6
+ exit /b 1
7
+ )
8
+ call "%~dp0cursor-agents-continue.cmd" %CALLER_PID% --start-on
9
+ endlocal
@@ -0,0 +1,16 @@
1
+ [CmdletBinding()]
2
+ param(
3
+ [Parameter(HelpMessage = 'Process id to keep (0 = kill every matching AutoHotkey instance).')]
4
+ [int] $KeepProcessId = 0
5
+ )
6
+
7
+ $scriptMarker = 'cursor-agents-continue.ahk'
8
+ $all_processes = Get-CimInstance Win32_Process -Filter "Name='AutoHotkey64.exe'" |
9
+ Where-Object { $_.CommandLine -like "*$scriptMarker*" }
10
+
11
+ foreach ($eachProcess in $all_processes) {
12
+ if ($KeepProcessId -ne 0 -and $eachProcess.ProcessId -eq $KeepProcessId) {
13
+ continue
14
+ }
15
+ Stop-Process -Id $eachProcess.ProcessId -Force -ErrorAction SilentlyContinue
16
+ }