claude-dev-env 1.19.0 → 1.19.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.
@@ -44,7 +44,7 @@ These rules are automatically enforced by `code-rules-enforcer.py`. Violations b
44
44
  | No NEW comments | `#` / `//` in new code only (existing comments NEVER removed; shebangs, type:, noqa, eslint, docstrings exempt) |
45
45
  | Imports at top | No `import` inside function bodies |
46
46
  | Logging format args | No `log_*(f"...")` - use `log_*("...", arg)` |
47
- | File line count | Max 400 lines per file |
47
+ | File line count | Advisory only see [File length guidance](#65-file-length-guidance) |
48
48
  | Magic values | No literals in function bodies (0, 1, -1 exempt). Includes string templates — if you strip the interpolations from an f-string and the remaining literal text is structural (paths, URLs, patterns), those fragments are magic values that belong in config |
49
49
  | Constants location | No `UPPER_SNAKE =` outside `config/` |
50
50
 
@@ -121,6 +121,28 @@ def function_name(
121
121
 
122
122
  ---
123
123
 
124
+ ## 6.5 FILE LENGTH GUIDANCE
125
+
126
+ File length is a **smell signal, not a hard threshold**. Long files often hide multiple responsibilities, but legitimately long files exist (migrations, generated code, registries, fixtures). The hook surfaces advisories instead of blocking.
127
+
128
+ **Two advisory thresholds (non-blocking, stderr only):**
129
+
130
+ | Threshold | Source basis | Hook behavior |
131
+ |-----------|--------------|---------------|
132
+ | `>= 400` lines | Robert C. Martin, *Clean Code* (2008), Ch. 5 "Formatting" — small files preferred; Martin Fowler, *Refactoring* — "Large Class" code smell | Soft advisory: "consider splitting" |
133
+ | `>= 1000` lines | pylint default `max-module-lines=1000`; SonarQube rule S104 default `1000` | Strong nudge: "exceeds widely-used static-analysis defaults" |
134
+
135
+ **What we deliberately reject:**
136
+
137
+ - **Hard numeric blocks** — Google's Python Style Guide imposes no file-length cap (only a ~40-line function review hint at https://google.github.io/styleguide/pyguide.html). A blocking rule produces false positives on legitimate cases.
138
+ - **A single magic number** — Different sources land at 200 (*Clean Code* preference), 750 (some SonarQube language profiles), or 1000 (pylint, Sonar Java). No source justifies a single universal cap.
139
+
140
+ **When to actually split:**
141
+
142
+ The size signal matters *because* of what it usually indicates: multiple responsibilities (Single Responsibility Principle — Robert C. Martin, *Agile Software Development*, 2002), poor cohesion (Steve McConnell, *Code Complete 2e*, 2004, Ch. 5–6), or the "Large Class" / "Long Function" smells (Fowler). Use the readability rubric (`~/.claude/skills/readability-review/SKILL.md`) when an advisory fires — split based on cohesion, not line count.
143
+
144
+ ---
145
+
124
146
  ## 7. RIGHT-SIZED ENGINEERING
125
147
 
126
148
  **Simple > Clever. Functions > Classes. Concrete > Abstract.**
@@ -175,7 +197,7 @@ Hook will enforce:
175
197
  [⚡] No magic values
176
198
  [⚡] Imports at top
177
199
  [⚡] Logging format args
178
- [] File under 400 lines
200
+ [ ] File length reasonable (advisory at 400, strong nudge at 1000 — see §6.5)
179
201
  [⚡] Constants in config/
180
202
 
181
203
  Manual check:
@@ -2,15 +2,17 @@
2
2
  """
3
3
  CODE_RULES.md enforcer - blocks code that violates mandatory rules.
4
4
 
5
- Checks (all blocking):
5
+ Checks (blocking):
6
6
  1. No comments (# or // in code, excluding shebangs/type: ignore)
7
7
  2. Imports at top (no imports inside functions)
8
8
  3. Logging f-strings (log_* calls must use format args)
9
- 4. File line count (>400 blocks)
10
- 5. Windows API None (win32gui calls with None parameter)
11
- 6. Magic values (literals in function bodies)
12
- 7. E2E test naming (no online/offline in test names)
13
- 8. Constants outside config (UPPER_SNAKE = in non-config files)
9
+ 4. Windows API None (win32gui calls with None parameter)
10
+ 5. Magic values (literals in function bodies)
11
+ 6. E2E test naming (no online/offline in test names)
12
+ 7. Constants outside config (UPPER_SNAKE = in non-config files)
13
+
14
+ Advisory only (non-blocking):
15
+ - File line count: stderr warning at 400 lines (soft) and 1000 lines (hard)
14
16
  """
15
17
  import json
16
18
  import re
@@ -27,6 +29,9 @@ HOOK_INFRASTRUCTURE_PATTERNS = {"/.claude/hooks/", "\\.claude\\hooks\\", "\\.cla
27
29
  WORKFLOW_REGISTRY_PATTERNS = {"/workflow/", "\\workflow\\", "_tab.py", "/states.py", "\\states.py", "/modules.py", "\\modules.py"}
28
30
  MIGRATION_PATH_PATTERNS = {"/migrations/", "\\migrations\\"}
29
31
 
32
+ ADVISORY_LINE_THRESHOLD_SOFT = 400
33
+ ADVISORY_LINE_THRESHOLD_HARD = 1000
34
+
30
35
 
31
36
  def get_file_extension(file_path: str) -> str:
32
37
  """Extract lowercase file extension."""
@@ -308,12 +313,27 @@ def check_logging_fstrings(content: str) -> list[str]:
308
313
  return issues
309
314
 
310
315
 
311
- def check_file_line_count(content: str) -> list[str]:
312
- """Check file line count."""
313
- line_count = content.count("\n") + 1
314
- if line_count > 400:
315
- return [f"File has {line_count} lines (max 400) - split into smaller modules"]
316
- return []
316
+ def advise_file_line_count(content: str, file_path: str) -> None:
317
+ """Emit non-blocking stderr advisories when a file crosses size smell thresholds.
318
+
319
+ Thresholds are smell signals, not hard caps. See CODE_RULES.md "File length guidance"
320
+ for rationale. Soft threshold aligns with Clean Code Ch. 5 / Fowler "Large Class".
321
+ Hard threshold matches pylint default max-module-lines and SonarQube S104 default.
322
+ """
323
+ line_count = len(content.splitlines())
324
+ if line_count >= ADVISORY_LINE_THRESHOLD_HARD:
325
+ print(
326
+ f"[CODE_RULES advisory] {file_path}: {line_count} lines - "
327
+ f"exceeds pylint/SonarQube default ({ADVISORY_LINE_THRESHOLD_HARD}); "
328
+ f"strongly consider splitting by responsibility (SRP / cohesion)",
329
+ file=sys.stderr,
330
+ )
331
+ elif line_count >= ADVISORY_LINE_THRESHOLD_SOFT:
332
+ print(
333
+ f"[CODE_RULES advisory] {file_path}: {line_count} lines - "
334
+ f"consider splitting (Clean Code Ch. 5; Fowler 'Large Class' smell)",
335
+ file=sys.stderr,
336
+ )
317
337
 
318
338
 
319
339
  def check_windows_api_none(content: str) -> list[str]:
@@ -485,7 +505,7 @@ def validate_content(content: str, file_path: str, old_content: str = "") -> lis
485
505
  all_issues.extend(check_e2e_test_naming(content, file_path))
486
506
 
487
507
  if extension in ALL_CODE_EXTENSIONS:
488
- all_issues.extend(check_file_line_count(content))
508
+ advise_file_line_count(content, file_path)
489
509
 
490
510
  return all_issues
491
511
 
@@ -8,7 +8,10 @@ import sys
8
8
  from content_search_zoekt_bash_block_reason import block_reason_for_bash_command
9
9
  from content_search_zoekt_block_payload import build_block_payload
10
10
  from content_search_zoekt_indexed_paths import is_in_indexed_repo, is_specific_file
11
- from content_search_zoekt_redirect_guidance import get_zoekt_redirect_guidance
11
+ from content_search_zoekt_redirect_guidance import (
12
+ get_zoekt_redirect_guidance,
13
+ get_zoekt_redirect_reason_brief,
14
+ )
12
15
 
13
16
 
14
17
  def main() -> None:
@@ -45,7 +48,8 @@ def main() -> None:
45
48
  short_label = f"blocked {block_reason}; use Zoekt MCP"
46
49
  payload = build_block_payload(
47
50
  brief_label=short_label,
48
- permission_decision_reason=get_zoekt_redirect_guidance(),
51
+ permission_decision_reason=get_zoekt_redirect_reason_brief(),
52
+ additional_context=get_zoekt_redirect_guidance(),
49
53
  )
50
54
  print(json.dumps(payload))
51
55
  sys.exit(0)
@@ -4,14 +4,18 @@
4
4
  def build_block_payload(
5
5
  brief_label: str,
6
6
  permission_decision_reason: str,
7
+ additional_context: str | None = None,
7
8
  ) -> dict:
8
9
  destructive_gate_label_prefix = "[destructive-gate]"
10
+ hook_specific_output: dict = {
11
+ "hookEventName": "PreToolUse",
12
+ "permissionDecision": "deny",
13
+ "permissionDecisionReason": permission_decision_reason,
14
+ }
15
+ if additional_context is not None:
16
+ hook_specific_output["additionalContext"] = additional_context
9
17
  return {
10
- "hookSpecificOutput": {
11
- "hookEventName": "PreToolUse",
12
- "permissionDecision": "deny",
13
- "permissionDecisionReason": permission_decision_reason,
14
- },
18
+ "hookSpecificOutput": hook_specific_output,
15
19
  "systemMessage": f"{destructive_gate_label_prefix} {brief_label}",
16
20
  "suppressOutput": True,
17
21
  }
@@ -1,4 +1,10 @@
1
- """Zoekt MCP usage and repo-to-disk path mapping for PreToolUse permissionDecisionReason."""
1
+ """Zoekt MCP usage and repo-to-disk path mapping for PreToolUse outputs."""
2
+
3
+
4
+ def get_zoekt_redirect_reason_brief() -> str:
5
+ return (
6
+ "Use Zoekt MCP (e.g. mcp__zoekt__search) instead of Grep/Search in Zoekt-indexed trees."
7
+ )
2
8
 
3
9
 
4
10
  def get_zoekt_redirect_guidance() -> str:
@@ -13,7 +13,10 @@ class ContentSearchHookIntegrationTests(unittest.TestCase):
13
13
  hook_directory = pathlib.Path(__file__).resolve().parent
14
14
  if str(hook_directory) not in sys.path:
15
15
  sys.path.insert(0, str(hook_directory))
16
- from content_search_zoekt_redirect_guidance import get_zoekt_redirect_guidance
16
+ from content_search_zoekt_redirect_guidance import (
17
+ get_zoekt_redirect_guidance,
18
+ get_zoekt_redirect_reason_brief,
19
+ )
17
20
 
18
21
  hook_path = hook_directory / "content-search-to-zoekt-redirector.py"
19
22
  destructive_gate_label_prefix = "[destructive-gate]"
@@ -38,6 +41,10 @@ class ContentSearchHookIntegrationTests(unittest.TestCase):
38
41
  )
39
42
  self.assertEqual(
40
43
  payload["hookSpecificOutput"]["permissionDecisionReason"],
44
+ get_zoekt_redirect_reason_brief(),
45
+ )
46
+ self.assertEqual(
47
+ payload["hookSpecificOutput"]["additionalContext"],
41
48
  get_zoekt_redirect_guidance(),
42
49
  )
43
50
  self.assertEqual(
@@ -11,7 +11,10 @@ if str(HOOK_DIRECTORY) not in sys.path:
11
11
  sys.path.insert(0, str(HOOK_DIRECTORY))
12
12
 
13
13
  from content_search_zoekt_block_payload import build_block_payload
14
- from content_search_zoekt_redirect_guidance import get_zoekt_redirect_guidance
14
+ from content_search_zoekt_redirect_guidance import (
15
+ get_zoekt_redirect_guidance,
16
+ get_zoekt_redirect_reason_brief,
17
+ )
15
18
 
16
19
 
17
20
  class BuildBlockPayloadTests(unittest.TestCase):
@@ -32,12 +35,14 @@ class BuildBlockPayloadTests(unittest.TestCase):
32
35
  self.assertEqual(payload["suppressOutput"], True)
33
36
  self.assertNotIn("decision", payload)
34
37
  self.assertNotIn("reason", payload)
38
+ self.assertNotIn("additionalContext", payload["hookSpecificOutput"])
35
39
 
36
40
  def test_serialized_payload_under_documented_context_cap(self) -> None:
37
41
  cap_characters = 10_000
38
42
  payload = build_block_payload(
39
43
  brief_label="blocked Bash(grep); use Zoekt MCP",
40
- permission_decision_reason=get_zoekt_redirect_guidance(),
44
+ permission_decision_reason=get_zoekt_redirect_reason_brief(),
45
+ additional_context=get_zoekt_redirect_guidance(),
41
46
  )
42
47
  serialized = json.dumps(payload)
43
48
  self.assertLessEqual(
package/hooks/hooks.json CHANGED
@@ -2,6 +2,16 @@
2
2
  "description": "Code standards enforcement, safety guards, and development workflow hooks",
3
3
  "hooks": {
4
4
  "PreToolUse": [
5
+ {
6
+ "matcher": "Grep|Search",
7
+ "hooks": [
8
+ {
9
+ "type": "command",
10
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/content-search-to-zoekt-redirector.py",
11
+ "timeout": 10
12
+ }
13
+ ]
14
+ },
5
15
  {
6
16
  "matcher": "Write|Edit",
7
17
  "hooks": [
@@ -84,6 +94,11 @@
84
94
  "type": "command",
85
95
  "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/test-preflight-check.py",
86
96
  "timeout": 10
97
+ },
98
+ {
99
+ "type": "command",
100
+ "command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/content-search-to-zoekt-redirector.py",
101
+ "timeout": 10
87
102
  }
88
103
  ]
89
104
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.19.0",
3
+ "version": "1.19.2",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {