claude-dev-env 1.35.0 → 1.36.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/agents/clean-coder.md +109 -1
- package/bin/install.mjs +28 -8
- package/bin/install.test.mjs +9 -1
- package/docs/CODE_RULES.md +3 -0
- package/docs/agents-md-alignment-plan.md +123 -0
- package/hooks/blocking/code_rules_enforcer.py +451 -39
- package/hooks/blocking/es_exe_path_rewriter.py +10 -4
- package/hooks/blocking/test_code_rules_enforcer.py +182 -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 +191 -0
- package/hooks/blocking/test_code_rules_enforcer_constant_equality.py +40 -0
- package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +291 -0
- package/hooks/blocking/test_code_rules_enforcer_loop_variable_naming.py +87 -3
- package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +49 -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/SKILL.md +364 -154
- 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/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/scripts/reflow_skill_md.py +298 -0
- package/skills/bugteam/test_skill_additions.py +13 -4
- package/skills/bugteam/test_team_lifecycle.py +103 -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 +1206 -131
- 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/reflow_skill_md.py +288 -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 +56 -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/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,136 @@
|
|
|
1
|
+
"""Open a follow-up draft PR addressing Copilot findings from the parent PR.
|
|
2
|
+
|
|
3
|
+
Subprocess sequence:
|
|
4
|
+
|
|
5
|
+
1. ``gh pr view <parent_number> --json baseRefName`` to resolve the parent's base ref.
|
|
6
|
+
2. ``git fetch origin <head_sha>`` to make the SHA available locally.
|
|
7
|
+
3. ``git switch -c <new_branch> <head_sha>`` to create the follow-up branch off ``head_sha``.
|
|
8
|
+
4. ``git push -u origin <new_branch>`` to publish it.
|
|
9
|
+
5. ``gh pr create --draft --base <base_ref> --head <new_branch> --title <...> --body-file <findings_file>``
|
|
10
|
+
per the gh-body-file rule.
|
|
11
|
+
|
|
12
|
+
Returns the trimmed PR URL emitted by ``gh pr create`` on stdout.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import subprocess
|
|
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 config.pr_converge_constants import (
|
|
29
|
+
COPILOT_FOLLOWUP_BRANCH_TEMPLATE,
|
|
30
|
+
COPILOT_FOLLOWUP_PR_TITLE_TEMPLATE,
|
|
31
|
+
COPILOT_FOLLOWUP_SHORT_SHA_LENGTH,
|
|
32
|
+
GH_REPO_ARG_TEMPLATE,
|
|
33
|
+
PR_BASE_REF_FIELDS,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def open_followup_copilot_pr(
|
|
38
|
+
*,
|
|
39
|
+
owner: str,
|
|
40
|
+
repo: str,
|
|
41
|
+
parent_number: int,
|
|
42
|
+
head: str,
|
|
43
|
+
findings_file: Path,
|
|
44
|
+
) -> str:
|
|
45
|
+
"""Create the follow-up branch + draft PR; return the new PR URL."""
|
|
46
|
+
repo_arg = GH_REPO_ARG_TEMPLATE.format(owner=owner, repo=repo)
|
|
47
|
+
parent_base_ref = _resolve_parent_base_ref(
|
|
48
|
+
parent_number=parent_number, repo_arg=repo_arg
|
|
49
|
+
)
|
|
50
|
+
short_sha = head[:COPILOT_FOLLOWUP_SHORT_SHA_LENGTH]
|
|
51
|
+
new_branch_name = COPILOT_FOLLOWUP_BRANCH_TEMPLATE.format(
|
|
52
|
+
parent_number=parent_number, short_sha=short_sha
|
|
53
|
+
)
|
|
54
|
+
_run_checked(["git", "fetch", "origin", head])
|
|
55
|
+
_run_checked(["git", "switch", "-c", new_branch_name, head])
|
|
56
|
+
_run_checked(["git", "push", "-u", "origin", new_branch_name])
|
|
57
|
+
pr_title = COPILOT_FOLLOWUP_PR_TITLE_TEMPLATE.format(parent_number=parent_number)
|
|
58
|
+
completed = _run_checked(
|
|
59
|
+
[
|
|
60
|
+
"gh",
|
|
61
|
+
"pr",
|
|
62
|
+
"create",
|
|
63
|
+
"--repo",
|
|
64
|
+
repo_arg,
|
|
65
|
+
"--draft",
|
|
66
|
+
"--base",
|
|
67
|
+
parent_base_ref,
|
|
68
|
+
"--head",
|
|
69
|
+
new_branch_name,
|
|
70
|
+
"--title",
|
|
71
|
+
pr_title,
|
|
72
|
+
"--body-file",
|
|
73
|
+
str(findings_file),
|
|
74
|
+
]
|
|
75
|
+
)
|
|
76
|
+
return completed.stdout.strip()
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _resolve_parent_base_ref(*, parent_number: int, repo_arg: str) -> str:
|
|
80
|
+
completed = _run_checked(
|
|
81
|
+
[
|
|
82
|
+
"gh",
|
|
83
|
+
"pr",
|
|
84
|
+
"view",
|
|
85
|
+
str(parent_number),
|
|
86
|
+
"--repo",
|
|
87
|
+
repo_arg,
|
|
88
|
+
"--json",
|
|
89
|
+
PR_BASE_REF_FIELDS,
|
|
90
|
+
]
|
|
91
|
+
)
|
|
92
|
+
parent_pr_metadata = json.loads(completed.stdout)
|
|
93
|
+
base_ref_name_field = parent_pr_metadata.get("baseRefName")
|
|
94
|
+
if not isinstance(base_ref_name_field, str):
|
|
95
|
+
raise TypeError(
|
|
96
|
+
f"gh pr view baseRefName field is not str: {type(base_ref_name_field).__name__}"
|
|
97
|
+
)
|
|
98
|
+
return base_ref_name_field
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _run_checked(all_command_arguments: list[str]) -> subprocess.CompletedProcess:
|
|
102
|
+
return subprocess.run(
|
|
103
|
+
all_command_arguments,
|
|
104
|
+
capture_output=True,
|
|
105
|
+
check=True,
|
|
106
|
+
text=True,
|
|
107
|
+
encoding="utf-8",
|
|
108
|
+
errors="replace",
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def main() -> int:
|
|
113
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
114
|
+
parser.add_argument("--owner", required=True)
|
|
115
|
+
parser.add_argument("--repo", required=True)
|
|
116
|
+
parser.add_argument(
|
|
117
|
+
"--parent-number", required=True, type=int, dest="parent_number"
|
|
118
|
+
)
|
|
119
|
+
parser.add_argument("--head", required=True)
|
|
120
|
+
parser.add_argument(
|
|
121
|
+
"--findings-file", required=True, type=Path, dest="findings_file"
|
|
122
|
+
)
|
|
123
|
+
parsed_arguments = parser.parse_args()
|
|
124
|
+
new_pr_url = open_followup_copilot_pr(
|
|
125
|
+
owner=parsed_arguments.owner,
|
|
126
|
+
repo=parsed_arguments.repo,
|
|
127
|
+
parent_number=parsed_arguments.parent_number,
|
|
128
|
+
head=parsed_arguments.head,
|
|
129
|
+
findings_file=parsed_arguments.findings_file,
|
|
130
|
+
)
|
|
131
|
+
sys.stdout.write(f"{new_pr_url}\n")
|
|
132
|
+
return 0
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
if __name__ == "__main__":
|
|
136
|
+
sys.exit(main())
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
function Resolve-InvocationMode {
|
|
2
|
+
param([string] $PullRequestInput, [string] $RepositoryInput, [int] $NumberInput)
|
|
3
|
+
|
|
4
|
+
if ($NumberInput -gt 0) {
|
|
5
|
+
if ([string]::IsNullOrWhiteSpace($RepositoryInput)) {
|
|
6
|
+
throw 'When -Number is set, -Repository must be owner/repo (for example jl-cmd/claude-code-config).'
|
|
7
|
+
}
|
|
8
|
+
return @{ Mode = 'RepoNumber'; Repository = $RepositoryInput; Number = $NumberInput }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
$trimmed = $PullRequestInput.Trim()
|
|
12
|
+
if (-not [string]::IsNullOrWhiteSpace($trimmed)) {
|
|
13
|
+
return @{ Mode = 'Uri'; PullRequest = $trimmed }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
throw 'Provide a pull request URL, owner/repo#number as the first argument, or -Repository with -Number.'
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function Build-GhArgumentList {
|
|
20
|
+
param([hashtable] $Invocation, [string] $BodyFilePath)
|
|
21
|
+
|
|
22
|
+
if ($Invocation.Mode -eq 'RepoNumber') {
|
|
23
|
+
return @(
|
|
24
|
+
'pr', 'comment',
|
|
25
|
+
$Invocation.Number.ToString(),
|
|
26
|
+
'-R', $Invocation.Repository,
|
|
27
|
+
'--body-file', $BodyFilePath
|
|
28
|
+
)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
$trimmed = $Invocation.PullRequest
|
|
32
|
+
if ($trimmed -match '^https://github\.com/[^/]+/[^/]+/pull/\d+$') {
|
|
33
|
+
return @('pr', 'comment', $trimmed, '--body-file', $BodyFilePath)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if ($trimmed -match '^([^/]+)/([^/#]+)#(\d+)$') {
|
|
37
|
+
$owner = $Matches[1]
|
|
38
|
+
$repository_name = $Matches[2]
|
|
39
|
+
$pull_number = $Matches[3]
|
|
40
|
+
return @(
|
|
41
|
+
'pr', 'comment',
|
|
42
|
+
$pull_number,
|
|
43
|
+
'-R', ('{0}/{1}' -f $owner, $repository_name),
|
|
44
|
+
'--body-file', $BodyFilePath
|
|
45
|
+
)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
throw ('Unrecognized PullRequest "{0}". Use a https://github.com/owner/repo/pull/NN URL or owner/repo#NN.' -f $trimmed)
|
|
49
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
[CmdletBinding()]
|
|
2
|
+
param(
|
|
3
|
+
[Parameter(Position = 0, HelpMessage = 'GitHub pull request URL or owner/repo#number.')]
|
|
4
|
+
[string] $PullRequest = '',
|
|
5
|
+
[Parameter(HelpMessage = 'owner/repo when using -Number instead of a URL or owner/repo#number.')]
|
|
6
|
+
[string] $Repository = '',
|
|
7
|
+
[Parameter(HelpMessage = 'Pull request number; requires -Repository.')]
|
|
8
|
+
[int] $Number = 0
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
$helpers_path = Join-Path $PSScriptRoot 'post-bugbot-run.helpers.ps1'
|
|
12
|
+
. $helpers_path
|
|
13
|
+
|
|
14
|
+
$LiteralBugbotRunBody = "bugbot run`n"
|
|
15
|
+
|
|
16
|
+
$invocation = Resolve-InvocationMode -PullRequestInput $PullRequest -RepositoryInput $Repository -NumberInput $Number
|
|
17
|
+
$scratch_temp_path = [System.IO.Path]::GetTempFileName()
|
|
18
|
+
$body_file_path = [System.IO.Path]::ChangeExtension($scratch_temp_path, '.md')
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
$utf8_without_byte_order_mark = New-Object System.Text.UTF8Encoding $false
|
|
22
|
+
[System.IO.File]::WriteAllText($body_file_path, $LiteralBugbotRunBody, $utf8_without_byte_order_mark)
|
|
23
|
+
|
|
24
|
+
$null = Get-Command gh -ErrorAction Stop
|
|
25
|
+
$argument_list = Build-GhArgumentList -Invocation $invocation -BodyFilePath $body_file_path
|
|
26
|
+
& gh @argument_list
|
|
27
|
+
if ($LASTEXITCODE -ne 0) {
|
|
28
|
+
throw ('gh exited with code {0}.' -f $LASTEXITCODE)
|
|
29
|
+
}
|
|
30
|
+
} finally {
|
|
31
|
+
Remove-Item -LiteralPath $body_file_path -Force -ErrorAction SilentlyContinue
|
|
32
|
+
Remove-Item -LiteralPath $scratch_temp_path -Force -ErrorAction SilentlyContinue
|
|
33
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""Reflow packages/claude-dev-env/skills/pr-converge/SKILL.md to 80 columns.
|
|
2
|
+
|
|
3
|
+
Merge soft line breaks outside fenced blocks (space join; URL path fragments
|
|
4
|
+
joined without a space only inside unfinished markdown link targets), then
|
|
5
|
+
wrap with textwrap. Preserves fenced blocks verbatim.
|
|
6
|
+
|
|
7
|
+
Run: python3 packages/claude-dev-env/skills/pr-converge/scripts/reflow_skill_md.py
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import re
|
|
13
|
+
import textwrap
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
MAX_WIDTH = 80
|
|
17
|
+
SKILL_PATH = Path(__file__).resolve().parent.parent / "SKILL.md"
|
|
18
|
+
|
|
19
|
+
ORDERED_RE = re.compile(r"^(\s*)(\d+\.\s)(.*)$")
|
|
20
|
+
BULLET_RE = re.compile(r"^(\s*)([-*]\s)(.*)$")
|
|
21
|
+
UNFINISHED_MD_LINK_TARGET = re.compile(r"\]\([^)]*$")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def wrap_paragraph_plain(text: str) -> list[str]:
|
|
25
|
+
collapsed = " ".join(text.split())
|
|
26
|
+
if not collapsed:
|
|
27
|
+
return []
|
|
28
|
+
return textwrap.fill(
|
|
29
|
+
collapsed,
|
|
30
|
+
width=MAX_WIDTH,
|
|
31
|
+
break_long_words=False,
|
|
32
|
+
break_on_hyphens=False,
|
|
33
|
+
).splitlines()
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def wrap_list_item(lead_ws: str, marker: str, body: str) -> list[str]:
|
|
37
|
+
collapsed = " ".join(body.split())
|
|
38
|
+
if not collapsed:
|
|
39
|
+
return [lead_ws + marker.rstrip()]
|
|
40
|
+
prefix = lead_ws + marker
|
|
41
|
+
subsequent = lead_ws + (" " * len(marker))
|
|
42
|
+
return textwrap.fill(
|
|
43
|
+
collapsed,
|
|
44
|
+
width=MAX_WIDTH,
|
|
45
|
+
initial_indent=prefix,
|
|
46
|
+
subsequent_indent=subsequent,
|
|
47
|
+
break_long_words=False,
|
|
48
|
+
break_on_hyphens=False,
|
|
49
|
+
).splitlines()
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def reflow_yaml_description_block(lines: list[str], body_start: int) -> tuple[list[str], int]:
|
|
53
|
+
body_parts: list[str] = []
|
|
54
|
+
index = body_start
|
|
55
|
+
while index < len(lines):
|
|
56
|
+
line = lines[index]
|
|
57
|
+
if line.strip() == "---":
|
|
58
|
+
index += 1
|
|
59
|
+
break
|
|
60
|
+
stripped = line.lstrip()
|
|
61
|
+
if stripped:
|
|
62
|
+
body_parts.append(stripped)
|
|
63
|
+
index += 1
|
|
64
|
+
merged = " ".join(body_parts)
|
|
65
|
+
merged = merged.replace(
|
|
66
|
+
"`<TMPDIR>/pr-converge-<session_id>/state.json` per",
|
|
67
|
+
"`<TMPDIR>/pr-converge-<session_id>/state.json>` per",
|
|
68
|
+
)
|
|
69
|
+
wrapped = textwrap.fill(
|
|
70
|
+
merged,
|
|
71
|
+
width=MAX_WIDTH,
|
|
72
|
+
initial_indent=" ",
|
|
73
|
+
subsequent_indent=" ",
|
|
74
|
+
break_long_words=False,
|
|
75
|
+
break_on_hyphens=False,
|
|
76
|
+
)
|
|
77
|
+
return wrapped.splitlines(), index
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def is_table_line(line: str) -> bool:
|
|
81
|
+
return line.lstrip().startswith("|")
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def is_new_logical_line(stripped: str) -> bool:
|
|
85
|
+
if not stripped:
|
|
86
|
+
return False
|
|
87
|
+
if stripped.startswith("```"):
|
|
88
|
+
return True
|
|
89
|
+
if stripped.startswith("#"):
|
|
90
|
+
return True
|
|
91
|
+
if stripped == "---":
|
|
92
|
+
return True
|
|
93
|
+
if is_table_line(stripped):
|
|
94
|
+
return True
|
|
95
|
+
if stripped.startswith("<example>") or stripped.startswith("</example>"):
|
|
96
|
+
return True
|
|
97
|
+
if ORDERED_RE.match(stripped) or BULLET_RE.match(stripped):
|
|
98
|
+
return True
|
|
99
|
+
return False
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def merge_without_space(buffer: str, continuation: str) -> bool:
|
|
103
|
+
"""Join without space only for split markdown link URL paths."""
|
|
104
|
+
base = buffer.rstrip()
|
|
105
|
+
stripped = continuation.lstrip()
|
|
106
|
+
if not base or not stripped:
|
|
107
|
+
return False
|
|
108
|
+
if stripped.startswith("/") and UNFINISHED_MD_LINK_TARGET.search(base):
|
|
109
|
+
return True
|
|
110
|
+
return False
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def merge_soft_breaks(lines: list[str]) -> list[str]:
|
|
114
|
+
output: list[str] = []
|
|
115
|
+
index = 0
|
|
116
|
+
in_fence = False
|
|
117
|
+
while index < len(lines):
|
|
118
|
+
raw = lines[index]
|
|
119
|
+
line = raw.rstrip("\n")
|
|
120
|
+
if line.lstrip().startswith("```"):
|
|
121
|
+
in_fence = not in_fence
|
|
122
|
+
output.append(line)
|
|
123
|
+
index += 1
|
|
124
|
+
continue
|
|
125
|
+
if in_fence:
|
|
126
|
+
output.append(line)
|
|
127
|
+
index += 1
|
|
128
|
+
continue
|
|
129
|
+
if line.strip() == "":
|
|
130
|
+
output.append(line)
|
|
131
|
+
index += 1
|
|
132
|
+
continue
|
|
133
|
+
buffer_line = line
|
|
134
|
+
index += 1
|
|
135
|
+
while index < len(lines):
|
|
136
|
+
next_raw = lines[index].rstrip("\n")
|
|
137
|
+
if next_raw.strip() == "":
|
|
138
|
+
break
|
|
139
|
+
if next_raw.lstrip().startswith("```"):
|
|
140
|
+
break
|
|
141
|
+
stripped_next = next_raw.lstrip()
|
|
142
|
+
if is_new_logical_line(stripped_next):
|
|
143
|
+
break
|
|
144
|
+
if merge_without_space(buffer_line, stripped_next):
|
|
145
|
+
buffer_line = buffer_line.rstrip() + stripped_next
|
|
146
|
+
else:
|
|
147
|
+
buffer_line = f"{buffer_line.rstrip()} {stripped_next}"
|
|
148
|
+
index += 1
|
|
149
|
+
output.append(buffer_line)
|
|
150
|
+
return output
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def reflow_merged_line(line: str) -> list[str]:
|
|
154
|
+
stripped = line.strip()
|
|
155
|
+
if stripped == "":
|
|
156
|
+
return [""]
|
|
157
|
+
if stripped.startswith("```"):
|
|
158
|
+
return [line]
|
|
159
|
+
if stripped.startswith("#"):
|
|
160
|
+
if len(stripped) <= MAX_WIDTH:
|
|
161
|
+
return [stripped]
|
|
162
|
+
title = stripped.lstrip("#").strip()
|
|
163
|
+
level = len(stripped) - len(stripped.lstrip("#"))
|
|
164
|
+
prefix = "#" * level + " "
|
|
165
|
+
return textwrap.fill(
|
|
166
|
+
title,
|
|
167
|
+
width=MAX_WIDTH,
|
|
168
|
+
initial_indent=prefix,
|
|
169
|
+
subsequent_indent=prefix,
|
|
170
|
+
break_long_words=False,
|
|
171
|
+
break_on_hyphens=False,
|
|
172
|
+
).splitlines()
|
|
173
|
+
if stripped == "---":
|
|
174
|
+
return ["---"]
|
|
175
|
+
if is_table_line(stripped):
|
|
176
|
+
return [stripped]
|
|
177
|
+
if stripped.startswith("</example>"):
|
|
178
|
+
return [stripped]
|
|
179
|
+
if stripped.startswith("<example>"):
|
|
180
|
+
inner = stripped[len("<example>") :].strip()
|
|
181
|
+
if not inner:
|
|
182
|
+
return ["<example>"]
|
|
183
|
+
tag = "<example> "
|
|
184
|
+
subsequent = " " * len(tag)
|
|
185
|
+
return textwrap.fill(
|
|
186
|
+
" ".join(inner.split()),
|
|
187
|
+
width=MAX_WIDTH,
|
|
188
|
+
initial_indent=tag,
|
|
189
|
+
subsequent_indent=subsequent,
|
|
190
|
+
break_long_words=False,
|
|
191
|
+
break_on_hyphens=False,
|
|
192
|
+
).splitlines()
|
|
193
|
+
|
|
194
|
+
ordered = ORDERED_RE.match(line)
|
|
195
|
+
if ordered:
|
|
196
|
+
return wrap_list_item(ordered.group(1), ordered.group(2), ordered.group(3))
|
|
197
|
+
|
|
198
|
+
bullet = BULLET_RE.match(line)
|
|
199
|
+
if bullet:
|
|
200
|
+
return wrap_list_item(bullet.group(1), bullet.group(2), bullet.group(3))
|
|
201
|
+
|
|
202
|
+
return wrap_paragraph_plain(stripped)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def reflow_markdown_body(lines: list[str]) -> list[str]:
|
|
206
|
+
merged = merge_soft_breaks(lines)
|
|
207
|
+
output: list[str] = []
|
|
208
|
+
for each_line in merged:
|
|
209
|
+
if each_line.strip() == "":
|
|
210
|
+
output.append("")
|
|
211
|
+
continue
|
|
212
|
+
output.extend(reflow_merged_line(each_line))
|
|
213
|
+
return output
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def wrap_long_bash_fence_lines(lines: list[str]) -> list[str]:
|
|
217
|
+
"""Hard-wrap only ```bash fence bodies that still exceed MAX_WIDTH."""
|
|
218
|
+
output: list[str] = []
|
|
219
|
+
in_bash_fence = False
|
|
220
|
+
for line in lines:
|
|
221
|
+
stripped = line.lstrip()
|
|
222
|
+
if stripped.startswith("```"):
|
|
223
|
+
if not in_bash_fence:
|
|
224
|
+
lang = stripped[3:].strip().lower()
|
|
225
|
+
in_bash_fence = lang == "bash"
|
|
226
|
+
else:
|
|
227
|
+
in_bash_fence = False
|
|
228
|
+
output.append(line)
|
|
229
|
+
continue
|
|
230
|
+
if in_bash_fence and len(line) > MAX_WIDTH:
|
|
231
|
+
indent_len = len(line) - len(line.lstrip())
|
|
232
|
+
indent = line[:indent_len]
|
|
233
|
+
content = line.lstrip()
|
|
234
|
+
wrapped_segments: list[str] = []
|
|
235
|
+
rest = content
|
|
236
|
+
while len(rest) > MAX_WIDTH - len(indent):
|
|
237
|
+
room = MAX_WIDTH - len(indent) - 2
|
|
238
|
+
window = rest[:room]
|
|
239
|
+
break_at = window.rfind(" ")
|
|
240
|
+
if break_at <= 0:
|
|
241
|
+
break_at = room
|
|
242
|
+
piece = rest[:break_at].rstrip()
|
|
243
|
+
rest = rest[break_at:].lstrip()
|
|
244
|
+
wrapped_segments.append(indent + piece + " \\")
|
|
245
|
+
if rest:
|
|
246
|
+
wrapped_segments.append(indent + (" " if wrapped_segments else "") + rest)
|
|
247
|
+
output.extend(wrapped_segments)
|
|
248
|
+
else:
|
|
249
|
+
output.append(line)
|
|
250
|
+
return output
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def main() -> None:
|
|
254
|
+
raw = SKILL_PATH.read_text(encoding="utf-8")
|
|
255
|
+
lines = raw.splitlines()
|
|
256
|
+
if not lines or lines[0].strip() != "---":
|
|
257
|
+
raise SystemExit("expected YAML front matter starting with ---")
|
|
258
|
+
|
|
259
|
+
out: list[str] = ["---"]
|
|
260
|
+
index = 1
|
|
261
|
+
while index < len(lines):
|
|
262
|
+
line = lines[index]
|
|
263
|
+
if line.startswith("description: >-"):
|
|
264
|
+
out.append(line)
|
|
265
|
+
index += 1
|
|
266
|
+
desc_lines, index = reflow_yaml_description_block(lines, index)
|
|
267
|
+
out.extend(desc_lines)
|
|
268
|
+
out.append("---")
|
|
269
|
+
break
|
|
270
|
+
out.append(line)
|
|
271
|
+
index += 1
|
|
272
|
+
|
|
273
|
+
body = reflow_markdown_body(lines[index:])
|
|
274
|
+
body = wrap_long_bash_fence_lines(body)
|
|
275
|
+
|
|
276
|
+
text = "\n".join(out + body) + "\n"
|
|
277
|
+
SKILL_PATH.write_text(text, encoding="utf-8", newline="\n")
|
|
278
|
+
|
|
279
|
+
all_lines = text.splitlines()
|
|
280
|
+
long_rows = [(i, len(ln)) for i, ln in enumerate(all_lines, 1) if len(ln) > MAX_WIDTH]
|
|
281
|
+
print("SKILL.md reflowed; lines:", len(all_lines))
|
|
282
|
+
print("lines longer than %d: %d" % (MAX_WIDTH, len(long_rows)))
|
|
283
|
+
if long_rows[:20]:
|
|
284
|
+
print("first long:", long_rows[:20])
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
if __name__ == "__main__":
|
|
288
|
+
main()
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"""Post an inline reply to a PR review comment.
|
|
2
|
+
|
|
3
|
+
Reply body is sourced from a file via `gh api ... -F body=@<path>` (per the
|
|
4
|
+
gh-body-file rule — passing a string body to gh can corrupt backticks).
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import argparse
|
|
8
|
+
import json
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
|
|
13
|
+
if str(Path(__file__).resolve().parent) not in sys.path:
|
|
14
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
15
|
+
|
|
16
|
+
from evict_cached_config_modules import evict_cached_config_modules
|
|
17
|
+
|
|
18
|
+
evict_cached_config_modules()
|
|
19
|
+
|
|
20
|
+
from config.pr_converge_constants import (
|
|
21
|
+
GH_FIELD_BODY_AT_PREFIX,
|
|
22
|
+
GH_INLINE_COMMENT_REPLY_PATH_TEMPLATE,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def reply_to_inline_comment(
|
|
27
|
+
*,
|
|
28
|
+
owner: str,
|
|
29
|
+
repo: str,
|
|
30
|
+
number: int,
|
|
31
|
+
comment_id: int,
|
|
32
|
+
body_file: Path,
|
|
33
|
+
) -> int:
|
|
34
|
+
"""POST an inline reply to a PR review comment, return gh's reply id."""
|
|
35
|
+
replies_endpoint = GH_INLINE_COMMENT_REPLY_PATH_TEMPLATE.format(
|
|
36
|
+
owner=owner, repo=repo, number=number, comment_id=comment_id
|
|
37
|
+
)
|
|
38
|
+
gh_command: list[str] = [
|
|
39
|
+
"gh",
|
|
40
|
+
"api",
|
|
41
|
+
"-X",
|
|
42
|
+
"POST",
|
|
43
|
+
replies_endpoint,
|
|
44
|
+
"-F",
|
|
45
|
+
f"{GH_FIELD_BODY_AT_PREFIX}{body_file}",
|
|
46
|
+
]
|
|
47
|
+
completed = subprocess.run(
|
|
48
|
+
gh_command,
|
|
49
|
+
capture_output=True,
|
|
50
|
+
check=True,
|
|
51
|
+
text=True,
|
|
52
|
+
encoding="utf-8",
|
|
53
|
+
errors="replace",
|
|
54
|
+
)
|
|
55
|
+
response_payload: dict[str, object] = json.loads(completed.stdout)
|
|
56
|
+
raw_reply_id = response_payload["id"]
|
|
57
|
+
if not isinstance(raw_reply_id, (int, str)):
|
|
58
|
+
raise TypeError(
|
|
59
|
+
f"gh response id field is not int|str: {type(raw_reply_id).__name__}"
|
|
60
|
+
)
|
|
61
|
+
return int(raw_reply_id)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def main() -> int:
|
|
65
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
66
|
+
parser.add_argument("--owner", required=True)
|
|
67
|
+
parser.add_argument("--repo", required=True)
|
|
68
|
+
parser.add_argument("--number", required=True, type=int)
|
|
69
|
+
parser.add_argument("--comment-id", required=True, type=int, dest="comment_id")
|
|
70
|
+
parser.add_argument("--body-file", required=True, type=Path, dest="body_file")
|
|
71
|
+
parsed_arguments = parser.parse_args()
|
|
72
|
+
reply_id = reply_to_inline_comment(
|
|
73
|
+
owner=parsed_arguments.owner,
|
|
74
|
+
repo=parsed_arguments.repo,
|
|
75
|
+
number=parsed_arguments.number,
|
|
76
|
+
comment_id=parsed_arguments.comment_id,
|
|
77
|
+
body_file=parsed_arguments.body_file,
|
|
78
|
+
)
|
|
79
|
+
sys.stdout.write(f"{reply_id}\n")
|
|
80
|
+
return 0
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == "__main__":
|
|
84
|
+
sys.exit(main())
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""Request a Copilot review on the current PR via the requested_reviewers API.
|
|
2
|
+
|
|
3
|
+
The reviewer ID literal is ``copilot-pull-request-reviewer[bot]`` - the
|
|
4
|
+
``[bot]`` suffix is load-bearing per ``skills/copilot-review/SKILL.md``;
|
|
5
|
+
``Copilot``, ``copilot``, and ``github-copilot`` all silently no-op on this
|
|
6
|
+
endpoint. After this POST returns, GitHub schedules Copilot to render a review
|
|
7
|
+
on the current HEAD; the caller polls ``fetch_copilot_reviews.py`` to converge.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
if str(Path(__file__).resolve().parent) not in sys.path:
|
|
16
|
+
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
17
|
+
|
|
18
|
+
from evict_cached_config_modules import evict_cached_config_modules
|
|
19
|
+
|
|
20
|
+
evict_cached_config_modules()
|
|
21
|
+
|
|
22
|
+
from config.pr_converge_constants import (
|
|
23
|
+
COPILOT_REVIEWER_REQUEST_ID,
|
|
24
|
+
GH_REQUESTED_REVIEWERS_FIELD_TEMPLATE,
|
|
25
|
+
GH_REQUESTED_REVIEWERS_PATH_TEMPLATE,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def request_copilot_review(*, owner: str, repo: str, number: int) -> None:
|
|
30
|
+
"""POST a Copilot review request to the PR's requested_reviewers endpoint."""
|
|
31
|
+
requested_reviewers_endpoint = GH_REQUESTED_REVIEWERS_PATH_TEMPLATE.format(
|
|
32
|
+
owner=owner, repo=repo, number=number
|
|
33
|
+
)
|
|
34
|
+
reviewer_field_value = GH_REQUESTED_REVIEWERS_FIELD_TEMPLATE.format(
|
|
35
|
+
reviewer_id=COPILOT_REVIEWER_REQUEST_ID
|
|
36
|
+
)
|
|
37
|
+
gh_command: list[str] = [
|
|
38
|
+
"gh",
|
|
39
|
+
"api",
|
|
40
|
+
"-X",
|
|
41
|
+
"POST",
|
|
42
|
+
requested_reviewers_endpoint,
|
|
43
|
+
"-f",
|
|
44
|
+
reviewer_field_value,
|
|
45
|
+
]
|
|
46
|
+
subprocess.run(
|
|
47
|
+
gh_command,
|
|
48
|
+
capture_output=True,
|
|
49
|
+
check=True,
|
|
50
|
+
text=True,
|
|
51
|
+
encoding="utf-8",
|
|
52
|
+
errors="replace",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def main() -> int:
|
|
57
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
58
|
+
parser.add_argument("--owner", required=True)
|
|
59
|
+
parser.add_argument("--repo", required=True)
|
|
60
|
+
parser.add_argument("--number", required=True, type=int)
|
|
61
|
+
parsed_arguments = parser.parse_args()
|
|
62
|
+
request_copilot_review(
|
|
63
|
+
owner=parsed_arguments.owner,
|
|
64
|
+
repo=parsed_arguments.repo,
|
|
65
|
+
number=parsed_arguments.number,
|
|
66
|
+
)
|
|
67
|
+
return 0
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
if __name__ == "__main__":
|
|
71
|
+
sys.exit(main())
|