claude-dev-env 1.49.0 → 1.50.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/audit-rubrics/category_rubrics/category-a-api-contracts.md +86 -0
- package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +36 -0
- package/audit-rubrics/category_rubrics/category-c-resource-cleanup.md +35 -0
- package/audit-rubrics/category_rubrics/category-d-scoping-and-ordering.md +35 -0
- package/audit-rubrics/category_rubrics/category-e-dead-code.md +38 -0
- package/audit-rubrics/category_rubrics/category-f-silent-failures.md +38 -0
- package/audit-rubrics/category_rubrics/category-g-bounds-and-overflow.md +38 -0
- package/audit-rubrics/category_rubrics/category-h-security-boundaries.md +40 -0
- package/audit-rubrics/category_rubrics/category-i-concurrency.md +38 -0
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +46 -0
- package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +59 -0
- package/audit-rubrics/category_rubrics/category-l-behavior-equivalence.md +45 -0
- package/audit-rubrics/category_rubrics/category-m-producer-consumer-cardinality.md +44 -0
- package/audit-rubrics/category_rubrics/category-n-test-name-scenario-verifier.md +45 -0
- package/audit-rubrics/prompts/category-a-api-contracts.md +399 -0
- package/audit-rubrics/prompts/category-b-selector-engine-compat.md +401 -0
- package/audit-rubrics/prompts/category-c-resource-cleanup.md +420 -0
- package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +414 -0
- package/audit-rubrics/prompts/category-e-dead-code.md +420 -0
- package/audit-rubrics/prompts/category-f-silent-failures.md +420 -0
- package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +383 -0
- package/audit-rubrics/prompts/category-h-security-boundaries.md +423 -0
- package/audit-rubrics/prompts/category-i-concurrency.md +429 -0
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +463 -0
- package/audit-rubrics/prompts/category-k-codebase-conflicts.md +328 -0
- package/audit-rubrics/prompts/category-l-behavior-equivalence.md +128 -0
- package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +129 -0
- package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +132 -0
- package/audit-rubrics/source-material-section-types.md +51 -0
- package/docs/CODE_RULES.md +6 -1
- package/hooks/blocking/code_rules_enforcer.py +323 -11
- package/hooks/blocking/md_to_html_blocker.py +2 -2
- package/hooks/blocking/test_code_rules_enforcer.py +65 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_args_signature.py +256 -0
- package/hooks/blocking/test_code_rules_enforcer_ignored_must_check_return.py +256 -0
- package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +137 -1
- package/hooks/blocking/test_md_to_html_blocker.py +38 -0
- package/hooks/hooks_constants/blocking_check_limits.py +2 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +15 -1
- package/hooks/hooks_constants/md_to_html_blocker_constants.py +1 -1
- package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +11 -4
- package/package.json +2 -1
- package/skills/bugteam/reference/teardown-publish-permissions.md +7 -2
|
@@ -26,7 +26,7 @@ TEST_FILE_PATH = "src/app/test_feature.py"
|
|
|
26
26
|
CONFIG_FILE_PATH = "src/config/settings.py"
|
|
27
27
|
WORKFLOW_FILE_PATH = "src/workflow/orders_tab.py"
|
|
28
28
|
HOOK_FILE_PATH = "/home/user/.claude/hooks/blocking/my_hook.py"
|
|
29
|
-
EXPECTED_PREFIX_GUIDANCE = "prefix with is_/has_/should_/can_"
|
|
29
|
+
EXPECTED_PREFIX_GUIDANCE = "prefix with is_/has_/should_/can_/was_/did_"
|
|
30
30
|
|
|
31
31
|
|
|
32
32
|
def _assert_flags_name(issues: list[str], name: str, line_number: int) -> None:
|
|
@@ -210,3 +210,139 @@ def test_should_allow_is_prefix_at_start_when_compound_word_follows() -> None:
|
|
|
210
210
|
assert issues == [], (
|
|
211
211
|
f"is_left_upper_snake has prefix at position 0, must pass, got: {issues}"
|
|
212
212
|
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
PARAMETER_PREFIX_GUIDANCE = "prefix with is_/has_/should_/can_/was_/did_"
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _assert_flags_parameter(issues: list[str], name: str, line_number: int) -> None:
|
|
219
|
+
expected = f"Line {line_number}: Boolean parameter {name} - {PARAMETER_PREFIX_GUIDANCE}"
|
|
220
|
+
assert expected in issues, f"expected {expected!r} in {issues!r}"
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_should_flag_bool_annotated_parameter_without_prefix() -> None:
|
|
224
|
+
source = "def run(dry_run: bool) -> None:\n print(dry_run)\n"
|
|
225
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
226
|
+
_assert_flags_parameter(issues, "dry_run", 1)
|
|
227
|
+
assert len(issues) == 1
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_should_flag_bool_default_parameter_without_annotation() -> None:
|
|
231
|
+
source = "def run(apply_historical_weight=False) -> None:\n print(apply_historical_weight)\n"
|
|
232
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
233
|
+
_assert_flags_parameter(issues, "apply_historical_weight", 1)
|
|
234
|
+
assert len(issues) == 1
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_should_flag_keyword_only_bool_parameter_without_prefix() -> None:
|
|
238
|
+
source = "def run(*, click_succeeded: bool = True) -> None:\n print(click_succeeded)\n"
|
|
239
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
240
|
+
_assert_flags_parameter(issues, "click_succeeded", 1)
|
|
241
|
+
assert len(issues) == 1
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def test_should_allow_is_prefixed_bool_parameter() -> None:
|
|
245
|
+
source = "def run(is_dry_run: bool) -> None:\n print(is_dry_run)\n"
|
|
246
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
247
|
+
assert issues == []
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def test_should_allow_was_prefixed_bool_parameter() -> None:
|
|
251
|
+
source = "def run(was_clicked: bool = False) -> None:\n print(was_clicked)\n"
|
|
252
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
253
|
+
assert issues == []
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def test_should_allow_did_prefixed_bool_parameter() -> None:
|
|
257
|
+
source = "def run(did_succeed: bool) -> None:\n print(did_succeed)\n"
|
|
258
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
259
|
+
assert issues == []
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def test_should_allow_was_prefixed_bool_assignment() -> None:
|
|
263
|
+
source = "def f() -> None:\n was_clicked = True\n"
|
|
264
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
265
|
+
assert issues == []
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_should_allow_did_prefixed_bool_assignment() -> None:
|
|
269
|
+
source = "def f() -> None:\n did_run = False\n"
|
|
270
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
271
|
+
assert issues == []
|
|
272
|
+
|
|
273
|
+
|
|
274
|
+
def test_should_skip_single_letter_bool_parameter() -> None:
|
|
275
|
+
source = "def run(x: bool) -> None:\n print(x)\n"
|
|
276
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
277
|
+
assert issues == []
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def test_should_skip_self_parameter_in_method() -> None:
|
|
281
|
+
source = (
|
|
282
|
+
"class Runner:\n"
|
|
283
|
+
" def run(self, enabled: bool) -> None:\n"
|
|
284
|
+
" print(self, enabled)\n"
|
|
285
|
+
)
|
|
286
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
287
|
+
_assert_flags_parameter(issues, "enabled", 2)
|
|
288
|
+
assert len(issues) == 1
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def test_should_not_flag_non_bool_parameter() -> None:
|
|
292
|
+
source = "def run(retries: int) -> None:\n print(retries)\n"
|
|
293
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
294
|
+
assert issues == []
|
|
295
|
+
|
|
296
|
+
|
|
297
|
+
def test_should_skip_bool_parameter_in_test_file() -> None:
|
|
298
|
+
source = "def run(dry_run: bool) -> None:\n print(dry_run)\n"
|
|
299
|
+
issues = check_boolean_naming(source, TEST_FILE_PATH)
|
|
300
|
+
assert issues == []
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def test_should_pair_positional_defaults_right_aligned() -> None:
|
|
304
|
+
source = (
|
|
305
|
+
"def run(name: str, verbose: bool = False) -> None:\n"
|
|
306
|
+
" print(name, verbose)\n"
|
|
307
|
+
)
|
|
308
|
+
issues = check_boolean_naming(source, PRODUCTION_FILE_PATH)
|
|
309
|
+
_assert_flags_parameter(issues, "verbose", 1)
|
|
310
|
+
assert len(issues) == 1
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
FULL_MODULE_WITH_TWO_UNPREFIXED_BOOL_PARAMETERS = (
|
|
314
|
+
"def pre_existing(verbose: bool) -> None:\n"
|
|
315
|
+
" print(verbose)\n"
|
|
316
|
+
"\n\n"
|
|
317
|
+
"def edited(detailed: bool) -> None:\n"
|
|
318
|
+
" print(detailed)\n"
|
|
319
|
+
)
|
|
320
|
+
PRE_EXISTING_BOOL_PARAMETER_LINE_NUMBER = 1
|
|
321
|
+
EDITED_BOOL_PARAMETER_LINE_NUMBER = 5
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
def test_should_flag_bool_parameter_on_changed_line() -> None:
|
|
325
|
+
issues = check_boolean_naming(
|
|
326
|
+
FULL_MODULE_WITH_TWO_UNPREFIXED_BOOL_PARAMETERS,
|
|
327
|
+
PRODUCTION_FILE_PATH,
|
|
328
|
+
{EDITED_BOOL_PARAMETER_LINE_NUMBER},
|
|
329
|
+
False,
|
|
330
|
+
)
|
|
331
|
+
_assert_flags_parameter(issues, "detailed", EDITED_BOOL_PARAMETER_LINE_NUMBER)
|
|
332
|
+
assert len(issues) == 1, (
|
|
333
|
+
"Only the bool parameter on the changed line must be flagged, got: "
|
|
334
|
+
f"{issues!r}"
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
|
|
338
|
+
def test_should_not_flag_pre_existing_bool_parameter_on_unchanged_line() -> None:
|
|
339
|
+
issues = check_boolean_naming(
|
|
340
|
+
FULL_MODULE_WITH_TWO_UNPREFIXED_BOOL_PARAMETERS,
|
|
341
|
+
PRODUCTION_FILE_PATH,
|
|
342
|
+
{EDITED_BOOL_PARAMETER_LINE_NUMBER},
|
|
343
|
+
False,
|
|
344
|
+
)
|
|
345
|
+
assert not any("verbose" in each_issue for each_issue in issues), (
|
|
346
|
+
"A pre-existing unprefixed bool parameter on an unedited line must not block "
|
|
347
|
+
f"the edit, got: {issues!r}"
|
|
348
|
+
)
|
|
@@ -252,6 +252,44 @@ def test_blocks_changelog_not_at_root():
|
|
|
252
252
|
assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
253
253
|
|
|
254
254
|
|
|
255
|
+
def test_passes_claude_md_at_root():
|
|
256
|
+
result = _run_hook(
|
|
257
|
+
"Write",
|
|
258
|
+
{"file_path": "CLAUDE.md", "content": "# CLAUDE"},
|
|
259
|
+
)
|
|
260
|
+
assert result.returncode == 0
|
|
261
|
+
assert result.stdout == ""
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def test_passes_agents_md_at_root():
|
|
265
|
+
result = _run_hook(
|
|
266
|
+
"Write",
|
|
267
|
+
{"file_path": "AGENTS.md", "content": "# AGENTS"},
|
|
268
|
+
)
|
|
269
|
+
assert result.returncode == 0
|
|
270
|
+
assert result.stdout == ""
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def test_blocks_claude_md_not_at_root():
|
|
274
|
+
result = _run_hook(
|
|
275
|
+
"Write",
|
|
276
|
+
{"file_path": "docs/CLAUDE.md", "content": "# CLAUDE"},
|
|
277
|
+
)
|
|
278
|
+
assert result.returncode == 0
|
|
279
|
+
output = json.loads(result.stdout)
|
|
280
|
+
assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def test_blocks_agents_md_not_at_root():
|
|
284
|
+
result = _run_hook(
|
|
285
|
+
"Write",
|
|
286
|
+
{"file_path": "sub/AGENTS.md", "content": "# AGENTS"},
|
|
287
|
+
)
|
|
288
|
+
assert result.returncode == 0
|
|
289
|
+
output = json.loads(result.stdout)
|
|
290
|
+
assert output["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
291
|
+
|
|
292
|
+
|
|
255
293
|
def test_blocks_relative_readme_when_cwd_is_not_repo_root():
|
|
256
294
|
sandbox_parent = _get_sandbox_parent_directory()
|
|
257
295
|
non_repo_cwd = os.path.join(sandbox_parent, "not-a-repo")
|
|
@@ -16,6 +16,8 @@ MAX_BARE_EXCEPT_ISSUES: int = 3
|
|
|
16
16
|
MAX_BOUNDARY_TYPE_ISSUES: int = 5
|
|
17
17
|
ALL_BANNED_PREFIX_NAMES: tuple[str, ...] = ("handle_", "process_", "manage_", "do_")
|
|
18
18
|
MAX_DOCSTRING_FORMAT_ISSUES: int = 5
|
|
19
|
+
MAX_DOCSTRING_ARGS_SIGNATURE_ISSUES: int = 5
|
|
20
|
+
MAX_IGNORED_MUST_CHECK_RETURN_ISSUES: int = 5
|
|
19
21
|
MAX_TYPE_ESCAPE_HATCH_ISSUES: int = 5
|
|
20
22
|
MAX_THIN_WRAPPER_ISSUES: int = 1
|
|
21
23
|
DOCSTRING_TRIVIAL_FUNCTION_BODY_LINE_LIMIT: int = 3
|
|
@@ -24,9 +24,23 @@ ALL_MIGRATION_PATH_PATTERNS = {"/migrations/", "\\migrations\\"}
|
|
|
24
24
|
ADVISORY_LINE_THRESHOLD_SOFT = 400
|
|
25
25
|
ADVISORY_LINE_THRESHOLD_HARD = 1000
|
|
26
26
|
|
|
27
|
-
ALL_BOOLEAN_NAME_PREFIXES: tuple[str, ...] = ("is_", "has_", "should_", "can_")
|
|
27
|
+
ALL_BOOLEAN_NAME_PREFIXES: tuple[str, ...] = ("is_", "has_", "should_", "can_", "was_", "did_")
|
|
28
28
|
UPPER_SNAKE_CONSTANT_PATTERN = re.compile(r"^[A-Z][A-Z0-9_]*$")
|
|
29
29
|
|
|
30
|
+
ALL_MUST_CHECK_RETURN_FUNCTION_NAMES: frozenset[str] = frozenset({"find_and_click", "write_outcome"})
|
|
31
|
+
|
|
32
|
+
DOCSTRING_ARG_ENTRY_PATTERN: re.Pattern[str] = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)\s*[:(]")
|
|
33
|
+
ALL_DOCSTRING_ARGS_SECTION_HEADERS: tuple[str, ...] = ("Args:", "Arguments:")
|
|
34
|
+
ALL_DOCSTRING_TERMINATING_SECTION_HEADERS: frozenset[str] = frozenset({
|
|
35
|
+
"Returns:",
|
|
36
|
+
"Yields:",
|
|
37
|
+
"Raises:",
|
|
38
|
+
"Examples:",
|
|
39
|
+
"Example:",
|
|
40
|
+
"Note:",
|
|
41
|
+
"Notes:",
|
|
42
|
+
})
|
|
43
|
+
|
|
30
44
|
|
|
31
45
|
TYPE_CHECKING_BLOCK_PATTERN = re.compile(r"^(?P<indent>\s*)if\s+(typing\.)?TYPE_CHECKING\s*:\s*$")
|
|
32
46
|
ALL_IMPORT_STATEMENT_PREFIXES: tuple[str, ...] = ("import ", "from ")
|
|
@@ -19,7 +19,7 @@ MINIMUM_SEGMENT_COUNT_TO_MATCH_INDICATOR: int = 4
|
|
|
19
19
|
ALL_EXEMPT_ANYWHERE_FILENAMES: tuple[str, ...] = ("SKILL.md",)
|
|
20
20
|
ALL_EXEMPT_PLUGIN_DIRECTORY_SEGMENTS: tuple[str, ...] = ("agents", "skills", "commands")
|
|
21
21
|
ALL_EXEMPT_HOME_RELATIVE_DIRECTORIES: tuple[str, ...] = ("SessionLog",)
|
|
22
|
-
ALL_EXEMPT_ROOT_FILENAMES: tuple[str, ...] = ("readme.md", "changelog.md")
|
|
22
|
+
ALL_EXEMPT_ROOT_FILENAMES: tuple[str, ...] = ("readme.md", "changelog.md", "claude.md", "agents.md")
|
|
23
23
|
REPO_ROOT_MARKER_NAME: str = ".git"
|
|
24
24
|
CLAUDE_DIRECTORY_NAME: str = ".claude"
|
|
25
25
|
PLUGIN_ROOT_MARKER_DIRECTORY_NAME: str = ".claude-plugin"
|
|
@@ -82,10 +82,17 @@ def test_exempt_home_relative_directories_include_session_log() -> None:
|
|
|
82
82
|
assert "ALL_EXEMPT_HOME_RELATIVE_DIRECTORIES" in constants_module.__all__
|
|
83
83
|
|
|
84
84
|
|
|
85
|
-
def
|
|
86
|
-
"""README.md and
|
|
87
|
-
every repo with a `.git` marker satisfies the root
|
|
88
|
-
|
|
85
|
+
def test_exempt_root_filenames_cover_readme_changelog_claude_and_agents() -> None:
|
|
86
|
+
"""README.md, CHANGELOG.md, CLAUDE.md, and AGENTS.md at a repo root are
|
|
87
|
+
universally exempt; every repo with a `.git` marker satisfies the root
|
|
88
|
+
check. CLAUDE.md and AGENTS.md are functional agent-instruction files that
|
|
89
|
+
Claude Code loads by name and must stay Markdown."""
|
|
90
|
+
assert constants_module.ALL_EXEMPT_ROOT_FILENAMES == (
|
|
91
|
+
"readme.md",
|
|
92
|
+
"changelog.md",
|
|
93
|
+
"claude.md",
|
|
94
|
+
"agents.md",
|
|
95
|
+
)
|
|
89
96
|
assert "ALL_EXEMPT_ROOT_FILENAMES" in constants_module.__all__
|
|
90
97
|
|
|
91
98
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-dev-env",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.50.0",
|
|
4
4
|
"description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"system-prompts/",
|
|
21
21
|
"scripts/",
|
|
22
22
|
"_shared/",
|
|
23
|
+
"audit-rubrics/",
|
|
23
24
|
"CLAUDE.md"
|
|
24
25
|
],
|
|
25
26
|
"keywords": [
|
|
@@ -2,7 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## Utility scripts (progressive disclosure)
|
|
4
4
|
|
|
5
|
-
Fragile or repeatable shell sequences
|
|
5
|
+
Fragile or repeatable shell sequences live in one of two shared script directories. Match the reference depth to the directory:
|
|
6
|
+
|
|
7
|
+
- **Package-root** [`_shared/pr-loop/scripts/`](../../../_shared/pr-loop/scripts/) holds `code_rules_gate.py`, `preflight.py`, `post_audit_thread.py`, and the permission helpers. Reference it as `../../../_shared/…` in markdown and `${CLAUDE_SKILL_DIR}/../../_shared/…` at runtime (resolves to `~/.claude/_shared/`). Inventory: [`../../../_shared/pr-loop/scripts/README.md`](../../../_shared/pr-loop/scripts/README.md).
|
|
8
|
+
- **Skill-tree** [`skills/_shared/pr-loop/scripts/`](../../_shared/pr-loop/scripts/) holds `teardown_worktrees.py`, `build_audit_prompt.py`, `build_fix_prompt.py`, `init_loop_state.py`, `write_audit_outcomes.py`, `write_fix_outcomes.py`, and `_path_resolver.py`. Reference it as `../../_shared/…` in markdown and `${CLAUDE_SKILL_DIR}/../_shared/…` at runtime (resolves to `~/.claude/skills/_shared/`).
|
|
9
|
+
|
|
10
|
+
Bugteam-specific scripts (e.g. revoke, see Step 5) live in the skill-local [`scripts/`](../scripts/) directory. Anthropic: [Progressive disclosure patterns](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices#progressive-disclosure-patterns) — utility scripts are **executed**, not loaded into context as primary reading. Bugteam-script inventory: [`../scripts/README.md`](../scripts/README.md). Gate-only merge-base / diff semantics: [`../../../_shared/pr-loop/code-rules-gate.md`](../../../_shared/pr-loop/code-rules-gate.md).
|
|
6
11
|
|
|
7
12
|
### Pre-flight (recommended before Step 0)
|
|
8
13
|
|
|
@@ -19,7 +24,7 @@ When the cycle exits (any reason), run these steps in order from **this** sessio
|
|
|
19
24
|
directory (Windows-safe `shutil.rmtree`):
|
|
20
25
|
|
|
21
26
|
```bash
|
|
22
|
-
python "${CLAUDE_SKILL_DIR}
|
|
27
|
+
python "${CLAUDE_SKILL_DIR}/../_shared/pr-loop/scripts/teardown_worktrees.py" \
|
|
23
28
|
--run-temp-dir "<run_temp_dir>" \
|
|
24
29
|
--all-pr-jsons '<json array of {number, owner, repo}>'
|
|
25
30
|
```
|