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.
Files changed (43) hide show
  1. package/audit-rubrics/category_rubrics/category-a-api-contracts.md +86 -0
  2. package/audit-rubrics/category_rubrics/category-b-selector-engine-compat.md +36 -0
  3. package/audit-rubrics/category_rubrics/category-c-resource-cleanup.md +35 -0
  4. package/audit-rubrics/category_rubrics/category-d-scoping-and-ordering.md +35 -0
  5. package/audit-rubrics/category_rubrics/category-e-dead-code.md +38 -0
  6. package/audit-rubrics/category_rubrics/category-f-silent-failures.md +38 -0
  7. package/audit-rubrics/category_rubrics/category-g-bounds-and-overflow.md +38 -0
  8. package/audit-rubrics/category_rubrics/category-h-security-boundaries.md +40 -0
  9. package/audit-rubrics/category_rubrics/category-i-concurrency.md +38 -0
  10. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +46 -0
  11. package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +59 -0
  12. package/audit-rubrics/category_rubrics/category-l-behavior-equivalence.md +45 -0
  13. package/audit-rubrics/category_rubrics/category-m-producer-consumer-cardinality.md +44 -0
  14. package/audit-rubrics/category_rubrics/category-n-test-name-scenario-verifier.md +45 -0
  15. package/audit-rubrics/prompts/category-a-api-contracts.md +399 -0
  16. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +401 -0
  17. package/audit-rubrics/prompts/category-c-resource-cleanup.md +420 -0
  18. package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +414 -0
  19. package/audit-rubrics/prompts/category-e-dead-code.md +420 -0
  20. package/audit-rubrics/prompts/category-f-silent-failures.md +420 -0
  21. package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +383 -0
  22. package/audit-rubrics/prompts/category-h-security-boundaries.md +423 -0
  23. package/audit-rubrics/prompts/category-i-concurrency.md +429 -0
  24. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +463 -0
  25. package/audit-rubrics/prompts/category-k-codebase-conflicts.md +328 -0
  26. package/audit-rubrics/prompts/category-l-behavior-equivalence.md +128 -0
  27. package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +129 -0
  28. package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +132 -0
  29. package/audit-rubrics/source-material-section-types.md +51 -0
  30. package/docs/CODE_RULES.md +6 -1
  31. package/hooks/blocking/code_rules_enforcer.py +323 -11
  32. package/hooks/blocking/md_to_html_blocker.py +2 -2
  33. package/hooks/blocking/test_code_rules_enforcer.py +65 -0
  34. package/hooks/blocking/test_code_rules_enforcer_docstring_args_signature.py +256 -0
  35. package/hooks/blocking/test_code_rules_enforcer_ignored_must_check_return.py +256 -0
  36. package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +137 -1
  37. package/hooks/blocking/test_md_to_html_blocker.py +38 -0
  38. package/hooks/hooks_constants/blocking_check_limits.py +2 -0
  39. package/hooks/hooks_constants/code_rules_enforcer_constants.py +15 -1
  40. package/hooks/hooks_constants/md_to_html_blocker_constants.py +1 -1
  41. package/hooks/hooks_constants/test_md_to_html_blocker_constants.py +11 -4
  42. package/package.json +2 -1
  43. 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 test_exempt_root_filenames_cover_readme_and_changelog() -> None:
86
- """README.md and CHANGELOG.md at a repo root are universally exempt;
87
- every repo with a `.git` marker satisfies the root check."""
88
- assert constants_module.ALL_EXEMPT_ROOT_FILENAMES == ("readme.md", "changelog.md")
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.49.0",
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 belong in `_shared/pr-loop/scripts/`. 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. Shared-script inventory: [`../../../_shared/pr-loop/scripts/README.md`](../../../_shared/pr-loop/scripts/README.md). 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).
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}/../../_shared/pr-loop/scripts/teardown_worktrees.py" \
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
  ```