claude-dev-env 1.36.0 → 1.36.2
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/audit-contract.md +159 -0
- package/_shared/pr-loop/code-rules-gate.md +64 -0
- package/_shared/pr-loop/fix-protocol.md +37 -0
- package/_shared/pr-loop/gh-payloads.md +85 -0
- package/_shared/pr-loop/scripts/README.md +20 -0
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +234 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +975 -0
- package/_shared/pr-loop/scripts/config/__init__.py +0 -0
- package/_shared/pr-loop/scripts/config/claude_permissions_constants.py +36 -0
- package/_shared/pr-loop/scripts/config/claude_settings_keys_constants.py +11 -0
- package/_shared/pr-loop/scripts/config/code_rules_gate_constants.py +56 -0
- package/_shared/pr-loop/scripts/config/fix_hookspath_constants.py +25 -0
- package/_shared/pr-loop/scripts/config/gh_util_constants.py +31 -0
- package/_shared/pr-loop/scripts/config/preflight_constants.py +47 -0
- package/_shared/pr-loop/scripts/fix_hookspath.py +260 -0
- package/_shared/pr-loop/scripts/gh_util.py +193 -0
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +130 -0
- package/_shared/pr-loop/scripts/preflight.py +227 -0
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +156 -0
- package/_shared/pr-loop/scripts/tests/conftest.py +51 -0
- package/_shared/pr-loop/scripts/tests/test__claude_permissions_common.py +135 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +169 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +58 -0
- package/_shared/pr-loop/scripts/tests/test_claude_settings_keys_constants.py +50 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +917 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +102 -0
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +374 -0
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath_constants.py +47 -0
- package/_shared/pr-loop/scripts/tests/test_gh_util.py +257 -0
- package/_shared/pr-loop/scripts/tests/test_gh_util_constants.py +61 -0
- package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +49 -0
- package/_shared/pr-loop/scripts/tests/test_preflight.py +333 -0
- package/_shared/pr-loop/scripts/tests/test_preflight_constants.py +82 -0
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +49 -0
- package/_shared/pr-loop/state-schema.md +81 -0
- package/package.json +2 -1
- package/skills/bugteam/SKILL.md +332 -108
- package/skills/bugteam/scripts/reflow_skill_md.py +298 -0
- package/skills/bugteam/test_team_lifecycle.py +9 -0
- package/skills/pr-converge/SKILL.md +1005 -395
- package/skills/pr-converge/scripts/reflow_skill_md.py +288 -0
- package/skills/pr-converge/test_team_lifecycle.py +9 -0
|
@@ -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()
|
|
@@ -45,3 +45,12 @@ def test_skill_tears_down_team_only_on_full_convergence():
|
|
|
45
45
|
def test_state_schema_includes_team_name_field():
|
|
46
46
|
skill_text = _skill_text()
|
|
47
47
|
assert '"team_name"' in skill_text or "team_name:" in skill_text
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_skill_md_physical_lines_fit_eighty_column_limit():
|
|
51
|
+
skill_text = _skill_text()
|
|
52
|
+
for each_line_number, each_physical_line in enumerate(skill_text.splitlines(), 1):
|
|
53
|
+
assert len(each_physical_line) <= 80, (
|
|
54
|
+
"SKILL.md line %s exceeds 80 columns (%s chars)"
|
|
55
|
+
% (each_line_number, len(each_physical_line))
|
|
56
|
+
)
|