claude-dev-env 1.36.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.
@@ -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
+ )