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.
- package/agents/clean-coder.md +109 -1
- package/agents/docs-agent.md +1 -1
- package/agents/project-docs-analyzer.md +0 -1
- package/agents/skill-to-agent-converter.md +0 -1
- package/bin/install.mjs +28 -8
- package/bin/install.test.mjs +9 -1
- package/commands/initialize.md +0 -1
- package/commands/readability-review.md +4 -4
- package/commands/review-plan.md +2 -4
- package/commands/stubcheck.md +1 -2
- package/docs/CODE_RULES.md +3 -0
- package/docs/agents-md-alignment-plan.md +123 -0
- package/hooks/blocking/code_rules_enforcer.py +686 -60
- package/hooks/blocking/es_exe_path_rewriter.py +10 -4
- package/hooks/blocking/test_code_rules_enforcer.py +273 -39
- package/hooks/blocking/test_code_rules_enforcer_annotations.py +97 -0
- package/hooks/blocking/test_code_rules_enforcer_banned_identifier.py +106 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +173 -0
- package/hooks/blocking/test_code_rules_enforcer_collection_prefix.py +328 -0
- package/hooks/blocking/test_code_rules_enforcer_config_path.py +0 -20
- package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +33 -11
- package/hooks/blocking/test_code_rules_enforcer_existence_checks.py +0 -18
- package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +291 -0
- package/hooks/blocking/test_code_rules_enforcer_inline_literal_collections.py +155 -0
- package/hooks/blocking/test_code_rules_enforcer_loop_variable_naming.py +194 -0
- package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +49 -13
- package/hooks/blocking/test_code_rules_enforcer_skip_decorators.py +0 -26
- package/hooks/blocking/test_code_rules_enforcer_string_magic.py +234 -0
- package/hooks/blocking/test_code_rules_enforcer_sys_path_insert.py +157 -0
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +244 -0
- package/hooks/blocking/test_es_exe_path_rewriter.py +81 -3
- package/hooks/blocking/test_windows_rmtree_blocker.py +120 -8
- package/hooks/blocking/windows_rmtree_blocker.py +23 -6
- package/hooks/config/banned_identifiers_constants.py +24 -0
- package/hooks/config/hardcoded_user_path_constants.py +12 -0
- package/hooks/config/hook_log_extractor_constants.py +1 -1
- package/hooks/config/pre_tool_use_stdin.py +48 -0
- package/hooks/config/setup_project_paths_constants.py +4 -0
- package/hooks/config/stuttering_check_config.py +14 -0
- package/hooks/config/stuttering_import_binding_constants.py +11 -0
- package/hooks/config/sys_path_insert_constants.py +4 -0
- package/hooks/config/test_banned_identifiers_constants.py +48 -0
- package/hooks/config/test_hardcoded_user_path_constants.py +78 -0
- package/hooks/config/test_hook_log_extractor_constants.py +3 -3
- package/hooks/config/test_pre_tool_use_stdin.py +80 -0
- package/hooks/config/unused_module_import_constants.py +7 -0
- package/hooks/config/windows_rmtree_blocker_constants.py +3 -0
- package/hooks/diagnostic/hook_log_stop_wrapper.py +7 -4
- package/hooks/git-hooks/config.py +3 -3
- package/hooks/git-hooks/test_gate_utils.py +10 -10
- package/hooks/mypy.ini +2 -0
- package/package.json +1 -1
- package/rules/gh-paginate.md +125 -0
- package/skills/bugteam/CONSTRAINTS.md +12 -6
- package/skills/bugteam/PROMPTS.md +0 -39
- package/skills/bugteam/SKILL.md +93 -125
- package/skills/bugteam/SKILL_EVALS.md +25 -23
- package/skills/bugteam/reference/README.md +2 -0
- package/skills/bugteam/reference/audit-and-teammates.md +2 -2
- package/skills/bugteam/reference/copilot-gap-analysis.md +12 -0
- package/skills/bugteam/reference/teardown-publish-permissions.md +1 -1
- package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +113 -0
- package/skills/bugteam/reference/workflow-path-b-task-harness.md +48 -0
- package/skills/bugteam/test_skill_additions.py +13 -4
- package/skills/bugteam/test_team_lifecycle.py +94 -0
- package/skills/findbugs/SKILL.md +3 -3
- package/skills/fixbugs/SKILL.md +4 -4
- package/skills/monitor-open-prs/SKILL.md +32 -2
- package/skills/monitor-open-prs/test_team_lifecycle.py +46 -0
- package/skills/pr-converge/SKILL.md +576 -95
- package/skills/pr-converge/scripts/README.md +145 -0
- package/skills/pr-converge/scripts/caller-window-pid.ps1 +86 -0
- package/skills/pr-converge/scripts/check_pr_mergeability.py +79 -0
- package/skills/pr-converge/scripts/config/pr_converge_constants.py +65 -0
- package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +176 -0
- package/skills/pr-converge/scripts/cursor-agents-continue-caller.cmd +9 -0
- package/skills/pr-converge/scripts/cursor-agents-continue-stop-others.ps1 +16 -0
- package/skills/pr-converge/scripts/cursor-agents-continue.ahk +172 -0
- package/skills/pr-converge/scripts/cursor-agents-continue.cmd +2 -0
- package/skills/pr-converge/scripts/evict_cached_config_modules.py +20 -0
- package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +110 -0
- package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +103 -0
- package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +112 -0
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +121 -0
- package/skills/pr-converge/scripts/mark_pr_ready.py +54 -0
- package/skills/pr-converge/scripts/open_followup_copilot_pr.py +136 -0
- package/skills/pr-converge/scripts/post-bugbot-run.helpers.ps1 +49 -0
- package/skills/pr-converge/scripts/post-bugbot-run.ps1 +33 -0
- package/skills/pr-converge/scripts/reply_to_inline_comment.py +84 -0
- package/skills/pr-converge/scripts/request_copilot_review.py +71 -0
- package/skills/pr-converge/scripts/resolve_pr_head.py +58 -0
- package/skills/pr-converge/scripts/review_field_helpers.py +43 -0
- package/skills/pr-converge/scripts/test_check_pr_mergeability.py +126 -0
- package/skills/pr-converge/scripts/test_evict_cached_config_modules.py +22 -0
- package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +342 -0
- package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +220 -0
- package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +372 -0
- package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +280 -0
- package/skills/pr-converge/scripts/test_mark_pr_ready.py +69 -0
- package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +236 -0
- package/skills/pr-converge/scripts/test_post_bugbot_run.py +195 -0
- package/skills/pr-converge/scripts/test_reply_to_inline_comment.py +159 -0
- package/skills/pr-converge/scripts/test_request_copilot_review.py +101 -0
- package/skills/pr-converge/scripts/test_resolve_pr_head.py +79 -0
- package/skills/pr-converge/scripts/test_review_field_helpers.py +80 -0
- package/skills/pr-converge/scripts/test_trigger_bugbot.py +139 -0
- package/skills/pr-converge/scripts/test_view_pr_context.py +111 -0
- package/skills/pr-converge/scripts/trigger_bugbot.py +77 -0
- package/skills/pr-converge/scripts/view_pr_context.py +47 -0
- package/skills/pr-converge/test_team_lifecycle.py +47 -0
- package/skills/pr-converge/workflows/ahk-auto-continue-loop.md +108 -0
- package/skills/pr-converge/workflows/schedule-wakeup-loop.md +37 -0
- package/skills/qbug/SKILL.md +4 -4
- package/skills/qbug/test_qbug_skill_post_fix_audit.py +2 -2
- package/skills/resume-review/SKILL.md +261 -0
- package/agents/agent-writer.md +0 -157
- package/agents/config-centralizer.md +0 -686
- package/agents/config-extraction-agent.md +0 -225
- package/agents/doc-orchestrator.md +0 -47
- package/agents/docx-agent.md +0 -211
- package/agents/magic-value-eliminator-agent.md +0 -72
- package/agents/mandatory-agent-workflow-agent.md +0 -88
- package/agents/parallel-workflow-coordinator.md +0 -779
- package/agents/pdf-agent.md +0 -302
- package/agents/project-context-loader.md +0 -238
- package/agents/readability-review-agent.md +0 -76
- package/agents/refactoring-specialist.md +0 -69
- package/agents/right-sized-engineer.md +0 -129
- package/agents/session-continuity-manager.md +0 -53
- package/agents/stub-detector-agent.md +0 -140
- package/agents/tdd-test-writer.md +0 -62
- package/agents/test-data-builder.md +0 -68
- package/agents/tooling-builder.md +0 -78
- package/agents/validation-expert.md +0 -71
- package/agents/xlsx-agent.md +0 -169
- package/skills/bugteam/scripts/README.md +0 -58
- package/skills/bugteam/scripts/_claude_permissions_common.py +0 -219
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +0 -633
- package/skills/bugteam/scripts/bugteam_fix_hookspath.py +0 -260
- package/skills/bugteam/scripts/bugteam_preflight.py +0 -201
- package/skills/bugteam/scripts/config/bugteam_fix_hookspath_constants.py +0 -17
- package/skills/bugteam/scripts/grant_project_claude_permissions.py +0 -109
- package/skills/bugteam/scripts/revoke_project_claude_permissions.py +0 -135
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +0 -271
- package/skills/bugteam/scripts/test_bugteam_fix_hookspath.py +0 -267
- package/skills/bugteam/scripts/test_bugteam_preflight.py +0 -189
- package/skills/bugteam/scripts/test_claude_permissions_common.py +0 -44
- /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
|
+
}
|