claude-dev-env 2.29.0 → 2.30.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.
Files changed (60) hide show
  1. package/.agents/agents/pr-description-writer.md +21 -6
  2. package/.agents/agents/reference/README.md +6 -0
  3. package/.agents/agents/reference/pr-description-illustrative-voice.md +45 -0
  4. package/.agents/agents/reference/pr-description-verification.md +19 -0
  5. package/.agents/agents/test_pr_description_writer_style.py +47 -0
  6. package/.agents/skills/e-simplify/SKILL.md +20 -8
  7. package/_shared/pr-loop/code-rules-gate.md +1 -1
  8. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -2
  9. package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +8 -8
  10. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +2 -2
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +1 -1
  12. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +15 -15
  13. package/docs/CODE_RULES.md +10 -0
  14. package/hooks/blocking/code_rules_enforcer.py +435 -318
  15. package/hooks/blocking/shell_substitution_blocker.py +106 -30
  16. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +14 -0
  17. package/hooks/blocking/test_code_rules_enforcer_banned_noun_word.py +1 -1
  18. package/hooks/blocking/test_code_rules_enforcer_class_docstring_methods.py +1 -1
  19. package/hooks/blocking/test_code_rules_enforcer_dispatch_wiring.py +117 -22
  20. package/hooks/blocking/test_code_rules_enforcer_docstring_args_signature.py +1 -1
  21. package/hooks/blocking/test_code_rules_enforcer_docstring_prose_wall_illustration.py +1 -1
  22. package/hooks/blocking/test_code_rules_enforcer_docstring_runon_sentence.py +1 -1
  23. package/hooks/blocking/test_code_rules_enforcer_docstring_unreferenced_param.py +1 -1
  24. package/hooks/blocking/test_code_rules_enforcer_fstring_scan.py +1 -1
  25. package/hooks/blocking/test_code_rules_enforcer_ignored_must_check_return.py +1 -1
  26. package/hooks/blocking/test_code_rules_enforcer_inline_tuple_string_magic.py +1 -1
  27. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +2 -2
  28. package/hooks/blocking/test_code_rules_enforcer_js_returns_object.py +3 -3
  29. package/hooks/blocking/test_code_rules_enforcer_module_docstring_roster.py +1 -1
  30. package/hooks/blocking/test_code_rules_enforcer_naming_pattern.py +1 -1
  31. package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +117 -38
  32. package/hooks/blocking/test_code_rules_enforcer_output_snapshot.py +121 -0
  33. package/hooks/blocking/test_code_rules_enforcer_precheck_forecast.py +17 -15
  34. package/hooks/blocking/test_code_rules_enforcer_split_entry_1.py +4 -2
  35. package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +1 -1
  36. package/hooks/blocking/test_code_rules_enforcer_stage.py +1 -1
  37. package/hooks/blocking/test_code_rules_enforcer_string_magic.py +4 -4
  38. package/hooks/blocking/test_code_rules_enforcer_thin_wrapper_files.py +2 -2
  39. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias.py +2 -2
  40. package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +2 -2
  41. package/hooks/blocking/test_code_rules_validation_phase.py +332 -0
  42. package/hooks/blocking/test_fable_spawn_gate.py +1 -1
  43. package/hooks/blocking/test_pre_tool_use_dispatcher.py +26 -11
  44. package/hooks/blocking/test_shell_substitution_blocker.py +69 -0
  45. package/hooks/hooks.json +1 -1
  46. package/hooks/hooks_constants/code_rules_enforcer_constants.py +7 -0
  47. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +4 -1
  48. package/hooks/hooks_constants/task_tool_prompt_constants.py +13 -0
  49. package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +12 -0
  50. package/hooks/hooks_constants/validation_phase_constants.py +34 -0
  51. package/hooks/session/task_tool_prompt.py +37 -0
  52. package/hooks/session/test_task_tool_prompt.py +65 -0
  53. package/hooks/validators/README.md +1 -1
  54. package/hooks/validators/config/hook_timing_harness_constants.py +16 -0
  55. package/hooks/validators/hook_timing_harness.py +287 -0
  56. package/hooks/validators/test_hook_timing_harness.py +251 -0
  57. package/package.json +1 -1
  58. package/hooks/hooks_constants/task_list_loop_starter_constants.py +0 -19
  59. package/hooks/session/task_list_loop_starter.py +0 -45
  60. package/hooks/session/test_task_list_loop_starter.py +0 -63
@@ -1,19 +1,34 @@
1
1
  ---
2
2
  name: pr-description-writer
3
- description: "Optional agent that drafts factual pull request descriptions and comments from the current diff and validates each statement against the master review guides."
3
+ description: "Optional agent that writes factual pull request titles, descriptions, and comments in plain illustrative language from the current diff."
4
4
  tools: Read,Grep,Glob,Bash
5
5
  ---
6
6
 
7
7
  # PR Description Writer
8
8
 
9
- Write the body from the current pull request diff and task. Resolve the active managed root and active agents home before reading guides: `~/.claude` is the default root, `CLAUDE_CONFIG_DIR` selects another root, and `--target DIR` takes precedence; the default `.claude` root uses sibling `~/.agents`, while another root uses sibling `<root-name>.agents`. Use `<agents-home>/skills/pr-title-description/SKILL.md` (source fallback: `packages/claude-dev-env/.agents/skills/pr-title-description/SKILL.md`) for required content and shape. Use the current review findings and task guidance for a review comment. Do not assume `~/.claude` or `~/.agents` for a named profile or explicit target.
9
+ Write from the current pull request diff and task. Resolve the active managed root and active agents home before reading guides: `~/.claude` is the default root, `CLAUDE_CONFIG_DIR` selects another root, and `--target DIR` takes precedence; the default `.claude` root uses sibling `~/.agents`, while another root uses sibling `<root-name>.agents`. Use `<agents-home>/skills/pr-title-description/SKILL.md` (source fallback: `packages/claude-dev-env/.agents/skills/pr-title-description/SKILL.md`) for required content and shape. Use the current review findings and task guidance for a review comment. Do not assume `~/.claude` or `~/.agents` for a named profile or explicit target.
10
+
11
+ ## Voice
12
+
13
+ Explain the change so a smart reader who knows nothing about the code can picture it on the first read.
14
+
15
+ - Start with a concrete scene or action.
16
+ - Prefer a tiny Before / After story when the change is about how something is found, asked for, checked, saved, retried, or stopped.
17
+ - Use small words and picture words.
18
+ - Replace jargon with the action it represents. When one technical term matters, explain it immediately in plain words.
19
+ - Keep the main behavior in `What this adds`, the reason in `Why`, and proof in `Verification`.
20
+ - Keep titles concrete. Say what someone gives it, what happens, or what comes back.
21
+
22
+ For a concrete voice sample only when needed, read `reference/pr-description-illustrative-voice.md` beside this agent. For a concrete verification sample only when needed, read `reference/pr-description-verification.md`. These samples guide the shape and voice; they are not content to copy into unrelated pull requests.
10
23
 
11
24
  ## Draft the body
12
25
 
13
26
  1. Inspect the cumulative diff, the current pull request body, and the validation results.
14
- 2. State the behavior change, its scope, and the validation that ran.
15
- 3. Preserve accurate issue links and curated sections from the current body.
16
- 4. Use headings when they help a reviewer scan the change.
27
+ 2. Find the main behavior a user, caller, operator, or reviewer can see.
28
+ 3. Write that behavior first in plain illustrative language.
29
+ 4. Preserve accurate issue links and curated sections from the current body when they remain useful.
30
+ 5. Use headings when they help a reviewer scan the change.
31
+ 6. In `Verification`, lead with what a person can open, see, click, compare, or try for themselves. Put test results in one short supporting line when useful.
17
32
 
18
33
  ## Publish through GitHub CLI
19
34
 
@@ -21,4 +36,4 @@ Place markdown in a BOM-free temporary file and pass its path with `--body-file`
21
36
 
22
37
  ## Check the draft
23
38
 
24
- Confirm every statement matches the diff and current pull request state. Return the final markdown and the file path ready for publication.
39
+ Confirm every statement matches the diff and current pull request state. Confirm the title and body stay clear without requiring the reader to decode repository jargon. Confirm `Verification` gives the reviewer a visible check they can perform themselves whenever the change has one. Return the final markdown and the file path ready for publication.
@@ -0,0 +1,6 @@
1
+ # PR writer references
2
+
3
+ These files are optional examples for `pr-description-writer.md`.
4
+
5
+ - `pr-description-illustrative-voice.md` — plain, picture-first title and description examples.
6
+ - `pr-description-verification.md` — human-checkable verification examples.
@@ -0,0 +1,45 @@
1
+ # PR description illustrative voice
2
+
3
+ Use this only when the PR writer needs a concrete voice sample.
4
+
5
+ ## Core pattern
6
+
7
+ Start with something a person can picture.
8
+
9
+ Before: show the old action in a tiny scene.
10
+
11
+ After: show the new action in a tiny scene.
12
+
13
+ Keep machine words out when a plain action works. If one technical term matters, explain it in the same breath with a simple picture.
14
+
15
+ ## Sample 1 — call the right helper directly
16
+
17
+ The theme package has a phone icon somewhere. We need to find it.
18
+
19
+ Before: yell, “anyone know how to find icons?” and wait for a hand.
20
+
21
+ After: call the finder by its name tag: “icon finder, go.”
22
+
23
+ That is all the operation name is here — the finder’s name tag.
24
+
25
+ ## Sample 2 — ask for a deeper review
26
+
27
+ You start the review loop. It looks at the code, then looks again, until it has nothing new to say.
28
+
29
+ Before: each look could be a quick glance.
30
+
31
+ After: each look uses the deepest review setting the model has.
32
+
33
+ The person running the loop asked for a full look, so every pass gets a full look.
34
+
35
+ ## Writing rules
36
+
37
+ - Few words. Small words. Clear on first read.
38
+ - Lead with the thing someone can see or do.
39
+ - Prefer Before / After when the change is about how something is asked, found, checked, saved, retried, or stopped.
40
+ - Use picture words: hand it, open it, find it, check it, save it, skip it, stop, call by name.
41
+ - Prefer spoken results: “found it,” “missing,” “duplicated,” “couldn’t.”
42
+ - Keep the main behavior in `What this adds`.
43
+ - Keep `Why` short and concrete.
44
+ - Put proof only in `Verification`.
45
+ - Explain jargon immediately or replace it.
@@ -0,0 +1,19 @@
1
+ # Human-checkable PR verification
2
+
3
+ Use this only when the PR writer needs a concrete verification sample.
4
+
5
+ Verification should lead with something a reviewer can open, see, click, compare, or try.
6
+
7
+ Good:
8
+
9
+ Open the generated PR description. The first paragraph should tell the story in plain words before it names any code detail. If the change has a visible result, point to the exact file, screen, output, or behavior the reviewer can inspect.
10
+
11
+ Tests: focused checks pass.
12
+
13
+ Weak:
14
+
15
+ - Contract test passed.
16
+ - Static analysis passed.
17
+ - Internal schema assertion passed.
18
+
19
+ Machine checks can support the claim. They should not be the whole proof when a person can inspect the result directly.
@@ -0,0 +1,47 @@
1
+ """Contract tests for the PR description writer voice."""
2
+
3
+ from pathlib import Path
4
+
5
+
6
+ AGENT_DIRECTORY = Path(__file__).parent
7
+ AGENT_PATH = AGENT_DIRECTORY / "pr-description-writer.md"
8
+ VOICE_REFERENCE_PATH = AGENT_DIRECTORY / "reference" / "pr-description-illustrative-voice.md"
9
+ VERIFICATION_REFERENCE_PATH = AGENT_DIRECTORY / "reference" / "pr-description-verification.md"
10
+
11
+
12
+ def test_pr_description_writer_uses_repo_owned_progressive_samples() -> None:
13
+ agent_text = AGENT_PATH.read_text(encoding="utf-8")
14
+
15
+ assert "reference/pr-description-illustrative-voice.md" in agent_text
16
+ assert "reference/pr-description-verification.md" in agent_text
17
+ assert "only when needed" in agent_text
18
+ assert "PR #2562" not in agent_text
19
+ assert "PR #1150" not in agent_text
20
+
21
+
22
+ def test_voice_reference_keeps_the_plain_illustrative_shape() -> None:
23
+ reference_text = VOICE_REFERENCE_PATH.read_text(encoding="utf-8")
24
+
25
+ for required_text in (
26
+ "Before:",
27
+ "After:",
28
+ "finder’s name tag",
29
+ "found it",
30
+ "missing",
31
+ "duplicated",
32
+ "What this adds",
33
+ "Why",
34
+ "Verification",
35
+ ):
36
+ assert required_text in reference_text
37
+
38
+
39
+ def test_verification_leads_with_a_human_visible_check() -> None:
40
+ agent_text = AGENT_PATH.read_text(encoding="utf-8")
41
+ reference_text = VERIFICATION_REFERENCE_PATH.read_text(encoding="utf-8")
42
+
43
+ for visible_action in ("open", "see", "click", "compare", "try"):
44
+ assert visible_action in agent_text
45
+ assert visible_action in reference_text
46
+ assert "Tests: focused checks pass." in reference_text
47
+ assert "Machine checks can support the claim." in reference_text
@@ -25,7 +25,7 @@ Triggers: `/e-simplify` on the current diff (or a PR/branch/path passed as an ar
25
25
 
26
26
  ## The process
27
27
 
28
- `/simplify → 4 cleanup agents in parallel → apply the fixes`
28
+ `/simplify → up to 4 cleanup agents in parallel → apply the fixes`
29
29
 
30
30
  You are improving the quality of the changed code, not hunting for bugs. Review
31
31
  it for reuse, simplification, efficiency, and altitude issues, then fix what you
@@ -40,13 +40,25 @@ include the working-tree changes in scope — the review often runs before the
40
40
  commit. If a PR number, branch name, or file path was passed as an argument,
41
41
  review that target instead. Treat this diff as the review scope.
42
42
 
43
- ### Phase 1 — Review (4 cleanup agents in parallel)
43
+ **Diff-size cap.** Run `git diff --shortstat` on the same range Phase 0 used
44
+ to get the diff, and add its insertions and deletions for the changed-line
45
+ count. Default ceiling: 400 changed lines for one run — a tunable default the
46
+ user may adjust. Above the ceiling, narrow the target first — a subdirectory,
47
+ a package, or one file path — and review that narrowed target. Do not launch
48
+ Phase 1 over a diff above the ceiling.
44
49
 
45
- Launch **4 independent review agents** via the Agent tool, all in a
46
- single message so they run concurrently. Pass each agent the diff and one of
47
- the four angles below. Each returns its findings with `file`, `line`, a
48
- one-line `summary`, and the concrete cost (what is duplicated, wasted, or
49
- harder to maintain).
50
+ ### Phase 1 — Review (up to 4 cleanup agents in parallel)
51
+
52
+ **Lens cap.** Four lenses is the ceiling, not the floor. Default split — also
53
+ tunable — by the `git diff --shortstat` changed-line count from Phase 0:
54
+ under 100 changed lines, run 2 lenses (reuse and simplification); at 100
55
+ changed lines or above, and under the diff-size cap, run all 4 lenses.
56
+
57
+ Launch the review agents for the lenses in scope for this run — 2 or 4 by the
58
+ cap above — via the Agent tool, all in a single message so they run
59
+ concurrently. Pass each agent the diff and one of the angles below. Each
60
+ returns its findings with `file`, `line`, a one-line `summary`, and the
61
+ concrete cost (what is duplicated, wasted, or harder to maintain).
50
62
 
51
63
  #### Reuse
52
64
 
@@ -79,7 +91,7 @@ special cases.
79
91
 
80
92
  ### Phase 2 — Apply the fixes
81
93
 
82
- Wait for all four agents to complete, dedup findings that point at the same
94
+ Wait for every launched agent to complete, dedup findings that point at the same
83
95
  line or mechanism, and fix each remaining one directly. Skip any finding whose
84
96
  fix would change intended behavior, require changes well outside the reviewed
85
97
  diff, or that you judge to be a false positive — note the skip rather than
@@ -1,6 +1,6 @@
1
1
  # CODE_RULES gate
2
2
 
3
- Pre-audit validator run before each AUDIT (and pre-commit when applicable). Wraps `validate_content` from `~/.claude/hooks/blocking/code_rules_enforcer.py`.
3
+ Pre-audit validator run before each AUDIT (and pre-commit when applicable). Wraps `validate_content_for_full_gate` from `~/.claude/hooks/blocking/code_rules_enforcer.py`.
4
4
 
5
5
  ## Script location
6
6
 
@@ -63,7 +63,7 @@ except ImportError as import_error:
63
63
  subprocess = git_file_sets.subprocess
64
64
 
65
65
  resolve_claude_dev_env_root = enforcer_loading.resolve_claude_dev_env_root
66
- load_validate_content = enforcer_loading.load_validate_content
66
+ load_validate_content_for_full_gate = enforcer_loading.load_validate_content_for_full_gate
67
67
  ValidateContentCallable = enforcer_loading.ValidateContentCallable
68
68
 
69
69
  resolve_merge_base = git_file_sets.resolve_merge_base
@@ -357,7 +357,7 @@ def main(all_arguments: list[str]) -> int:
357
357
  repository_root = (
358
358
  arguments.repo_root.resolve() if arguments.repo_root is not None else Path.cwd().resolve()
359
359
  )
360
- validate_content = load_validate_content()
360
+ validate_content = load_validate_content_for_full_gate()
361
361
  if arguments.paths:
362
362
  return _run_explicit_paths_mode(validate_content, arguments, repository_root)
363
363
  if arguments.immediate:
@@ -1,8 +1,8 @@
1
- """Load the code-rules enforcer's ``validate_content`` for in-process use.
1
+ """Load the code-rules enforcer's ``validate_content_for_full_gate`` for in-process use.
2
2
 
3
- The gate runs the same ``validate_content`` the PreToolUse enforcer runs, so it
4
- locates the ``hooks/blocking/code_rules_enforcer.py`` module from disk, executes
5
- it with the hooks directory on ``sys.path``, and hands back its callable.
3
+ The gate runs the enforcer's full-gate validation phase, so it locates the
4
+ ``hooks/blocking/code_rules_enforcer.py`` module from disk, executes it with
5
+ the hooks directory on ``sys.path``, and hands back its callable.
6
6
  """
7
7
 
8
8
  import importlib.machinery
@@ -138,11 +138,11 @@ def _exec_enforcer_with_hooks_on_path(
138
138
  _restore_hooks_constants_modules(all_saved_modules, hooks_root_path)
139
139
 
140
140
 
141
- def load_validate_content() -> ValidateContentCallable:
142
- """Load ``code_rules_enforcer.validate_content`` for in-process use.
141
+ def load_validate_content_for_full_gate() -> ValidateContentCallable:
142
+ """Load ``code_rules_enforcer.validate_content_for_full_gate`` for in-process use.
143
143
 
144
144
  Returns:
145
- The ``validate_content`` callable from the enforcer module.
145
+ The ``validate_content_for_full_gate`` callable from the enforcer module.
146
146
 
147
147
  Raises:
148
148
  SystemExit: When the package root or the enforcer module cannot load.
@@ -153,7 +153,7 @@ def load_validate_content() -> ValidateContentCallable:
153
153
  )
154
154
  module = importlib.util.module_from_spec(specification)
155
155
  _exec_enforcer_with_hooks_on_path(specification, module)
156
- return module.validate_content
156
+ return module.validate_content_for_full_gate
157
157
 
158
158
 
159
159
  def _restore_hooks_constants_modules(
@@ -10,8 +10,8 @@ def test_resolve_claude_dev_env_root_finds_enforcer_bearing_root() -> None:
10
10
  assert (resolved_root / "hooks" / "blocking" / "code_rules_enforcer.py").is_file()
11
11
 
12
12
 
13
- def test_load_validate_content_returns_callable_passing_a_clean_module() -> None:
14
- validate_content = enforcer_loading.load_validate_content()
13
+ def test_load_validate_content_for_full_gate_returns_callable_passing_a_clean_module() -> None:
14
+ validate_content = enforcer_loading.load_validate_content_for_full_gate()
15
15
  clean_module = '"""Clean module."""\n\n\ndef ping() -> str:\n return "pong"\n'
16
16
  issues = validate_content(clean_module, "sample.py", "")
17
17
  assert issues == []
@@ -130,7 +130,7 @@ def test_gate_caller_scans_staged_content_with_commit_stage_scope(
130
130
 
131
131
  resolved_file_path = file_path.resolve()
132
132
  exit_code = gate_running.run_gate(
133
- validate_content=enforcer_loading.load_validate_content(),
133
+ validate_content=enforcer_loading.load_validate_content_for_full_gate(),
134
134
  all_file_paths=[resolved_file_path],
135
135
  repository_root=repository_root,
136
136
  all_added_lines_by_path={resolved_file_path: {2, 3}},
@@ -735,7 +735,7 @@ def test_run_gate_flags_copied_sibling_when_cwd_is_outside_repo_root(
735
735
  copied_file = package_directory / "copied_helper.py"
736
736
  existing_file.write_text(_DUPLICATE_HELPER_SOURCE, encoding="utf-8")
737
737
  copied_file.write_text(_DUPLICATE_HELPER_SOURCE, encoding="utf-8")
738
- validate_content = gate_module.load_validate_content()
738
+ validate_content = gate_module.load_validate_content_for_full_gate()
739
739
 
740
740
  monkeypatch.chdir(temporary_git_repository.parent)
741
741
  blocking_by_file, _advisory_by_file, _skipped = (
@@ -813,7 +813,7 @@ def test_duplicate_body_span_range_covers_the_definition_through_last_body_line(
813
813
  assert span == range(definition_line, last_body_line + 1)
814
814
 
815
815
 
816
- def test_split_violations_blocks_duplicate_body_when_span_intersects_added_lines(
816
+ def test_split_violations_by_scope_blocks_duplicate_body_when_span_intersects_added_lines(
817
817
  tmp_path: Path,
818
818
  ) -> None:
819
819
  """A duplicate-body issue whose copied-function span overlaps the diff's added
@@ -829,7 +829,7 @@ def test_split_violations_blocks_duplicate_body_when_span_intersects_added_lines
829
829
  assert advisory == []
830
830
 
831
831
 
832
- def test_split_violations_advises_duplicate_body_when_span_misses_added_lines(
832
+ def test_split_violations_by_scope_advises_duplicate_body_when_span_misses_added_lines(
833
833
  tmp_path: Path,
834
834
  ) -> None:
835
835
  """A duplicate-body issue for an untouched pre-existing copy — whose span does
@@ -1002,7 +1002,7 @@ def test_collect_partitioned_violations_advises_pre_existing_sibling_duplicate(
1002
1002
  _DUPLICATE_HELPER_SOURCE + "unrelated_constant = 1\n", encoding="utf-8"
1003
1003
  )
1004
1004
  unrelated_added_line = _DUPLICATE_HELPER_SOURCE.count("\n") + 1
1005
- validate_content = gate_module.load_validate_content()
1005
+ validate_content = gate_module.load_validate_content_for_full_gate()
1006
1006
 
1007
1007
  resolved_copied = copied_file.resolve()
1008
1008
  blocking_by_file, advisory_by_file, _skipped = (
@@ -1055,7 +1055,7 @@ def test_collect_partitioned_violations_blocks_sibling_duplicate_in_added_region
1055
1055
  definition_line = 3
1056
1056
  last_body_line = 15
1057
1057
  all_copied_function_lines = set(range(definition_line, last_body_line + 1))
1058
- validate_content = gate_module.load_validate_content()
1058
+ validate_content = gate_module.load_validate_content_for_full_gate()
1059
1059
 
1060
1060
  resolved_copied = copied_file.resolve()
1061
1061
  blocking_by_file, _advisory_by_file, _skipped = (
@@ -1549,10 +1549,10 @@ def test_main_blocks_when_function_body_grows_past_threshold_with_def_line_untou
1549
1549
  assert exit_code == 1
1550
1550
 
1551
1551
 
1552
- def test_split_violations_blocks_function_length_when_span_intersects_added_lines() -> None:
1552
+ def test_split_violations_by_scope_blocks_function_length_when_span_intersects_added_lines() -> None:
1553
1553
  """A function-length issue whose declared span overlaps the diff's added
1554
1554
  lines is blocking — the body grew, which is exactly Finding B's intent."""
1555
- validate_content = gate_module.load_validate_content()
1555
+ validate_content = gate_module.load_validate_content_for_full_gate()
1556
1556
  long_function = _build_function_module(
1557
1557
  "oversized", body_line_count=70, leading_lines=3
1558
1558
  )
@@ -1571,12 +1571,12 @@ def test_split_violations_blocks_function_length_when_span_intersects_added_line
1571
1571
  assert advisory == []
1572
1572
 
1573
1573
 
1574
- def test_split_violations_advises_function_length_when_span_misses_added_lines() -> None:
1574
+ def test_split_violations_by_scope_advises_function_length_when_span_misses_added_lines() -> None:
1575
1575
  """A function-length issue for an untouched pre-existing function — whose
1576
1576
  declared span does not overlap any added line — is advisory, not blocking.
1577
1577
  This prevents the over-block regression where every pre-existing >=60-line
1578
1578
  function in a touched file was forced into the blocking payload."""
1579
- validate_content = gate_module.load_validate_content()
1579
+ validate_content = gate_module.load_validate_content_for_full_gate()
1580
1580
  long_function = _build_function_module(
1581
1581
  "oversized", body_line_count=70, leading_lines=3
1582
1582
  )
@@ -1595,7 +1595,7 @@ def test_split_violations_advises_function_length_when_span_misses_added_lines()
1595
1595
 
1596
1596
 
1597
1597
  def _isolation_issues_for_home_probe_test() -> list[str]:
1598
- validate_content = gate_module.load_validate_content()
1598
+ validate_content = gate_module.load_validate_content_for_full_gate()
1599
1599
  header = "from pathlib import Path\n"
1600
1600
  test_body = (
1601
1601
  "def test_reads_home() -> None:\n"
@@ -1606,7 +1606,7 @@ def _isolation_issues_for_home_probe_test() -> list[str]:
1606
1606
  return [each_issue for each_issue in issues if "probes" in each_issue]
1607
1607
 
1608
1608
 
1609
- def test_split_violations_blocks_isolation_when_function_span_intersects_added_lines() -> None:
1609
+ def test_split_violations_by_scope_blocks_isolation_when_function_span_intersects_added_lines() -> None:
1610
1610
  """An isolation issue whose enclosing test-function span overlaps the diff's
1611
1611
  added lines is blocking — a signature-line change that un-isolates an
1612
1612
  unchanged-body probe must block, matching the enforcer's terminal path."""
@@ -1621,7 +1621,7 @@ def test_split_violations_blocks_isolation_when_function_span_intersects_added_l
1621
1621
  assert advisory == []
1622
1622
 
1623
1623
 
1624
- def test_split_violations_advises_isolation_when_function_span_misses_added_lines() -> None:
1624
+ def test_split_violations_by_scope_advises_isolation_when_function_span_misses_added_lines() -> None:
1625
1625
  """An isolation issue for an untouched pre-existing probe — whose enclosing
1626
1626
  test-function span does not overlap any added line — is advisory, not
1627
1627
  blocking, mirroring the function-length scope contract."""
@@ -1814,7 +1814,7 @@ def test_check_wrapper_plumb_through_stays_under_function_length_threshold() ->
1814
1814
 
1815
1815
 
1816
1816
  def _banned_noun_parameter_issues() -> list[str]:
1817
- validate_content = gate_module.load_validate_content()
1817
+ validate_content = gate_module.load_validate_content_for_full_gate()
1818
1818
  source = (
1819
1819
  "def aggregate(canned_results: int) -> int:\n"
1820
1820
  " doubled = canned_results * 2\n"
@@ -1824,7 +1824,7 @@ def _banned_noun_parameter_issues() -> list[str]:
1824
1824
  return [each_issue for each_issue in issues if "banned noun" in each_issue]
1825
1825
 
1826
1826
 
1827
- def test_split_violations_blocks_banned_noun_when_binding_line_is_added() -> None:
1827
+ def test_split_violations_by_scope_blocks_banned_noun_when_binding_line_is_added() -> None:
1828
1828
  """A banned-noun binding is blocking when its own binding line is among the
1829
1829
  added lines. The gate reconstructs the one-line binding span through the
1830
1830
  same shared extractor registry it uses for function-length and isolation,
@@ -1840,7 +1840,7 @@ def test_split_violations_blocks_banned_noun_when_binding_line_is_added() -> Non
1840
1840
  assert advisory == []
1841
1841
 
1842
1842
 
1843
- def test_split_violations_advises_banned_noun_when_binding_line_untouched() -> None:
1843
+ def test_split_violations_by_scope_advises_banned_noun_when_binding_line_untouched() -> None:
1844
1844
  """A banned-noun binding whose own line is not among the added lines is
1845
1845
  advisory — editing an unrelated body line does not pull a pre-existing
1846
1846
  binding into scope, mirroring the companion exact-match identifier check."""
@@ -109,3 +109,13 @@ If you already have the data, don't fetch it again.
109
109
  ## 11. ENFORCEMENT SURFACES
110
110
 
111
111
  ⚡ **Hooks** block pattern-matchable violations at Write/Edit time. 🤖 **Prompt context** carries judgment principles (SRP, Right-Sized Engineering, research-first action on ambiguous intent, BDD discovery, docstring-prose-matches-implementation). 👥 **Audit rubrics** (`/check`, `packages/claude-dev-env/audit-rubrics/` categories A–Q) cover cross-file architectural concerns. Rules with documented-but-pending hook coverage live in `~/.claude/rules/*.md`; each names its own promotion path. The docstring-prose standard (free-form enumerations match the body) lives in `packages/claude-dev-env/rules/docstring-prose-matches-implementation.md`, enforced via Category O6 audit. The diagram-first docstring standard (a summary line, then a `::` example or doctest, then a couple of short prose lines) lives in `packages/claude-dev-env/rules/plain-illustrative-docstrings.md`, enforced by the `check_docstring_runon_sentence` and `check_docstring_prose_wall_without_illustration` backstop hooks and Category O9 audit.
112
+
113
+ ## 11.5 VALIDATION-PHASE PRECEDENCE
114
+
115
+ `code_rules_enforcer.py` decides what a run checks and reports along three independent axes. Each axis filters a narrower scope than the one before it; none widens what the axis before it already decided.
116
+
117
+ 1. **Phase selects the roster.** `EDIT_LANE_PHASE` or `FULL_GATE_PHASE` decides which checks exist in the lane at all. `validate_content_for_phase` takes `phase` keyword-only with no default, so every caller names its lane explicitly.
118
+ 2. **Target classification filters within a lane.** The hook-infrastructure patterns and the ephemeral-path check decide whether a target is validated, and with which subset. Classification narrows a lane; it never adds a check the phase already excluded.
119
+ 3. **Changed-line scope filters only the report.** `defer_scope_to_caller` and the changed-line set decide which found violations block. Scope filters findings after every check in the roster already ran; it adds or removes no check.
120
+
121
+ `hooks/hooks_constants/validation_phase_constants.py` is the single source for all three axes: the phase names (`EDIT_LANE_PHASE`, `FULL_GATE_PHASE`), the full-gate-only roster (`ALL_FULL_GATE_ONLY_CHECK_NAMES`), and the hook-infrastructure edit-lane roster (`ALL_HOOK_INFRASTRUCTURE_EDIT_LANE_CHECK_NAMES`).