claude-dev-env 1.72.0 → 1.74.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 (99) hide show
  1. package/CLAUDE.md +2 -0
  2. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +2 -2
  3. package/bin/install.mjs +73 -5
  4. package/bin/install.test.mjs +360 -4
  5. package/hooks/blocking/CLAUDE.md +6 -1
  6. package/hooks/blocking/block_main_commit.py +14 -0
  7. package/hooks/blocking/bot_mention_comment_blocker.py +7 -0
  8. package/hooks/blocking/claude_md_orphan_file_blocker.py +19 -48
  9. package/hooks/blocking/code_rules_dead_config_field.py +69 -56
  10. package/hooks/blocking/code_rules_docstrings.py +839 -0
  11. package/hooks/blocking/code_rules_enforcer.py +38 -0
  12. package/hooks/blocking/code_rules_shared.py +19 -0
  13. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +426 -0
  14. package/hooks/blocking/convergence_gate_blocker.py +17 -3
  15. package/hooks/blocking/destructive_command_blocker.py +7 -0
  16. package/hooks/blocking/docstring_rule_gate_count_blocker.py +321 -0
  17. package/hooks/blocking/gh_body_arg_blocker.py +8 -0
  18. package/hooks/blocking/gh_pr_author_enforcer.py +7 -0
  19. package/hooks/blocking/hedging_language_blocker.py +16 -10
  20. package/hooks/blocking/hook_prose_detector_consistency.py +7 -0
  21. package/hooks/blocking/intent_only_ending_blocker.py +17 -11
  22. package/hooks/blocking/md_to_html_blocker.py +17 -10
  23. package/hooks/blocking/open_questions_in_plans_blocker.py +15 -8
  24. package/hooks/blocking/package_inventory_stale_blocker.py +398 -0
  25. package/hooks/blocking/plain_language_blocker.py +57 -16
  26. package/hooks/blocking/pr_converge_bugteam_enforcer.py +11 -5
  27. package/hooks/blocking/pr_description_enforcer.py +6 -0
  28. package/hooks/blocking/pre_tool_use_dispatcher.py +545 -0
  29. package/hooks/blocking/precommit_code_rules_gate.py +10 -1
  30. package/hooks/blocking/pytest_testpaths_orphan_blocker.py +366 -0
  31. package/hooks/blocking/question_to_user_enforcer.py +18 -12
  32. package/hooks/blocking/send_user_file_open_locally_blocker.py +70 -0
  33. package/hooks/blocking/sensitive_file_protector.py +15 -1
  34. package/hooks/blocking/session_handoff_blocker.py +14 -8
  35. package/hooks/blocking/state_description_blocker.py +81 -36
  36. package/hooks/blocking/subprocess_budget_completeness.py +9 -3
  37. package/hooks/blocking/tdd_enforcer.py +6 -0
  38. package/hooks/blocking/test_code_rules_enforcer_dead_config_field.py +81 -0
  39. package/hooks/blocking/test_code_rules_enforcer_docstring_inline_literal_claim.py +93 -0
  40. package/hooks/blocking/test_code_rules_enforcer_docstring_returns_plural_cardinality.py +207 -0
  41. package/hooks/blocking/test_code_rules_enforcer_docstring_step_dispatch.py +262 -0
  42. package/hooks/blocking/test_code_rules_enforcer_docstring_undefined_constant.py +253 -0
  43. package/hooks/blocking/test_code_rules_enforcer_docstring_unguarded_payload.py +188 -0
  44. package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +279 -0
  45. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +501 -0
  46. package/hooks/blocking/test_docstring_rule_gate_count_blocker.py +203 -0
  47. package/hooks/blocking/test_hook_block_logger_coverage.py +53 -0
  48. package/hooks/blocking/test_package_inventory_stale_blocker.py +329 -0
  49. package/hooks/blocking/test_plain_language_blocker.py +36 -0
  50. package/hooks/blocking/test_pre_tool_use_dispatcher.py +816 -0
  51. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +341 -0
  52. package/hooks/blocking/test_pytest_testpaths_orphan_blocker.py +247 -0
  53. package/hooks/blocking/test_send_user_file_open_locally_blocker.py +114 -0
  54. package/hooks/blocking/test_shared_stdin_adoption.py +208 -0
  55. package/hooks/blocking/test_state_description_blocker.py +41 -0
  56. package/hooks/blocking/test_verdict_directory_write_blocker.py +49 -0
  57. package/hooks/blocking/test_workflow_substitution_slot_blocker.py +4 -19
  58. package/hooks/blocking/verdict_directory_write_blocker.py +21 -7
  59. package/hooks/blocking/verified_commit_gate.py +11 -0
  60. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +16 -1
  61. package/hooks/blocking/windows_rmtree_blocker.py +7 -0
  62. package/hooks/blocking/workflow_substitution_slot_blocker.py +10 -5
  63. package/hooks/blocking/write_existing_file_blocker.py +16 -1
  64. package/hooks/hooks.json +19 -79
  65. package/hooks/hooks_constants/CLAUDE.md +7 -1
  66. package/hooks/hooks_constants/blocking_check_limits.py +74 -0
  67. package/hooks/hooks_constants/code_rules_enforcer_constants.py +9 -0
  68. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +45 -0
  69. package/hooks/hooks_constants/dead_config_field_constants.py +5 -5
  70. package/hooks/hooks_constants/docstring_rule_gate_count_blocker_constants.py +90 -0
  71. package/hooks/hooks_constants/hook_block_logger.py +59 -0
  72. package/hooks/hooks_constants/multi_edit_reconstruction.py +56 -0
  73. package/hooks/hooks_constants/mypy_validator_cache_constants.py +36 -0
  74. package/hooks/hooks_constants/package_inventory_stale_blocker_constants.py +111 -0
  75. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +68 -0
  76. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +143 -0
  77. package/hooks/hooks_constants/pytest_testpaths_orphan_blocker_constants.py +79 -0
  78. package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +18 -0
  79. package/hooks/hooks_constants/test_dispatcher_constants_docstrings.py +44 -0
  80. package/hooks/hooks_constants/test_hook_block_logger.py +159 -0
  81. package/hooks/lifecycle/config_change_guard.py +12 -0
  82. package/hooks/lifecycle/test_config_change_guard.py +23 -0
  83. package/hooks/validation/hook_format_validator.py +13 -0
  84. package/hooks/validation/mypy_validator.py +245 -18
  85. package/hooks/validation/post_tool_use_dispatcher.py +344 -0
  86. package/hooks/validation/test_hook_format_validator.py +64 -0
  87. package/hooks/validation/test_mypy_validator.py +206 -1
  88. package/hooks/validation/test_post_tool_use_dispatcher.py +610 -0
  89. package/hooks/workflow/test_auto_formatter.py +10 -9
  90. package/package.json +1 -1
  91. package/rules/CLAUDE.md +1 -0
  92. package/rules/docstring-prose-matches-implementation.md +4 -2
  93. package/rules/package-inventory-stale-entry.md +24 -0
  94. package/skills/autoconverge/SKILL.md +111 -1
  95. package/skills/autoconverge/workflow/converge.contract.test.mjs +106 -0
  96. package/skills/autoconverge/workflow/converge.mjs +29 -3
  97. package/skills/autoconverge/workflow/converge.path-aware.test.mjs +47 -0
  98. package/skills/autoconverge/workflow/converge_multi.mjs +161 -0
  99. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +100 -0
@@ -8,6 +8,13 @@ Exemptions: Jupyter notebooks (.ipynb) and files in ~/.claude/hooks/ (standalone
8
8
  import json
9
9
  import os
10
10
  import sys
11
+ from pathlib import Path
12
+
13
+ _hooks_dir = str(Path(__file__).resolve().parent.parent)
14
+ if _hooks_dir not in sys.path:
15
+ sys.path.insert(0, _hooks_dir)
16
+
17
+ from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
11
18
 
12
19
  JUPYTER_EXTENSION = ".ipynb"
13
20
  HOOKS_DIRECTORY = os.path.normpath(os.path.expanduser("~/.claude/hooks"))
@@ -48,13 +55,21 @@ def main() -> None:
48
55
  if not os.path.exists(target_file_path):
49
56
  sys.exit(0)
50
57
 
58
+ deny_reason = f"BLOCKED: Write on existing file {target_file_path}. Use Edit tool instead."
51
59
  denial = {
52
60
  "hookSpecificOutput": {
53
61
  "hookEventName": "PreToolUse",
54
62
  "permissionDecision": "deny",
55
- "permissionDecisionReason": f"BLOCKED: Write on existing file {target_file_path}. Use Edit tool instead.",
63
+ "permissionDecisionReason": deny_reason,
56
64
  }
57
65
  }
66
+ log_hook_block(
67
+ calling_hook_name="write_existing_file_blocker.py",
68
+ hook_event="PreToolUse",
69
+ block_reason=deny_reason,
70
+ tool_name="Write",
71
+ offending_input_preview=target_file_path,
72
+ )
58
73
  print(json.dumps(denial))
59
74
  sys.exit(0)
60
75
 
package/hooks/hooks.json CHANGED
@@ -5,60 +5,10 @@
5
5
  {
6
6
  "matcher": "Write|Edit",
7
7
  "hooks": [
8
- {
9
- "type": "command",
10
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/write_existing_file_blocker.py",
11
- "timeout": 10
12
- },
13
- {
14
- "type": "command",
15
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/sensitive_file_protector.py",
16
- "timeout": 10
17
- },
18
- {
19
- "type": "command",
20
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/validation/hook_format_validator.py",
21
- "timeout": 15
22
- },
23
- {
24
- "type": "command",
25
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/code_rules_enforcer.py",
26
- "timeout": 30
27
- },
28
8
  {
29
9
  "type": "command",
30
10
  "command": "python3 -c \"import sys; sys.path.insert(0, r'${CLAUDE_PLUGIN_ROOT}/hooks'); from validators.run_all_validators import main; sys.exit(main())\"",
31
11
  "timeout": 15
32
- },
33
- {
34
- "type": "command",
35
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/tdd_enforcer.py",
36
- "timeout": 10
37
- },
38
- {
39
- "type": "command",
40
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/windows_rmtree_blocker.py",
41
- "timeout": 10
42
- },
43
- {
44
- "type": "command",
45
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/state_description_blocker.py",
46
- "timeout": 10
47
- },
48
- {
49
- "type": "command",
50
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/subprocess_budget_completeness.py",
51
- "timeout": 10
52
- },
53
- {
54
- "type": "command",
55
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/hook_prose_detector_consistency.py",
56
- "timeout": 10
57
- },
58
- {
59
- "type": "command",
60
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/verified_commit_message_accuracy_blocker.py",
61
- "timeout": 10
62
12
  }
63
13
  ]
64
14
  },
@@ -67,23 +17,8 @@
67
17
  "hooks": [
68
18
  {
69
19
  "type": "command",
70
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/workflow_substitution_slot_blocker.py",
71
- "timeout": 10
72
- },
73
- {
74
- "type": "command",
75
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/claude_md_orphan_file_blocker.py",
76
- "timeout": 10
77
- },
78
- {
79
- "type": "command",
80
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/open_questions_in_plans_blocker.py",
81
- "timeout": 10
82
- },
83
- {
84
- "type": "command",
85
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/plain_language_blocker.py",
86
- "timeout": 10
20
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/pre_tool_use_dispatcher.py",
21
+ "timeout": 60
87
22
  }
88
23
  ]
89
24
  },
@@ -199,6 +134,11 @@
199
134
  "type": "command",
200
135
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/pr_converge_bugteam_enforcer.py",
201
136
  "timeout": 10
137
+ },
138
+ {
139
+ "type": "command",
140
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/code_verifier_spawn_preflight_gate.py",
141
+ "timeout": 30
202
142
  }
203
143
  ]
204
144
  },
@@ -221,6 +161,16 @@
221
161
  "timeout": 10
222
162
  }
223
163
  ]
164
+ },
165
+ {
166
+ "matcher": "SendUserFile",
167
+ "hooks": [
168
+ {
169
+ "type": "command",
170
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/send_user_file_open_locally_blocker.py",
171
+ "timeout": 10
172
+ }
173
+ ]
224
174
  }
225
175
  ],
226
176
  "SessionStart": [
@@ -324,18 +274,8 @@
324
274
  "hooks": [
325
275
  {
326
276
  "type": "command",
327
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/validation/mypy_validator.py",
328
- "timeout": 30
329
- },
330
- {
331
- "type": "command",
332
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/workflow/auto_formatter.py",
333
- "timeout": 30
334
- },
335
- {
336
- "type": "command",
337
- "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/workflow/doc_gist_auto_publish.py ${CLAUDE_PLUGIN_ROOT}",
338
- "timeout": 60
277
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/validation/post_tool_use_dispatcher.py",
278
+ "timeout": 180
339
279
  }
340
280
  ]
341
281
  },
@@ -14,13 +14,15 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
14
14
  | `claude_md_orphan_file_blocker_constants.py` | Table patterns, file extensions, scan budget, and block-message text for the CLAUDE.md orphan-file blocker |
15
15
  | `code_rules_enforcer_constants.py` | File-extension sets, test-path patterns, advisory line thresholds, boolean-name prefixes |
16
16
  | `code_rules_path_utils_constants.py` | Path-matching helpers used by the code-rules check modules |
17
+ | `code_verifier_spawn_preflight_gate_constants.py` | Subagent type, merge-tree command flags, timeouts, and deny-message text for the code-verifier spawn pre-flight gate |
17
18
  | `convergence_branch_constants.py` | Branch and worktree naming patterns for the convergence gate |
18
19
  | `dead_argparse_argument_constants.py` | Patterns for detecting unused argparse arguments |
19
- | `dead_config_field_constants.py` | Patterns for detecting unused dataclass config fields |
20
+ | `dead_config_field_constants.py` | Patterns for detecting unused `*Config` / `*Selectors` dataclass fields |
20
21
  | `dead_dataclass_field_constants.py` | Patterns for detecting unused dataclass fields |
21
22
  | `dead_module_constant_constants.py` | Patterns for detecting unexported `UPPER_SNAKE` constants in `*_constants.py` modules |
22
23
  | `destructive_command_segment_constants.py` | The list of destructive shell command patterns the blocker matches |
23
24
  | `doc_gist_auto_publish_constants.py` | Sentinel marker and URL patterns for the doc-gist auto-publish hook |
25
+ | `docstring_rule_gate_count_blocker_constants.py` | Target rule basename, spelled-out-number lookup, count-clause and `check_*` validator patterns, and block-message text for the docstring-rule gate-count staleness blocker |
24
26
  | `duplicate_function_body_constants.py` | Hashing and comparison config for the duplicate-body check |
25
27
  | `dynamic_stderr_handler.py` | `DynamicStderrHandler` — a logging handler that resolves `sys.stderr` at emit time (for testability) |
26
28
  | `gh_pr_author_swap_constants.py` | Constants for the PR-author swap enforcement hooks |
@@ -31,8 +33,10 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
31
33
  | `inline_tuple_string_magic_constants.py` | Patterns for detecting magic strings in inline tuple literals |
32
34
  | `md_to_html_blocker_constants.py` | Path exemptions and trigger patterns for the markdown-to-html blocker |
33
35
  | `messages.py` | Short user-facing notice strings shown when a Stop hook redirects agent behavior |
36
+ | `multi_edit_reconstruction.py` | `apply_edits()` / `edits_for_tool()` — shared helpers that reconstruct the post-edit content of an Edit or MultiEdit, imported by the blockers that judge post-edit content |
34
37
  | `open_questions_in_plans_blocker_constants.py` | Patterns for detecting unresolved open questions in plan documents |
35
38
  | `orphan_css_class_constants.py` | Scan radius and selector patterns for the orphan-CSS-class check |
39
+ | `package_inventory_stale_blocker_constants.py` | Inventory document names, production code extensions, backtick token pattern, smallest inventory size, exempt names, scan budget, and block-message text for the package-inventory stale-entry blocker |
36
40
  | `path_rewriter_constants.py` | Path rewriting patterns for the Everything-search path rewriter |
37
41
  | `plain_language_blocker_constants.py` | The list of heavy words and their everyday replacements |
38
42
  | `pr_converge_bugteam_enforcer_constants.py` | State keys and timing config for the bugteam-parallel enforcer |
@@ -41,6 +45,8 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
41
45
  | `pre_tool_use_stdin.py` | `read_hook_input_dictionary_from_stdin()` — shared stdin parser for PreToolUse hooks |
42
46
  | `precommit_code_rules_gate_constants.py` | Scope argument and exit-code constants for the precommit gate |
43
47
  | `project_paths_reader.py` | Loads `~/.claude/project-paths.json` — the per-user project-path registry |
48
+ | `pytest_testpaths_orphan_blocker_constants.py` | Marker filename, section and key names, test-file pattern, search budget, and block-message text for the pytest unregistered-test-directory blocker |
49
+ | `send_user_file_open_locally_blocker_constants.py` | Tool name, proactive status, and the block message for the open-locally attach blocker |
44
50
  | `session_env_cleanup_constants.py` | Stale-age threshold and directory names for the session-env cleanup hook |
45
51
  | `session_handoff_blocker_constants.py` | Trigger phrases for the session-handoff blocker |
46
52
  | `setup_project_paths_constants.py` | Encoding policy, BOM marker, and registry meta-key used across multiple hooks |
@@ -30,8 +30,61 @@ DOCSTRING_TRIVIAL_FUNCTION_BODY_LINE_LIMIT: int = 3
30
30
  MAX_DOCSTRING_FALLBACK_BRANCH_ISSUES: int = 3
31
31
  DOCSTRING_FALLBACK_BRANCH_MINIMUM_ROUTE_COUNT: int = 2
32
32
  MAX_DOCSTRING_NO_CONSUMER_CLAIM_ISSUES: int = 3
33
+ MAX_DOCSTRING_UNGUARDED_PAYLOAD_CLAIM_ISSUES: int = 3
33
34
  MAX_STALE_TEST_NAME_TARGET_ISSUES: int = 3
34
35
  STALE_TEST_NAME_MINIMUM_SHARED_TOKEN_COUNT: int = 2
36
+ MAX_MODULE_DOCSTRING_CHECK_ROSTER_ISSUES: int = 5
37
+ MINIMUM_PUBLIC_CHECKS_FOR_MODULE_DOCSTRING_ROSTER: int = 2
38
+ MAX_DOCSTRING_TUPLE_ENUMERATION_ISSUES: int = 5
39
+ MINIMUM_TUPLE_MEMBERS_FOR_DOCSTRING_ENUMERATION: int = 2
40
+ MAX_DOCSTRING_STEP_DISPATCH_ISSUES: int = 5
41
+ MINIMUM_NAMED_LINEAR_STEPS_FOR_DISPATCH_CHECK: int = 2
42
+ MINIMUM_TOKENS_FOR_DISPATCH_CALLEE: int = 2
43
+ MAX_DOCSTRING_UNDEFINED_CONSTANT_ISSUES: int = 3
44
+ MAX_DOCSTRING_RETURNS_PLURAL_CARDINALITY_ISSUES: int = 5
45
+ SINGLE_DICT_KEY_COUNT_FOR_PLURAL_CARDINALITY_DRIFT: int = 1
46
+ ALL_NAMING_CONVENTION_DESCRIPTOR_TOKENS: frozenset[str] = frozenset(
47
+ {
48
+ "UPPER_SNAKE_CASE",
49
+ "SCREAMING_SNAKE_CASE",
50
+ "UPPER_CASE",
51
+ "SNAKE_CASE",
52
+ "CAMEL_CASE",
53
+ "PASCAL_CASE",
54
+ "KEBAB_CASE",
55
+ "TITLE_CASE",
56
+ }
57
+ )
58
+ ALL_DOCSTRING_NON_CONSTANT_REFERENCE_MARKERS: frozenset[str] = frozenset(
59
+ {
60
+ "rule",
61
+ "rules",
62
+ "doc",
63
+ "docs",
64
+ "document",
65
+ "file",
66
+ "env",
67
+ "environment",
68
+ "variable",
69
+ "set",
70
+ "reads",
71
+ "read",
72
+ "per",
73
+ "follows",
74
+ "following",
75
+ "see",
76
+ }
77
+ )
78
+ ALL_DOCSTRING_FILE_REFERENCE_SUFFIXES: tuple[str, ...] = (
79
+ ".md",
80
+ ".py",
81
+ ".txt",
82
+ ".json",
83
+ )
84
+ DOCSTRING_REFERENCE_MARKER_WINDOW: int = 2
85
+ ALL_GENERIC_CHECK_NAME_TOKENS: frozenset[str] = frozenset(
86
+ {"check", "checks", "test", "tests", "in", "for", "and", "the"}
87
+ )
35
88
 
36
89
  ALL_DOCSTRING_NO_CONSUMER_CLAIM_PHRASES: tuple[str, ...] = (
37
90
  "no consumer reads",
@@ -44,6 +97,27 @@ ALL_DOCSTRING_NO_CONSUMER_CLAIM_PHRASES: tuple[str, ...] = (
44
97
  "not yet read by any consumer",
45
98
  )
46
99
 
100
+ ALL_DOCSTRING_GUARDED_FAILURE_CLAIM_PHRASES: tuple[str, ...] = (
101
+ "malformed payload resolves to none",
102
+ "malformed payload returns none",
103
+ "malformed response resolves to none",
104
+ "malformed response returns none",
105
+ "bad payload resolves to none",
106
+ "invalid payload resolves to none",
107
+ "malformed payload yields none",
108
+ )
109
+
110
+ MAX_DOCSTRING_INLINE_LITERAL_CLAIM_ISSUES: int = 3
111
+ ALL_DOCSTRING_NO_INLINE_LITERAL_CLAIM_PHRASES: tuple[str, ...] = (
112
+ "no literals appear inline",
113
+ "no literal appears inline",
114
+ "no literals inline",
115
+ "no inline literals",
116
+ "no string literals appear inline",
117
+ "without any inline literals",
118
+ "no hardcoded literals remain",
119
+ )
120
+
47
121
  ALL_DOCSTRING_EXCLUSIVE_SCOPE_PHRASES: tuple[str, ...] = (
48
122
  "only when",
49
123
  "only if",
@@ -17,6 +17,10 @@ ALL_JAVASCRIPT_EXTENSIONS = {".js", ".ts", ".tsx", ".jsx"}
17
17
  ALL_CODE_EXTENSIONS = ALL_PYTHON_EXTENSIONS | ALL_JAVASCRIPT_EXTENSIONS
18
18
 
19
19
  ALL_TEST_PATH_PATTERNS = {"test_", "_test.", ".test.", ".spec.", "/tests/", "\\tests\\", "/tests.py", "\\tests.py"}
20
+ STRICT_TEST_FILE_BASENAME_PATTERN: re.Pattern[str] = re.compile(
21
+ r"^(test_.*|.*_test|.*\.test|.*\.spec)\.[^.]+$|^conftest\.py$"
22
+ )
23
+ ALL_STRICT_TEST_DIRECTORY_SEGMENTS: tuple[str, ...] = ("/tests/",)
20
24
  ALL_ROOT_ANCHORED_EPHEMERAL_DIRECTORIES: tuple[str, str] = ("/tmp", "/temp")
21
25
  CLAUDE_JOB_DIR_ENVIRONMENT_VARIABLE_NAME: str = "CLAUDE_JOB_DIR"
22
26
  CLAUDE_JOB_DIR_SCRATCH_SUBDIRECTORY: str = "tmp"
@@ -38,6 +42,11 @@ UPPER_SNAKE_CONSTANT_PATTERN = re.compile(r"^[A-Z][A-Z0-9_]*$")
38
42
  ALL_MUST_CHECK_RETURN_FUNCTION_NAMES: frozenset[str] = frozenset({"find_and_click", "write_outcome"})
39
43
 
40
44
  DOCSTRING_ARG_ENTRY_PATTERN: re.Pattern[str] = re.compile(r"^([A-Za-z_][A-Za-z0-9_]*)\s*[:(]")
45
+ DOCSTRING_PLURAL_FAMILY_STOP_PATTERN: re.Pattern[str] = re.compile(
46
+ r"\bthe\s+([a-z][a-z]+)\s+stops\b"
47
+ )
48
+ INLINE_CODE_TOKEN_PATTERN: re.Pattern[str] = re.compile(r"``?(\.?[A-Za-z_][A-Za-z0-9_.]*)``?")
49
+ IDENTIFIER_SHAPED_TUPLE_MEMBER_PATTERN: re.Pattern[str] = re.compile(r"^\.?[A-Za-z_][A-Za-z0-9_]*$")
41
50
  ALL_DOCSTRING_ARGS_SECTION_HEADERS: tuple[str, ...] = ("Args:", "Arguments:")
42
51
  ALL_DOCSTRING_TERMINATING_SECTION_HEADERS: frozenset[str] = frozenset({
43
52
  "Returns:",
@@ -0,0 +1,45 @@
1
+ """Configuration constants for the code_verifier_spawn_preflight_gate hook.
2
+
3
+ The gate denies an ``Agent`` spawn whose ``subagent_type`` is ``code-verifier``
4
+ when the branch carries a merge conflict against its base ref or a CODE_RULES
5
+ violation on a line added in the uncommitted working tree. It runs two
6
+ pre-flight checks before the expensive verification spawn and addresses its
7
+ deny reason to the spawning agent so that agent fixes the named issues and
8
+ re-spawns. Every literal the hook body reads lives here; the hook imports
9
+ ``AGENT_TOOL_NAME`` from ``pr_converge_bugteam_enforcer_constants`` rather than
10
+ redefining it.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from pathlib import Path
16
+
17
+ CODE_VERIFIER_SUBAGENT_TYPE: str = "code-verifier"
18
+
19
+ ALL_MERGE_TREE_COMMAND_FLAGS: tuple[str, ...] = (
20
+ "merge-tree",
21
+ "--write-tree",
22
+ "--name-only",
23
+ )
24
+ MERGE_TREE_CONFLICT_EXIT_CODE: int = 1
25
+ MERGE_TREE_CLEAN_EXIT_CODE: int = 0
26
+ MERGE_TREE_TIMEOUT_SECONDS: int = 30
27
+
28
+ ALL_NAME_ONLY_WORKTREE_DIFF_FLAGS: tuple[str, ...] = (
29
+ "-c",
30
+ "core.quotePath=false",
31
+ "diff",
32
+ "--name-only",
33
+ "--no-renames",
34
+ )
35
+ ALL_UNIFIED_ZERO_DIFF_FLAGS: tuple[str, ...] = ("diff", "--unified=0")
36
+
37
+ DENY_REASON_LEAD: str = (
38
+ "BLOCKED [code-verifier-spawn-preflight]: an Agent spawn with subagent_type "
39
+ "code-verifier is blocked because the branch is not in a committable state. "
40
+ "Fix these, then re-spawn the code-verifier:"
41
+ )
42
+ MERGE_CONFLICT_SECTION_HEADER: str = "Merge conflicts vs {base_ref}:"
43
+ CODE_RULES_SECTION_HEADER: str = "CODE_RULES violations on changed lines:"
44
+
45
+ GATE_SCRIPTS_RELATIVE_PATH: Path = Path("_shared") / "pr-loop" / "scripts"
@@ -16,18 +16,18 @@ from hooks_constants.dead_module_constant_constants import (
16
16
  PYTHON_SOURCE_SUFFIX,
17
17
  )
18
18
 
19
- CONFIG_CLASS_NAME_SUFFIX: str = "Config"
19
+ ALL_CONFIG_CLASS_NAME_SUFFIXES: tuple[str, ...] = ("Config", "Selectors")
20
20
  DATACLASSES_MODULE_NAME: str = "dataclasses"
21
21
  MAX_DEAD_CONFIG_FIELD_ISSUES: int = 25
22
22
  DEAD_CONFIG_FIELD_GUIDANCE: str = (
23
- "config dataclass field is defined but read by no production module in the"
24
- " enclosing package tree - remove the dead field, or read it where the value"
25
- " is needed (CODE_RULES §9.8)"
23
+ "config or selectors dataclass field is defined but read by no production"
24
+ " module in the enclosing package tree - remove the dead field, or read it"
25
+ " where the value is needed (CODE_RULES §9.8)"
26
26
  )
27
27
 
28
28
  __all__ = [
29
+ "ALL_CONFIG_CLASS_NAME_SUFFIXES",
29
30
  "ALL_REFLECTIVE_FIELD_CONSUMER_NAMES",
30
- "CONFIG_CLASS_NAME_SUFFIX",
31
31
  "CONFIG_DIRECTORY_SEGMENT",
32
32
  "DATACLASSES_MODULE_NAME",
33
33
  "DEAD_CONFIG_FIELD_GUIDANCE",
@@ -0,0 +1,90 @@
1
+ """Constants for the docstring-rule gate-count staleness blocker.
2
+
3
+ The rule file ``docstring-prose-matches-implementation.md`` enumerates the
4
+ ``check_docstring_*`` gate validators that cover deterministic slices of docstring
5
+ prose, both as a spelled-out count ("Three more gate validators", "four gated
6
+ slices") and as a backticked list of the validator names. When a new gate
7
+ validator is registered but the count word is left unchanged, the rule's stated
8
+ count drifts from the validators it actually names — the same companion-doc drift
9
+ the rule itself governs. This module holds the target rule basename, the
10
+ spelled-out-number lookup, the code-fence pattern that marks lines to skip, the
11
+ patterns that find the "<count> more gate validators" and "<count> gated slices"
12
+ count clauses and the backticked ``check_*`` validator names, the args-gate name,
13
+ the issue budget, and the block-message text the hook emits.
14
+ """
15
+
16
+ import re
17
+
18
+ __all__ = [
19
+ "TARGET_RULE_BASENAME",
20
+ "ALL_NUMBER_WORDS_BY_VALUE",
21
+ "CODE_FENCE_PATTERN",
22
+ "FREE_FORM_GATE_COUNT_PATTERN",
23
+ "TOTAL_GATED_SLICE_COUNT_PATTERN",
24
+ "GATE_VALIDATOR_NAME_PATTERN",
25
+ "ARGS_GATE_VALIDATOR_NAME",
26
+ "MAX_GATE_COUNT_ISSUES",
27
+ "GATE_COUNT_MESSAGE_TEMPLATE",
28
+ "GATE_COUNT_SYSTEM_MESSAGE",
29
+ "GATE_COUNT_ADDITIONAL_CONTEXT",
30
+ ]
31
+
32
+ TARGET_RULE_BASENAME: str = "docstring-prose-matches-implementation.md"
33
+
34
+ ALL_NUMBER_WORDS_BY_VALUE: dict[str, int] = {
35
+ "zero": 0,
36
+ "one": 1,
37
+ "two": 2,
38
+ "three": 3,
39
+ "four": 4,
40
+ "five": 5,
41
+ "six": 6,
42
+ "seven": 7,
43
+ "eight": 8,
44
+ "nine": 9,
45
+ "ten": 10,
46
+ }
47
+
48
+ CODE_FENCE_PATTERN: re.Pattern[str] = re.compile(r"^\s*(?:```|~~~)")
49
+
50
+ FREE_FORM_GATE_COUNT_PATTERN: re.Pattern[str] = re.compile(
51
+ r"\b([A-Za-z]+)\s+more\s+gate\s+validators\b",
52
+ re.IGNORECASE,
53
+ )
54
+
55
+ TOTAL_GATED_SLICE_COUNT_PATTERN: re.Pattern[str] = re.compile(
56
+ r"\b([A-Za-z]+)\s+gated\s+slices\b",
57
+ re.IGNORECASE,
58
+ )
59
+
60
+ GATE_VALIDATOR_NAME_PATTERN: re.Pattern[str] = re.compile(r"`(check_[A-Za-z0-9_]+)`")
61
+
62
+ ARGS_GATE_VALIDATOR_NAME: str = "check_docstring_args_match_signature"
63
+
64
+ MAX_GATE_COUNT_ISSUES: int = 4
65
+
66
+ GATE_COUNT_MESSAGE_TEMPLATE: str = (
67
+ "{rule_basename} states '{stated_phrase}' ({stated_count}) but names "
68
+ "{named_count} distinct free-form gate validator(s) ({named_validators}). The "
69
+ "rule's spelled-out gate count drifts from the validators it enumerates — the "
70
+ "companion-doc-vs-implementation drift this rule governs. Update the count "
71
+ "word to {named_count} and the '... gated slices' total to {total_count} in "
72
+ "this same change, and name every gate validator the prose counts."
73
+ )
74
+
75
+ GATE_COUNT_SYSTEM_MESSAGE: str = (
76
+ "Gate-validator count in docstring-prose-matches-implementation.md drifted "
77
+ "from the validators it names - update the count word in this same change"
78
+ )
79
+
80
+ GATE_COUNT_ADDITIONAL_CONTEXT: str = (
81
+ "The rule docstring-prose-matches-implementation.md states a spelled-out "
82
+ "count of free-form docstring gate validators ('Four more gate validators') "
83
+ "and a total ('five gated slices'), then names each validator in backticks "
84
+ "(`check_docstring_fallback_branch_coverage`, ...). When a new "
85
+ "`check_docstring_*` gate is added, name it in the prose and bump both count "
86
+ "words: the 'N more gate validators' count equals the number of distinct "
87
+ "free-form validators named after it, and the 'M gated slices' total equals "
88
+ "that count plus one for check_docstring_args_match_signature. Keep the count "
89
+ "words and the named-validator list in step in the same change."
90
+ )
@@ -0,0 +1,59 @@
1
+ """Shared fail-safe logger for hook block events.
2
+
3
+ Every blocking hook calls log_hook_block at the moment it decides to block,
4
+ so the user has a single log showing what tripped and why.
5
+ """
6
+
7
+ import datetime
8
+ import json
9
+ from pathlib import Path
10
+
11
+ _HOOK_BLOCKS_LOG_RELATIVE_PATH = ".claude/logs/hook-blocks.log"
12
+ _MAX_PREVIEW_LENGTH = 500
13
+
14
+
15
+ def log_hook_block(
16
+ calling_hook_name: str,
17
+ hook_event: str,
18
+ block_reason: str,
19
+ tool_name: str | None = None,
20
+ offending_input_preview: str | None = None,
21
+ ) -> None:
22
+ """Append one JSON record to the hook-blocks log for a block decision.
23
+
24
+ Creates the logs directory if absent. Skips logging when the home directory
25
+ cannot be resolved, and silently swallows all IO errors otherwise, so a
26
+ logging failure never changes a hook's decision.
27
+
28
+ Args:
29
+ calling_hook_name: The script basename of the hook that is blocking.
30
+ hook_event: The hook event type, e.g. ``PreToolUse`` or ``Stop``.
31
+ block_reason: The human-readable reason the hook is blocking.
32
+ tool_name: The Claude tool name when available, e.g. ``Bash``.
33
+ offending_input_preview: A short excerpt of the input that triggered
34
+ the block; truncated to 500 characters before writing.
35
+ """
36
+ try:
37
+ home_directory = Path.home()
38
+ except RuntimeError:
39
+ return
40
+
41
+ try:
42
+ log_path = home_directory / _HOOK_BLOCKS_LOG_RELATIVE_PATH
43
+ log_path.parent.mkdir(parents=True, exist_ok=True)
44
+
45
+ log_record: dict[str, str] = {
46
+ "timestamp": datetime.datetime.now().isoformat(),
47
+ "hook": calling_hook_name,
48
+ "event": hook_event,
49
+ "reason": block_reason,
50
+ }
51
+ if tool_name is not None:
52
+ log_record["tool"] = tool_name
53
+ if offending_input_preview is not None:
54
+ log_record["preview"] = offending_input_preview[:_MAX_PREVIEW_LENGTH]
55
+
56
+ with log_path.open("a", encoding="utf-8") as log_file:
57
+ log_file.write(json.dumps(log_record) + "\n")
58
+ except OSError:
59
+ pass
@@ -0,0 +1,56 @@
1
+ """Shared helpers that reconstruct the post-edit content of an Edit or MultiEdit.
2
+
3
+ Several PreToolUse blockers judge the content a write would leave on disk rather
4
+ than the raw payload fragment, so an edit on a line the blocker watches still
5
+ participates even when an untouched line elsewhere supplies the context. Both the
6
+ edit-replacement applier and the edit-list extractor are identical across those
7
+ blockers, so they live here once and are imported from each.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ __all__ = [
13
+ "apply_edits",
14
+ "edits_for_tool",
15
+ ]
16
+
17
+
18
+ def apply_edits(existing_content: str, all_edits: list[dict]) -> str:
19
+ """Return *existing_content* with each Edit/MultiEdit replacement applied in order.
20
+
21
+ Args:
22
+ existing_content: The current on-disk file content.
23
+ all_edits: The Edit payload (as a single-element list) or MultiEdit
24
+ ``edits`` list, each a mapping with an ``old_string`` and a
25
+ ``new_string``.
26
+
27
+ Returns:
28
+ The content after replacing the first occurrence of each edit's
29
+ ``old_string`` with its ``new_string``, in list order.
30
+ """
31
+ edited_content = existing_content
32
+ for each_edit in all_edits:
33
+ if not isinstance(each_edit, dict):
34
+ continue
35
+ old_string = each_edit.get("old_string", "")
36
+ new_string = each_edit.get("new_string", "")
37
+ if isinstance(old_string, str) and isinstance(new_string, str) and old_string:
38
+ edited_content = edited_content.replace(old_string, new_string, 1)
39
+ return edited_content
40
+
41
+
42
+ def edits_for_tool(tool_name: str, tool_input: dict) -> list[dict]:
43
+ """Return the edit mappings an Edit or MultiEdit payload carries.
44
+
45
+ Args:
46
+ tool_name: The intercepted tool — ``Edit`` or ``MultiEdit``.
47
+ tool_input: The tool's input payload.
48
+
49
+ Returns:
50
+ A single-element list holding the Edit payload, or the MultiEdit
51
+ ``edits`` list when it is present as a list; an empty list otherwise.
52
+ """
53
+ if tool_name == "Edit":
54
+ return [tool_input]
55
+ all_edits = tool_input.get("edits", [])
56
+ return all_edits if isinstance(all_edits, list) else []
@@ -0,0 +1,36 @@
1
+ """Cache paths and tunables for the mypy_validator per-session caches.
2
+
3
+ The validator keeps two per-session caches so a Write/Edit burst under one
4
+ project root does not repeat work whose result has not changed: a config-walk
5
+ cache keyed by the target file's directory, and a content-hash cache keyed by
6
+ target file.
7
+ Both live as JSON files under the per-session hook-state cache directory the
8
+ live tree already uses for hook state. A cold or missing cache simply does the
9
+ work, so these paths are safe to miss.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+
16
+ __all__ = [
17
+ "HOOK_STATE_CACHE_DIRECTORY",
18
+ "MYPY_CONFIG_CACHE_FILENAME",
19
+ "MYPY_CONTENT_HASH_CACHE_FILENAME",
20
+ "SESSION_ID_ENVIRONMENT_VARIABLE",
21
+ "UNKNOWN_SESSION_IDENTIFIER",
22
+ "CONTENT_HASH_CACHE_PASSING_EXIT_CODE",
23
+ "CACHE_FILE_ENCODING",
24
+ ]
25
+
26
+ HOOK_STATE_CACHE_DIRECTORY = os.path.join(os.path.expanduser("~"), ".claude", "cache")
27
+
28
+ MYPY_CONFIG_CACHE_FILENAME = "mypy-validator-config-cache.json"
29
+ MYPY_CONTENT_HASH_CACHE_FILENAME = "mypy-validator-content-hash-cache.json"
30
+
31
+ SESSION_ID_ENVIRONMENT_VARIABLE = "CLAUDE_CODE_SESSION_ID"
32
+ UNKNOWN_SESSION_IDENTIFIER = "unknown-session"
33
+
34
+ CONTENT_HASH_CACHE_PASSING_EXIT_CODE = 0
35
+
36
+ CACHE_FILE_ENCODING = "utf-8"