claude-dev-env 1.36.2 → 1.37.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 (70) hide show
  1. package/_shared/pr-loop/scripts/config/preflight_constants.py +29 -8
  2. package/_shared/pr-loop/scripts/preflight.py +242 -20
  3. package/_shared/pr-loop/scripts/tests/test_preflight.py +362 -25
  4. package/_shared/pr-loop/scripts/tests/test_preflight_constants.py +9 -14
  5. package/hooks/blocking/code_rules_enforcer.py +269 -23
  6. package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +157 -1
  7. package/hooks/config/test_unused_module_import_constants.py +48 -0
  8. package/hooks/config/unused_module_import_constants.py +41 -0
  9. package/package.json +1 -1
  10. package/skills/bg-agent/SKILL.md +69 -0
  11. package/skills/bugteam/CONSTRAINTS.md +10 -19
  12. package/skills/bugteam/PROMPTS.md +3 -3
  13. package/skills/bugteam/SKILL.md +103 -202
  14. package/skills/bugteam/SKILL_EVALS.md +75 -114
  15. package/skills/bugteam/reference/README.md +2 -4
  16. package/skills/bugteam/reference/design-rationale.md +3 -8
  17. package/skills/bugteam/reference/team-setup.md +11 -19
  18. package/skills/bugteam/reference/teardown-publish-permissions.md +2 -14
  19. package/skills/bugteam/scripts/config/__init__.py +0 -0
  20. package/skills/bugteam/scripts/config/reflow_skill_md_constants.py +12 -0
  21. package/skills/bugteam/scripts/reflow_skill_md.py +51 -47
  22. package/skills/bugteam/sources.md +1 -25
  23. package/skills/bugteam/test_skill_additions.py +4 -13
  24. package/skills/fresh-branch/SKILL.md +71 -0
  25. package/skills/gotcha/SKILL.md +73 -0
  26. package/skills/monitor-open-prs/SKILL.md +4 -37
  27. package/skills/monitor-open-prs/test_skill_contract.py +0 -5
  28. package/skills/pr-converge/SKILL.md +60 -1298
  29. package/skills/pr-converge/reference/convergence-gates.md +118 -0
  30. package/skills/pr-converge/reference/examples.md +76 -0
  31. package/skills/pr-converge/reference/fix-protocol.md +54 -0
  32. package/skills/pr-converge/reference/ground-rules.md +13 -0
  33. package/skills/pr-converge/reference/multi-pr-orchestration.md +204 -0
  34. package/skills/pr-converge/reference/per-tick.md +201 -0
  35. package/skills/pr-converge/reference/state-schema.md +19 -0
  36. package/skills/pr-converge/reference/stop-conditions.md +26 -0
  37. package/skills/pr-converge/scripts/README.md +36 -9
  38. package/skills/pr-converge/scripts/check_pr_mergeability.py +1 -2
  39. package/skills/pr-converge/scripts/config/pr_converge_constants.py +58 -5
  40. package/skills/pr-converge/scripts/config/reflow_skill_md_constants.py +13 -0
  41. package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +0 -24
  42. package/skills/pr-converge/scripts/cursor-agents-continue.ahk +22 -2
  43. package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +19 -59
  44. package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +15 -61
  45. package/skills/pr-converge/scripts/fetch_claude_inline_comments.py +70 -0
  46. package/skills/pr-converge/scripts/fetch_claude_reviews.py +61 -0
  47. package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +19 -61
  48. package/skills/pr-converge/scripts/fetch_copilot_reviews.py +14 -74
  49. package/skills/pr-converge/scripts/reflow_skill_md.py +71 -50
  50. package/skills/pr-converge/scripts/reviewer_fetch_core.py +153 -0
  51. package/skills/pr-converge/scripts/reviewer_specs.py +98 -0
  52. package/skills/pr-converge/scripts/test_cursor_agents_continue.py +65 -0
  53. package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +107 -6
  54. package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +85 -6
  55. package/skills/pr-converge/scripts/test_fetch_claude_inline_comments.py +485 -0
  56. package/skills/pr-converge/scripts/test_fetch_claude_reviews.py +368 -0
  57. package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +74 -6
  58. package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +94 -8
  59. package/skills/pr-converge/scripts/test_reflow_skill_md.py +162 -0
  60. package/skills/pr-converge/scripts/test_reviewer_fetch_core.py +448 -0
  61. package/skills/pr-converge/scripts/test_reviewer_specs.py +107 -0
  62. package/skills/pr-converge/workflows/schedule-wakeup-loop.md +24 -22
  63. package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +0 -113
  64. package/skills/bugteam/reference/workflow-path-b-task-harness.md +0 -48
  65. package/skills/bugteam/test_team_lifecycle.py +0 -103
  66. package/skills/monitor-open-prs/test_team_lifecycle.py +0 -46
  67. package/skills/pr-converge/scripts/open_followup_copilot_pr.py +0 -136
  68. package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +0 -236
  69. package/skills/pr-converge/test_team_lifecycle.py +0 -56
  70. package/skills/pr-converge/workflows/ahk-auto-continue-loop.md +0 -108
@@ -0,0 +1,19 @@
1
+ # State across ticks
2
+
3
+ **Dual persistence:** `<TMPDIR>/pr-converge-<session_id>/state.json`
4
+ exists (multi-PR) → that file is source of truth for `phase`, heads,
5
+ counters, status, not conversation transcript. No `state.json` (typical
6
+ single-PR `/pr-converge`) → track in each assistant turn as
7
+ plain text so next tick re-reads from context:
8
+
9
+ - `phase`: `BUGBOT` or `BUGTEAM`. Start `BUGBOT` on first tick.
10
+ - `bugbot_clean_at`: HEAD SHA where bugbot last reported clean, or `null`.
11
+ Reset to `null` on every push.
12
+ - `inline_lag_streak`: integer, init `0`. Consecutive ticks where review
13
+ body shows findings against `current_head` but inline API returns zero
14
+ matching. Reset to `0` on any other branch outcome.
15
+ - `tick_count`: integer, init `0`. Increment every tick.
16
+
17
+ Tick begins reading prior state line from most recent assistant message
18
+ (no `state.json`) and ends by emitting updated state line; with
19
+ `state.json`, follow `multi-pr-orchestration.md` §What orchestrator does per tick.
@@ -0,0 +1,26 @@
1
+ # Stop conditions
2
+
3
+ - **Convergence** (back-to-back clean ∧ no outstanding Copilot findings
4
+ on `current_head` ∧ `mergeStateStatus == "CLEAN"` with `mergeable ==
5
+ "MERGEABLE"` ∧ post-convergence Copilot request returned `clean` at
6
+ `current_head`): prefer `mark_pr_ready.py`; else `gh pr ready`. With
7
+ `state.json`, append convergence row to
8
+ `<TMPDIR>/pr-converge-<session_id>/converged.log` per `multi-pr-orchestration.md` §Memory; else
9
+ skip. Report [convergence-gates.md](convergence-gates.md) (d) summary, then **omit loop pacing**
10
+ per **Convergence** in `../workflows/schedule-wakeup-loop.md`. End all loops
11
+ once all PRs terminal (converged or blocked).
12
+ - **Hard blocker:** API auth failure across two ticks, CI regression
13
+ whose root cause falls outside this PR, hook rejection unresolved
14
+ across three commits, `inline_lag_streak >= 3`, **bugteam** reports
15
+ stuck, or post-convergence Copilot request fails to surface review on
16
+ `current_head` after three consecutive wakeups. Report specific
17
+ blocker and diagnosis, **omit loop pacing** per
18
+ `../workflows/schedule-wakeup-loop.md`.
19
+ - **Hard blocker (`mergeStateStatus` non-CLEAN non-DIRTY):**
20
+ `mergeStateStatus` is `BLOCKED`, `UNKNOWN`, or `BEHIND` (required
21
+ checks pending, branch behind base without textual conflicts, or
22
+ GitHub indeterminate). Investigate before retrying; `rebase` skill
23
+ handles `DIRTY` (textual conflicts) only. Report specific
24
+ `mergeStateStatus`, **omit loop pacing**.
25
+ - **User stops loop:** "stop the converge loop" → **omit loop pacing**
26
+ per `../workflows/schedule-wakeup-loop.md`.
@@ -25,7 +25,7 @@ python "${CLAUDE_SKILL_DIR}/scripts/fetch_bugbot_reviews.py" \
25
25
  --owner <OWNER> --repo <REPO> --number <NUMBER>
26
26
  ```
27
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).
28
+ Output: JSON array of `{review_id, commit_id, submitted_at, state, 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 issue 10459). Login filter is a case-insensitive substring match on `cursor` (handles login-shape divergence between review-level and inline-comment endpoints).
29
29
 
30
30
  ### `fetch_bugbot_inline_comments.py`
31
31
 
@@ -86,7 +86,8 @@ Output: the new reply id from gh's JSON response, on stdout.
86
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
87
 
88
88
  ```bash
89
- python "${CLAUDE_SKILL_DIR}/scripts/check_pr_mergeability.py"
89
+ python "${CLAUDE_SKILL_DIR}/scripts/check_pr_mergeability.py" \
90
+ --owner <OWNER> --repo <REPO> --number <NUMBER>
90
91
  ```
91
92
 
92
93
  Output: `{"mergeable", "mergeStateStatus", "headRefOid"}`.
@@ -100,7 +101,7 @@ python "${CLAUDE_SKILL_DIR}/scripts/fetch_copilot_reviews.py" \
100
101
  --owner <OWNER> --repo <REPO> --number <NUMBER>
101
102
  ```
102
103
 
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
+ 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`. Login filter is a case-insensitive substring match on `copilot` (handles the divergence where Copilot reviews come from `copilot-pull-request-reviewer[bot]` but its inline comments are authored by `Copilot`).
104
105
 
105
106
  ### `fetch_copilot_inline_comments.py`
106
107
 
@@ -124,17 +125,43 @@ python "${CLAUDE_SKILL_DIR}/scripts/request_copilot_review.py" \
124
125
 
125
126
  Output: none on success (gh's stdout is suppressed); `subprocess.CalledProcessError` on failure.
126
127
 
127
- ### `open_followup_copilot_pr.py`
128
+ ### `fetch_claude_reviews.py`
128
129
 
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
+ Fetches every Claude reviewer-bot review on the PR newest-first, classified per the review's `state` field. Mirror of `fetch_copilot_reviews.py` for an Anthropic Claude PR review bot (e.g. `claude[bot]`, `claude-code[bot]`, or any login containing `claude`).
130
131
 
131
132
  ```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>
133
+ python "${CLAUDE_SKILL_DIR}/scripts/fetch_claude_reviews.py" \
134
+ --owner <OWNER> --repo <REPO> --number <NUMBER>
135
+ ```
136
+
137
+ Output: JSON array of `{review_id, commit_id, submitted_at, state, body, classification}` — same shape and same state-based classification rules as `fetch_copilot_reviews.py`. Login filter is a case-insensitive substring match on `claude`.
138
+
139
+ ### `fetch_claude_inline_comments.py`
140
+
141
+ Fetches unaddressed Claude inline comments for the **newest submitted Claude review** on the requested `--commit` SHA (matches `pull_request_review_id` to the review returned by `fetch_claude_reviews.py` so stale inline threads from an older Claude review on the same SHA are ignored).
142
+
143
+ ```bash
144
+ python "${CLAUDE_SKILL_DIR}/scripts/fetch_claude_inline_comments.py" \
145
+ --owner <OWNER> --repo <REPO> --number <NUMBER> --commit <CURRENT_HEAD>
135
146
  ```
136
147
 
137
- Output: the new PR URL on stdout, trimmed.
148
+ Output: JSON array of `{comment_id, commit_id, path, line, body}`. Same `--paginate --slurp` pattern.
149
+
150
+ ## Shared modules
151
+
152
+ The reviewer fetch scripts share their fetch and classification logic via two internal modules. Entry-point scripts (`fetch_bugbot_reviews.py`, `fetch_copilot_reviews.py`, `fetch_claude_reviews.py` and their inline-comment counterparts) are thin wrappers — they import a per-reviewer spec, call the shared core, and shape argparse / JSON output.
153
+
154
+ ### `reviewer_specs.py`
155
+
156
+ Defines the `ReviewerSpec` frozen dataclass (two fields: `login_filter_substring` and `classify_review`) plus three module-level instances: `bugbot_spec`, `copilot_spec`, `claude_spec`. The state-based classifier used by Copilot and Claude is built via the shared `_make_state_based_classifier` factory; Bugbot has its own body-regex classifier because Bugbot uses `state == "COMMENTED"` for both clean and dirty reviews and only the body distinguishes them.
157
+
158
+ Spec instances use lowercase names because they are frozen dataclass values rather than scalar configuration constants — keeps them out of the `UPPER_SNAKE` constants-location rule that requires module-level constants outside `config/` to be hoisted there.
159
+
160
+ ### `reviewer_fetch_core.py`
161
+
162
+ Exports `fetch_reviewer_reviews(spec, ...)` and `fetch_reviewer_inline_comments(spec, ..., all_reviews=...)`. The inline-comments function takes pre-fetched reviews as an argument rather than fetching them internally, so each entry-point script keeps its own patchable `fetch_X_reviews` function for tests that mock the reviews fetch on the entry-point module.
163
+
164
+ The core enforces the gh-paginate contract (`--paginate --slurp` + Python JSON flattening, never `gh --jq` for cross-page operations) and the case-insensitive substring login filter in one place.
138
165
 
139
166
  ## Tests
140
167
 
@@ -5,8 +5,7 @@ so the skill body emits one script invocation. Single-object endpoint - no
5
5
  pagination. Explicit ``--owner``/``--repo``/``--number`` targeting matches every
6
6
  sibling convergence-gate script (``fetch_*_reviews.py``,
7
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
8
+ ``mark_pr_ready.py``); under multi-PR orchestration the gate is guaranteed
10
9
  to query the intended PR rather than whichever PR the current git context
11
10
  points at.
12
11
 
@@ -4,18 +4,37 @@ Path templates accept ``str.format(**kwargs)`` substitution; bugbot strings
4
4
  match the literal phrasing the Cursor Bugbot reviewer emits.
5
5
  """
6
6
 
7
+ import re
8
+ from pathlib import Path
9
+
7
10
  CURSOR_BOT_LOGIN: str = "cursor[bot]"
8
11
 
12
+ CURSOR_LOGIN_FILTER_SUBSTRING: str = "cursor"
13
+
9
14
  COPILOT_REVIEWER_LOGIN: str = "copilot-pull-request-reviewer[bot]"
10
15
 
11
16
  COPILOT_REVIEWER_REQUEST_ID: str = COPILOT_REVIEWER_LOGIN
12
17
 
18
+ COPILOT_LOGIN_FILTER_SUBSTRING: str = "copilot"
19
+
13
20
  COPILOT_CLEAN_REVIEW_STATE: str = "APPROVED"
14
21
 
15
22
  ALL_COPILOT_DIRTY_REVIEW_STATES: tuple[str, ...] = ("CHANGES_REQUESTED", "COMMENTED")
16
23
 
17
24
  COPILOT_SOFT_DIRTY_REVIEW_STATE: str = "COMMENTED"
18
25
 
26
+ CLAUDE_REVIEWER_LOGIN: str = "claude[bot]"
27
+
28
+ CLAUDE_REVIEWER_REQUEST_ID: str = CLAUDE_REVIEWER_LOGIN
29
+
30
+ CLAUDE_LOGIN_FILTER_SUBSTRING: str = "claude"
31
+
32
+ CLAUDE_CLEAN_REVIEW_STATE: str = "APPROVED"
33
+
34
+ ALL_CLAUDE_DIRTY_REVIEW_STATES: tuple[str, ...] = ("CHANGES_REQUESTED", "COMMENTED")
35
+
36
+ CLAUDE_SOFT_DIRTY_REVIEW_STATE: str = "COMMENTED"
37
+
19
38
  BUGBOT_DIRTY_BODY_REGEX: str = (
20
39
  r"Cursor Bugbot has reviewed your changes and found \d+ potential issue"
21
40
  )
@@ -54,12 +73,46 @@ GH_FIELD_BODY_AT_PREFIX: str = "body=@"
54
73
 
55
74
  GH_REPO_ARG_TEMPLATE: str = "{owner}/{repo}"
56
75
 
57
- PR_BASE_REF_FIELDS: str = "baseRefName"
76
+ SKILL_REFLOW_MAXIMUM_WIDTH: int = 80
77
+
78
+ PR_CONVERGE_SKILL_PATH: Path = Path(__file__).resolve().parent.parent.parent / "SKILL.md"
79
+
80
+ MARKDOWN_CODE_FENCE_MARKER: str = "```"
81
+
82
+ YAML_FRONT_MATTER_DELIMITER: str = "---"
83
+
84
+ YAML_DESCRIPTION_PREFIX: str = "description: >-"
58
85
 
59
- COPILOT_FOLLOWUP_BRANCH_TEMPLATE: str = "chore/copilot-followup-{parent_number}-{short_sha}"
86
+ EXAMPLE_OPEN_TAG: str = "<example>"
60
87
 
61
- COPILOT_FOLLOWUP_PR_TITLE_TEMPLATE: str = (
62
- "chore: address Copilot findings from PR #{parent_number}"
88
+ EXAMPLE_CLOSE_TAG: str = "</example>"
89
+
90
+ BASH_FENCE_LANGUAGE: str = "bash"
91
+
92
+ BASH_LINE_CONTINUATION_SUFFIX: str = " \\"
93
+
94
+ BASH_CONTINUATION_INDENT: str = " "
95
+
96
+ REFLOW_FRONT_MATTER_ERROR: str = "expected YAML front matter starting with ---"
97
+
98
+ ORDERED_MARKDOWN_LIST_PATTERN: re.Pattern[str] = re.compile(
99
+ r"^(?P<leading_whitespace>\s*)(?P<marker>\d+\.\s)(?P<body>.*)$"
100
+ )
101
+
102
+ BULLET_MARKDOWN_LIST_PATTERN: re.Pattern[str] = re.compile(
103
+ r"^(?P<leading_whitespace>\s*)(?P<marker>[-*]\s)(?P<body>.*)$"
63
104
  )
64
105
 
65
- COPILOT_FOLLOWUP_SHORT_SHA_LENGTH: int = 8
106
+ UNFINISHED_MARKDOWN_LINK_TARGET_PATTERN: re.Pattern[str] = re.compile(r"\]\([^)]*$")
107
+
108
+ MARKDOWN_HEADING_PATTERN: re.Pattern[str] = re.compile(r"^#{1,6}\s+.+$")
109
+
110
+ MARKDOWN_REFERENCE_DEFINITION_PATTERN: re.Pattern[str] = re.compile(r"^\[[^\]]+\]:\s+\S+")
111
+
112
+ BASH_LINE_CONTINUATION_MARKER_WIDTH: int = 2
113
+
114
+ CODE_FENCE_MARKER_LENGTH: int = 3
115
+
116
+ BASH_MINIMUM_SEGMENT_WIDTH: int = 1
117
+
118
+ LONG_ROW_PREVIEW_LIMIT: int = 20
@@ -0,0 +1,13 @@
1
+ """Configuration for the pr-converge skill Markdown reflow script."""
2
+
3
+ import re
4
+ from pathlib import Path
5
+
6
+ MAXIMUM_LINE_WIDTH: int = 80
7
+ BASH_CONTINUATION_MARKER_WIDTH: int = 2
8
+ TARGET_SKILL_PATH: Path = Path(__file__).resolve().parent.parent.parent / "SKILL.md"
9
+
10
+ ORDERED_LIST_ITEM_PATTERN: re.Pattern[str] = re.compile(r"^(\s*)(\d+\.\s)(.*)$")
11
+ BULLET_LIST_ITEM_PATTERN: re.Pattern[str] = re.compile(r"^(\s*)([-*]\s)(.*)$")
12
+ UNFINISHED_MARKDOWN_LINK_TARGET_PATTERN: re.Pattern[str] = re.compile(r"\]\([^)]*$")
13
+ MARKDOWN_REFERENCE_DEFINITION_PATTERN: re.Pattern[str] = re.compile(r"^\s*\[[^\]]+\]:\s*\S")
@@ -150,27 +150,3 @@ def test_requested_reviewers_field_template_accepts_reviewer_id() -> None:
150
150
  assert rendered == "reviewers[]=copilot-pull-request-reviewer[bot]"
151
151
 
152
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
@@ -34,14 +34,34 @@ LABEL_ON := "AGENTS: ON (Ctrl+Alt+A)"
34
34
  DPI_REFERENCE := 96
35
35
 
36
36
  AUTO_START_FLAG := "--start-on"
37
+ STOP_SCRIPT_FILE_NAME := "cursor-agents-continue-stop-others.ps1"
38
+ POWERSHELL_CORE_SHELL_NAME := "pwsh"
39
+ WINDOWS_POWERSHELL_SHELL_NAME := "powershell.exe"
40
+ STOP_SCRIPT_ARGUMENTS_FORMAT := ' -NoProfile -NoLogo -ExecutionPolicy Bypass -File "{1}" -KeepProcessId {2}'
41
+ STOP_SCRIPT_FAILURE_MESSAGE_FORMAT := 'Could not run duplicate cleanup script "{1}" with {2} or {3}.'
42
+ RUN_WAIT_WINDOW_OPTION := "Hide"
37
43
 
38
44
  INT32_MINIMUM_VALUE := -2147483648
39
45
 
40
46
  terminate_other_script_instances() {
41
- stop_script := A_ScriptDir "\cursor-agents-continue-stop-others.ps1"
47
+ stop_script := A_ScriptDir "\" STOP_SCRIPT_FILE_NAME
42
48
  if !FileExist(stop_script)
43
49
  return
44
- RunWait('pwsh -NoProfile -NoLogo -ExecutionPolicy Bypass -File "' stop_script '" -KeepProcessId ' ProcessExist(), , "Hide")
50
+ if run_stop_script_with_shell(POWERSHELL_CORE_SHELL_NAME, stop_script)
51
+ return
52
+ if run_stop_script_with_shell(WINDOWS_POWERSHELL_SHELL_NAME, stop_script)
53
+ return
54
+ throw Error(Format(STOP_SCRIPT_FAILURE_MESSAGE_FORMAT, stop_script, POWERSHELL_CORE_SHELL_NAME, WINDOWS_POWERSHELL_SHELL_NAME))
55
+ }
56
+
57
+ run_stop_script_with_shell(shell_name, stop_script) {
58
+ stop_command_arguments := Format(STOP_SCRIPT_ARGUMENTS_FORMAT, stop_script, ProcessExist())
59
+ try {
60
+ RunWait(shell_name stop_command_arguments, , RUN_WAIT_WINDOW_OPTION)
61
+ return true
62
+ } catch {
63
+ return false
64
+ }
45
65
  }
46
66
 
47
67
  terminate_other_script_instances()
@@ -1,17 +1,19 @@
1
1
  """Fetch unaddressed Cursor Bugbot inline comments for the latest Bugbot review on a commit.
2
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.
3
+ Thin wrapper around ``reviewer_fetch_core.fetch_reviewer_inline_comments``
4
+ parameterised by ``bugbot_spec``. The ``fetch_bugbot_reviews`` call lives here
5
+ (rather than inside the core) so tests can patch it on this module to exercise
6
+ the inline-comments fetch in isolation.
7
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.
8
+ Wraps the gh CLI invocation required by the gh-paginate rule for the comments
9
+ list: ``gh api`` on ``repos/{owner}/{repo}/pulls/{number}/comments`` with
10
+ ``--paginate --slurp`` and external JSON handling.
10
11
  """
11
12
 
13
+ from __future__ import annotations
14
+
12
15
  import argparse
13
16
  import json
14
- import subprocess
15
17
  import sys
16
18
  from pathlib import Path
17
19
 
@@ -22,12 +24,9 @@ from evict_cached_config_modules import evict_cached_config_modules
22
24
 
23
25
  evict_cached_config_modules()
24
26
 
25
- from config.pr_converge_constants import (
26
- CURSOR_BOT_LOGIN,
27
- GH_INLINE_COMMENTS_PATH_TEMPLATE,
28
- )
29
27
  from fetch_bugbot_reviews import fetch_bugbot_reviews
30
- from review_field_helpers import body_of, login_of
28
+ from reviewer_fetch_core import fetch_reviewer_inline_comments
29
+ from reviewer_specs import bugbot_spec
31
30
 
32
31
 
33
32
  def fetch_bugbot_inline_comments(
@@ -37,55 +36,16 @@ def fetch_bugbot_inline_comments(
37
36
  number: int,
38
37
  current_head: str,
39
38
  ) -> 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
- """
39
+ """Return Bugbot inline comments for the latest Bugbot review on ``current_head``."""
44
40
  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",
41
+ return fetch_reviewer_inline_comments(
42
+ bugbot_spec,
43
+ owner=owner,
44
+ repo=repo,
45
+ number=number,
46
+ current_head=current_head,
47
+ all_reviews=all_bugbot_reviews,
73
48
  )
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
49
 
90
50
 
91
51
  def main() -> int:
@@ -1,15 +1,16 @@
1
1
  """Fetch Cursor Bugbot reviews newest-first, classified as dirty or clean.
2
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).
3
+ Thin wrapper around ``reviewer_fetch_core.fetch_reviewer_reviews`` parameterised
4
+ by ``bugbot_spec``. Wraps the gh CLI invocation required by the gh-paginate
5
+ rule: ``gh api '...?per_page=100' --paginate --slurp`` piped through external
6
+ Python JSON handling (instead of ``gh --jq``, which runs per-page and breaks
7
+ cross-page operations like sort/reverse - see GitHub CLI issue 10459).
7
8
  """
8
9
 
10
+ from __future__ import annotations
11
+
9
12
  import argparse
10
13
  import json
11
- import re
12
- import subprocess
13
14
  import sys
14
15
  from pathlib import Path
15
16
 
@@ -20,12 +21,8 @@ from evict_cached_config_modules import evict_cached_config_modules
20
21
 
21
22
  evict_cached_config_modules()
22
23
 
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
24
+ from reviewer_fetch_core import fetch_reviewer_reviews
25
+ from reviewer_specs import bugbot_spec
29
26
 
30
27
 
31
28
  def fetch_bugbot_reviews(
@@ -34,55 +31,10 @@ def fetch_bugbot_reviews(
34
31
  repo: str,
35
32
  number: int,
36
33
  ) -> 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
34
+ """Return Cursor Bugbot reviews newest-first, each with a classification."""
35
+ return fetch_reviewer_reviews(
36
+ bugbot_spec, owner=owner, repo=repo, number=number
70
37
  )
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
38
 
87
39
 
88
40
  def main() -> int:
@@ -92,7 +44,9 @@ def main() -> int:
92
44
  parser.add_argument("--number", required=True, type=int)
93
45
  parsed_arguments = parser.parse_args()
94
46
  all_reviews = fetch_bugbot_reviews(
95
- owner=parsed_arguments.owner, repo=parsed_arguments.repo, number=parsed_arguments.number
47
+ owner=parsed_arguments.owner,
48
+ repo=parsed_arguments.repo,
49
+ number=parsed_arguments.number,
96
50
  )
97
51
  json.dump(all_reviews, sys.stdout)
98
52
  sys.stdout.write("\n")
@@ -0,0 +1,70 @@
1
+ """Fetch unaddressed Claude inline comments for the latest Claude review on a commit.
2
+
3
+ Thin wrapper around ``reviewer_fetch_core.fetch_reviewer_inline_comments``
4
+ parameterised by ``claude_spec``. The ``fetch_claude_reviews`` call lives here
5
+ (rather than inside the core) so tests can patch it on this module to exercise
6
+ the inline-comments fetch in isolation.
7
+
8
+ Wraps the gh CLI invocation required by the gh-paginate rule for the comments
9
+ list: ``gh api`` on ``repos/{owner}/{repo}/pulls/{number}/comments`` with
10
+ ``--paginate --slurp`` and external JSON handling.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import json
17
+ import sys
18
+ from pathlib import Path
19
+
20
+ if str(Path(__file__).resolve().parent) not in sys.path:
21
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
22
+
23
+ from evict_cached_config_modules import evict_cached_config_modules
24
+
25
+ evict_cached_config_modules()
26
+
27
+ from fetch_claude_reviews import fetch_claude_reviews
28
+ from reviewer_fetch_core import fetch_reviewer_inline_comments
29
+ from reviewer_specs import claude_spec
30
+
31
+
32
+ def fetch_claude_inline_comments(
33
+ *,
34
+ owner: str,
35
+ repo: str,
36
+ number: int,
37
+ current_head: str,
38
+ ) -> list[dict[str, object]]:
39
+ """Return Claude inline comments for the latest Claude review on ``current_head``."""
40
+ all_claude_reviews = fetch_claude_reviews(owner=owner, repo=repo, number=number)
41
+ return fetch_reviewer_inline_comments(
42
+ claude_spec,
43
+ owner=owner,
44
+ repo=repo,
45
+ number=number,
46
+ current_head=current_head,
47
+ all_reviews=all_claude_reviews,
48
+ )
49
+
50
+
51
+ def main() -> int:
52
+ parser = argparse.ArgumentParser(description=__doc__)
53
+ parser.add_argument("--owner", required=True)
54
+ parser.add_argument("--repo", required=True)
55
+ parser.add_argument("--number", required=True, type=int)
56
+ parser.add_argument("--commit", required=True, dest="current_head")
57
+ parsed_arguments = parser.parse_args()
58
+ all_comments = fetch_claude_inline_comments(
59
+ owner=parsed_arguments.owner,
60
+ repo=parsed_arguments.repo,
61
+ number=parsed_arguments.number,
62
+ current_head=parsed_arguments.current_head,
63
+ )
64
+ json.dump(all_comments, sys.stdout)
65
+ sys.stdout.write("\n")
66
+ return 0
67
+
68
+
69
+ if __name__ == "__main__":
70
+ sys.exit(main())
@@ -0,0 +1,61 @@
1
+ """Fetch Claude reviewer-bot reviews newest-first, classified as dirty or clean.
2
+
3
+ Thin wrapper around ``reviewer_fetch_core.fetch_reviewer_reviews`` parameterised
4
+ by ``claude_spec``. Classification follows the review's ``state`` field
5
+ (``APPROVED`` -> clean; ``CHANGES_REQUESTED`` -> dirty; ``COMMENTED`` with
6
+ non-empty body -> dirty; everything else -> clean) - see ``reviewer_specs``.
7
+
8
+ Wraps the gh CLI invocation required by the gh-paginate rule:
9
+ ``gh api '...?per_page=100' --paginate --slurp`` piped through external Python
10
+ JSON handling (instead of ``gh --jq``, which runs per-page and breaks
11
+ cross-page operations like sort/reverse - see GitHub CLI issue 10459).
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ import json
18
+ import sys
19
+ from pathlib import Path
20
+
21
+ if str(Path(__file__).resolve().parent) not in sys.path:
22
+ sys.path.insert(0, str(Path(__file__).resolve().parent))
23
+
24
+ from evict_cached_config_modules import evict_cached_config_modules
25
+
26
+ evict_cached_config_modules()
27
+
28
+ from reviewer_fetch_core import fetch_reviewer_reviews
29
+ from reviewer_specs import claude_spec
30
+
31
+
32
+ def fetch_claude_reviews(
33
+ *,
34
+ owner: str,
35
+ repo: str,
36
+ number: int,
37
+ ) -> list[dict[str, object]]:
38
+ """Return Claude reviews newest-first, each with a classification."""
39
+ return fetch_reviewer_reviews(
40
+ claude_spec, owner=owner, repo=repo, number=number
41
+ )
42
+
43
+
44
+ def main() -> int:
45
+ parser = argparse.ArgumentParser(description=__doc__)
46
+ parser.add_argument("--owner", required=True)
47
+ parser.add_argument("--repo", required=True)
48
+ parser.add_argument("--number", required=True, type=int)
49
+ parsed_arguments = parser.parse_args()
50
+ all_reviews = fetch_claude_reviews(
51
+ owner=parsed_arguments.owner,
52
+ repo=parsed_arguments.repo,
53
+ number=parsed_arguments.number,
54
+ )
55
+ json.dump(all_reviews, sys.stdout)
56
+ sys.stdout.write("\n")
57
+ return 0
58
+
59
+
60
+ if __name__ == "__main__":
61
+ sys.exit(main())