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.
- package/_shared/pr-loop/scripts/config/preflight_constants.py +29 -8
- package/_shared/pr-loop/scripts/preflight.py +242 -20
- package/_shared/pr-loop/scripts/tests/test_preflight.py +362 -25
- package/_shared/pr-loop/scripts/tests/test_preflight_constants.py +9 -14
- package/hooks/blocking/code_rules_enforcer.py +269 -23
- package/hooks/blocking/test_code_rules_enforcer_unused_imports.py +157 -1
- package/hooks/config/test_unused_module_import_constants.py +48 -0
- package/hooks/config/unused_module_import_constants.py +41 -0
- package/package.json +1 -1
- package/skills/bg-agent/SKILL.md +69 -0
- package/skills/bugteam/CONSTRAINTS.md +10 -19
- package/skills/bugteam/PROMPTS.md +3 -3
- package/skills/bugteam/SKILL.md +103 -202
- package/skills/bugteam/SKILL_EVALS.md +75 -114
- package/skills/bugteam/reference/README.md +2 -4
- package/skills/bugteam/reference/design-rationale.md +3 -8
- package/skills/bugteam/reference/team-setup.md +11 -19
- package/skills/bugteam/reference/teardown-publish-permissions.md +2 -14
- package/skills/bugteam/scripts/config/__init__.py +0 -0
- package/skills/bugteam/scripts/config/reflow_skill_md_constants.py +12 -0
- package/skills/bugteam/scripts/reflow_skill_md.py +51 -47
- package/skills/bugteam/sources.md +1 -25
- package/skills/bugteam/test_skill_additions.py +4 -13
- package/skills/fresh-branch/SKILL.md +71 -0
- package/skills/gotcha/SKILL.md +73 -0
- package/skills/monitor-open-prs/SKILL.md +4 -37
- package/skills/monitor-open-prs/test_skill_contract.py +0 -5
- package/skills/pr-converge/SKILL.md +60 -1298
- package/skills/pr-converge/reference/convergence-gates.md +118 -0
- package/skills/pr-converge/reference/examples.md +76 -0
- package/skills/pr-converge/reference/fix-protocol.md +54 -0
- package/skills/pr-converge/reference/ground-rules.md +13 -0
- package/skills/pr-converge/reference/multi-pr-orchestration.md +204 -0
- package/skills/pr-converge/reference/per-tick.md +201 -0
- package/skills/pr-converge/reference/state-schema.md +19 -0
- package/skills/pr-converge/reference/stop-conditions.md +26 -0
- package/skills/pr-converge/scripts/README.md +36 -9
- package/skills/pr-converge/scripts/check_pr_mergeability.py +1 -2
- package/skills/pr-converge/scripts/config/pr_converge_constants.py +58 -5
- package/skills/pr-converge/scripts/config/reflow_skill_md_constants.py +13 -0
- package/skills/pr-converge/scripts/config/test_pr_converge_constants.py +0 -24
- package/skills/pr-converge/scripts/cursor-agents-continue.ahk +22 -2
- package/skills/pr-converge/scripts/fetch_bugbot_inline_comments.py +19 -59
- package/skills/pr-converge/scripts/fetch_bugbot_reviews.py +15 -61
- package/skills/pr-converge/scripts/fetch_claude_inline_comments.py +70 -0
- package/skills/pr-converge/scripts/fetch_claude_reviews.py +61 -0
- package/skills/pr-converge/scripts/fetch_copilot_inline_comments.py +19 -61
- package/skills/pr-converge/scripts/fetch_copilot_reviews.py +14 -74
- package/skills/pr-converge/scripts/reflow_skill_md.py +71 -50
- package/skills/pr-converge/scripts/reviewer_fetch_core.py +153 -0
- package/skills/pr-converge/scripts/reviewer_specs.py +98 -0
- package/skills/pr-converge/scripts/test_cursor_agents_continue.py +65 -0
- package/skills/pr-converge/scripts/test_fetch_bugbot_inline_comments.py +107 -6
- package/skills/pr-converge/scripts/test_fetch_bugbot_reviews.py +85 -6
- package/skills/pr-converge/scripts/test_fetch_claude_inline_comments.py +485 -0
- package/skills/pr-converge/scripts/test_fetch_claude_reviews.py +368 -0
- package/skills/pr-converge/scripts/test_fetch_copilot_inline_comments.py +74 -6
- package/skills/pr-converge/scripts/test_fetch_copilot_reviews.py +94 -8
- package/skills/pr-converge/scripts/test_reflow_skill_md.py +162 -0
- package/skills/pr-converge/scripts/test_reviewer_fetch_core.py +448 -0
- package/skills/pr-converge/scripts/test_reviewer_specs.py +107 -0
- package/skills/pr-converge/workflows/schedule-wakeup-loop.md +24 -22
- package/skills/bugteam/reference/workflow-path-a-orchestrated-teams.md +0 -113
- package/skills/bugteam/reference/workflow-path-b-task-harness.md +0 -48
- package/skills/bugteam/test_team_lifecycle.py +0 -103
- package/skills/monitor-open-prs/test_team_lifecycle.py +0 -46
- package/skills/pr-converge/scripts/open_followup_copilot_pr.py +0 -136
- package/skills/pr-converge/scripts/test_open_followup_copilot_pr.py +0 -236
- package/skills/pr-converge/test_team_lifecycle.py +0 -56
- 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
|
|
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
|
-
### `
|
|
128
|
+
### `fetch_claude_reviews.py`
|
|
128
129
|
|
|
129
|
-
|
|
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/
|
|
133
|
-
--owner <OWNER> --repo <REPO> --
|
|
134
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
86
|
+
EXAMPLE_OPEN_TAG: str = "<example>"
|
|
60
87
|
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
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 "\
|
|
47
|
+
stop_script := A_ScriptDir "\" STOP_SCRIPT_FILE_NAME
|
|
42
48
|
if !FileExist(stop_script)
|
|
43
49
|
return
|
|
44
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
9
|
-
``gh api`` on ``repos/{owner}/{repo}/pulls/{number}/comments`` with
|
|
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
|
|
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
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
|
24
|
-
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
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,
|
|
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())
|