claude-dev-env 2.31.0 → 2.32.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 (80) hide show
  1. package/bin/install.test.mjs +11 -3
  2. package/docs/CODE_RULES.md +2 -1
  3. package/hooks/blocking/bash_post_call_dispatcher.py +70 -0
  4. package/hooks/blocking/bash_pre_tool_use_dispatcher.py +8 -21
  5. package/hooks/blocking/code_rules_duplicate_body.py +280 -102
  6. package/hooks/blocking/code_rules_enforcer.py +32 -14
  7. package/hooks/blocking/code_rules_enforcer_test_support.py +39 -0
  8. package/hooks/blocking/code_rules_shared.py +64 -14
  9. package/hooks/blocking/pr_description_writer_gate.py +18 -25
  10. package/hooks/blocking/shell_substitution_blocker.py +175 -8
  11. package/hooks/blocking/stop_dispatcher.py +6 -9
  12. package/hooks/blocking/tdd_enforcer.py +11 -5
  13. package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +7 -0
  14. package/hooks/blocking/tdd_enforcer_parts/content_hash_store.py +141 -23
  15. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_hash_store.py +114 -1
  16. package/hooks/blocking/test_bash_post_call_dispatcher.py +112 -0
  17. package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +12 -1
  18. package/hooks/blocking/test_code_rules_enforcer_ephemeral_repository_root.py +118 -0
  19. package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +251 -0
  20. package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +22 -0
  21. package/hooks/blocking/test_pr_description_writer_gate.py +21 -3
  22. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +14 -2
  23. package/hooks/blocking/test_shell_substitution_blocker.py +105 -0
  24. package/hooks/blocking/test_state_description_blocker.py +84 -53
  25. package/hooks/blocking/test_tdd_enforcer.py +131 -1
  26. package/hooks/hooks.json +2 -2
  27. package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +22 -0
  28. package/hooks/hooks_constants/hosted_hook_runner.py +53 -0
  29. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +9 -2
  30. package/hooks/hooks_constants/pr_description_writer_gate_constants.py +34 -0
  31. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +10 -0
  32. package/hooks/hooks_constants/state_file_lock.py +76 -0
  33. package/hooks/hooks_constants/subprocess_budget_completeness_content.py +2 -2
  34. package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +27 -0
  35. package/hooks/hooks_constants/test_failure_recorder_constants.py +35 -0
  36. package/hooks/hooks_constants/test_hosted_hook_runner.py +55 -1
  37. package/hooks/hooks_constants/test_multi_edit_reconstruction.py +1 -0
  38. package/hooks/hooks_constants/test_subprocess_budget_completeness_content.py +33 -0
  39. package/hooks/observability/pr_description_writer_spawn_tracker.py +4 -12
  40. package/hooks/observability/session_file_edit_tracker.py +2 -55
  41. package/hooks/observability/test_failure_recorder.py +219 -0
  42. package/hooks/observability/test_pr_description_writer_spawn_tracker.py +6 -2
  43. package/hooks/observability/test_test_failure_recorder.py +173 -0
  44. package/hooks/validators/abbreviation_checks.py +3 -3
  45. package/hooks/validators/code_quality_checks.py +4 -4
  46. package/hooks/validators/config/baseline_identity_constants.py +12 -0
  47. package/hooks/validators/config/directory_exemption_constants.py +42 -8
  48. package/hooks/validators/config/system_temporary_roots_constants.py +15 -0
  49. package/hooks/validators/config/validator_base_constants.py +9 -0
  50. package/hooks/validators/fast_save_validators.py +23 -80
  51. package/hooks/validators/mypy_integration.py +10 -36
  52. package/hooks/validators/pr_reference_checks.py +2 -2
  53. package/hooks/validators/project_roots.py +58 -0
  54. package/hooks/validators/python_antipattern_checks.py +3 -3
  55. package/hooks/validators/python_style_checks.py +3 -2
  56. package/hooks/validators/react_checks.py +2 -9
  57. package/hooks/validators/run_all_validators.py +247 -148
  58. package/hooks/validators/security_checks.py +3 -3
  59. package/hooks/validators/system_temporary_roots.py +1 -1
  60. package/hooks/validators/test_directory_exemption_constants.py +30 -24
  61. package/hooks/validators/test_fast_save_validators.py +16 -16
  62. package/hooks/validators/test_hook_timing_harness.py +10 -1
  63. package/hooks/validators/test_run_all_validators.py +22 -0
  64. package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +249 -0
  65. package/hooks/validators/test_run_all_validators_pretooluse.py +161 -0
  66. package/hooks/validators/test_run_all_validators_reporting.py +66 -0
  67. package/hooks/validators/test_run_all_validators_repository_root.py +202 -0
  68. package/hooks/validators/test_validator_base.py +51 -2
  69. package/hooks/validators/todo_checks.py +2 -2
  70. package/hooks/validators/type_safety_checks.py +3 -3
  71. package/hooks/validators/useless_test_checks.py +3 -3
  72. package/hooks/validators/validator_base.py +73 -1
  73. package/package.json +1 -1
  74. package/rules/shell-invocation.md +1 -1
  75. package/rules/testing.md +4 -0
  76. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +19 -0
  77. package/scripts/dev_env_scripts_constants/shared_tree_constants.py +17 -0
  78. package/scripts/grok_headless_runner.py +15 -12
  79. package/scripts/shared_tree_paths.py +104 -0
  80. package/scripts/test_shared_tree_paths.py +173 -0
@@ -0,0 +1,104 @@
1
+ """Resolve a scripts directory inside the installed shared tree.
2
+
3
+ An install can place a managed directory behind a directory junction. Path
4
+ resolution walks that link, so a parent computed from the resolved path
5
+ names the link target's parent, where the shared tree does not sit.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from collections.abc import Mapping
11
+ from pathlib import Path
12
+
13
+ from dev_env_scripts_constants.shared_tree_constants import (
14
+ CLAUDE_CONFIG_DIR_ENV_VAR,
15
+ PROCESS_TREE_DIRECTORY_NAME,
16
+ PROCESS_TREE_KILL_MODULE_FILENAME,
17
+ SCRIPTS_DIRECTORY_NAME,
18
+ SHARED_PACKAGE_DIRECTORY_NAME,
19
+ )
20
+
21
+
22
+ def _shared_scripts_directory(from_root: Path, shared_subpackage_name: str) -> Path:
23
+ """Return the scripts directory one named sub-package holds under a root."""
24
+ return (
25
+ from_root
26
+ / SHARED_PACKAGE_DIRECTORY_NAME
27
+ / shared_subpackage_name
28
+ / SCRIPTS_DIRECTORY_NAME
29
+ )
30
+
31
+
32
+ def resolve_shared_scripts_directory(
33
+ module_file: str | Path,
34
+ all_environment: Mapping[str, str],
35
+ shared_subpackage_name: str,
36
+ marker_filename: str,
37
+ anchor_depth: int,
38
+ ) -> Path:
39
+ """Return the first shared scripts directory that holds the marker file.
40
+
41
+ ::
42
+
43
+ the managed scripts directory is a junction
44
+ ok: an un-resolved parent keeps the link side, where _shared sits
45
+ flag: a resolved parent walks the link and misses _shared entirely
46
+
47
+ Tries the un-resolved parent first, then the configured managed root,
48
+ then the resolved parent. When none holds the marker, returns the
49
+ un-resolved candidate, so an import failure names a readable path.
50
+
51
+ Args:
52
+ module_file: Path of the importing module (``__file__``).
53
+ all_environment: Mapping that may hold ``CLAUDE_CONFIG_DIR``.
54
+ shared_subpackage_name: Directory under ``_shared`` to resolve.
55
+ marker_filename: File whose presence marks the right directory.
56
+ anchor_depth: Parent levels above the module to treat as the root.
57
+
58
+ Returns:
59
+ Directory that should hold the marker file.
60
+ """
61
+ module_path = Path(module_file)
62
+ un_resolved_candidate = _shared_scripts_directory(
63
+ module_path.absolute().parents[anchor_depth], shared_subpackage_name
64
+ )
65
+ all_candidates = [un_resolved_candidate]
66
+ configured_root_text = all_environment.get(CLAUDE_CONFIG_DIR_ENV_VAR, "").strip()
67
+ if configured_root_text:
68
+ all_candidates.append(
69
+ _shared_scripts_directory(Path(configured_root_text), shared_subpackage_name)
70
+ )
71
+ all_candidates.append(
72
+ _shared_scripts_directory(
73
+ module_path.resolve().parents[anchor_depth], shared_subpackage_name
74
+ )
75
+ )
76
+ for each_candidate in all_candidates:
77
+ if (each_candidate / marker_filename).is_file():
78
+ return each_candidate
79
+ return un_resolved_candidate
80
+
81
+
82
+ def resolve_shared_process_tree_scripts_directory(
83
+ module_file: str | Path,
84
+ all_environment: Mapping[str, str],
85
+ ) -> Path:
86
+ """Return the process-tree scripts directory that holds the kill helper.
87
+
88
+ Names the process-tree sub-package for a caller sitting one level under
89
+ the installed root, such as a script in the managed ``scripts`` directory.
90
+
91
+ Args:
92
+ module_file: Path of the importing module (``__file__``).
93
+ all_environment: Mapping that may hold ``CLAUDE_CONFIG_DIR``.
94
+
95
+ Returns:
96
+ Directory that should hold ``process_tree_kill.py``.
97
+ """
98
+ return resolve_shared_scripts_directory(
99
+ module_file,
100
+ all_environment,
101
+ PROCESS_TREE_DIRECTORY_NAME,
102
+ PROCESS_TREE_KILL_MODULE_FILENAME,
103
+ 1,
104
+ )
@@ -0,0 +1,173 @@
1
+ """Behavioral tests for shared-tree directory resolution."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib
6
+ import os
7
+ import sys
8
+ from pathlib import Path
9
+
10
+ import pytest
11
+
12
+ _SCRIPTS_DIR = Path(__file__).resolve().parent
13
+ if str(_SCRIPTS_DIR) not in sys.path:
14
+ sys.path.insert(0, str(_SCRIPTS_DIR))
15
+
16
+ import shared_tree_paths # noqa: E402
17
+ from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
18
+ CLAUDE_CONFIG_DIR_ENV_VAR,
19
+ PROCESS_TREE_DIRECTORY_NAME,
20
+ PROCESS_TREE_KILL_MODULE_FILENAME,
21
+ SCRIPTS_DIRECTORY_NAME,
22
+ SHARED_PACKAGE_DIRECTORY_NAME,
23
+ )
24
+
25
+
26
+ def _create_directory_link(*, from_link: Path, to_target: Path) -> None:
27
+ if sys.platform.startswith("win32"):
28
+ importlib.import_module("_winapi").CreateJunction(
29
+ str(to_target), str(from_link)
30
+ )
31
+ return
32
+ try:
33
+ os.symlink(to_target, from_link, target_is_directory=True)
34
+ except OSError:
35
+ pytest.skip("directory symlink creation is not available")
36
+
37
+
38
+ def _build_junction_scripts_layout(tmp_path: Path) -> Path:
39
+ real_scripts_directory = tmp_path / "real" / SCRIPTS_DIRECTORY_NAME
40
+ real_scripts_directory.mkdir(parents=True)
41
+ (real_scripts_directory / "runner.py").write_text("", encoding="utf-8")
42
+ managed_root = tmp_path / "root"
43
+ managed_root.mkdir()
44
+ _create_directory_link(
45
+ from_link=managed_root / SCRIPTS_DIRECTORY_NAME,
46
+ to_target=real_scripts_directory,
47
+ )
48
+ return managed_root / SCRIPTS_DIRECTORY_NAME / "runner.py"
49
+
50
+
51
+ def _write_process_tree_kill_stub(from_root: Path) -> Path:
52
+ scripts_directory = (
53
+ from_root
54
+ / SHARED_PACKAGE_DIRECTORY_NAME
55
+ / PROCESS_TREE_DIRECTORY_NAME
56
+ / SCRIPTS_DIRECTORY_NAME
57
+ )
58
+ scripts_directory.mkdir(parents=True)
59
+ (scripts_directory / PROCESS_TREE_KILL_MODULE_FILENAME).write_text(
60
+ "", encoding="utf-8"
61
+ )
62
+ return scripts_directory
63
+
64
+
65
+ def test_junction_scripts_dir_finds_shared_tree_on_unresolved_parent(
66
+ tmp_path: Path,
67
+ ) -> None:
68
+ module_file = _build_junction_scripts_layout(tmp_path)
69
+ expected_scripts_directory = _write_process_tree_kill_stub(tmp_path / "root")
70
+ resolved_scripts_directory = (
71
+ shared_tree_paths.resolve_shared_process_tree_scripts_directory(
72
+ module_file, all_environment={}
73
+ )
74
+ )
75
+ assert resolved_scripts_directory == expected_scripts_directory
76
+
77
+
78
+ def test_resolved_parent_is_used_when_link_side_has_no_shared_tree(
79
+ tmp_path: Path,
80
+ ) -> None:
81
+ module_file = _build_junction_scripts_layout(tmp_path)
82
+ expected_scripts_directory = _write_process_tree_kill_stub(tmp_path / "real")
83
+ resolved_scripts_directory = (
84
+ shared_tree_paths.resolve_shared_process_tree_scripts_directory(
85
+ module_file, all_environment={}
86
+ )
87
+ )
88
+ assert resolved_scripts_directory == expected_scripts_directory
89
+
90
+
91
+ def test_configured_root_is_used_when_neither_parent_holds_the_module(
92
+ tmp_path: Path,
93
+ ) -> None:
94
+ module_file = _build_junction_scripts_layout(tmp_path)
95
+ configured_root = tmp_path / "configured"
96
+ expected_scripts_directory = _write_process_tree_kill_stub(configured_root)
97
+ resolved_scripts_directory = (
98
+ shared_tree_paths.resolve_shared_process_tree_scripts_directory(
99
+ module_file,
100
+ all_environment={
101
+ CLAUDE_CONFIG_DIR_ENV_VAR: str(configured_root)
102
+ },
103
+ )
104
+ )
105
+ assert resolved_scripts_directory == expected_scripts_directory
106
+
107
+
108
+ def test_unresolved_parent_wins_when_configured_root_also_holds_the_module(
109
+ tmp_path: Path,
110
+ ) -> None:
111
+ module_file = _build_junction_scripts_layout(tmp_path)
112
+ expected_scripts_directory = _write_process_tree_kill_stub(tmp_path / "root")
113
+ configured_root = tmp_path / "configured"
114
+ _write_process_tree_kill_stub(configured_root)
115
+ resolved_scripts_directory = (
116
+ shared_tree_paths.resolve_shared_process_tree_scripts_directory(
117
+ module_file,
118
+ all_environment={
119
+ CLAUDE_CONFIG_DIR_ENV_VAR: str(configured_root)
120
+ },
121
+ )
122
+ )
123
+ assert resolved_scripts_directory == expected_scripts_directory
124
+
125
+
126
+ def test_missing_module_returns_unresolved_candidate(tmp_path: Path) -> None:
127
+ module_file = _build_junction_scripts_layout(tmp_path)
128
+ expected_scripts_directory = (
129
+ tmp_path
130
+ / "root"
131
+ / SHARED_PACKAGE_DIRECTORY_NAME
132
+ / PROCESS_TREE_DIRECTORY_NAME
133
+ / SCRIPTS_DIRECTORY_NAME
134
+ )
135
+ resolved_scripts_directory = (
136
+ shared_tree_paths.resolve_shared_process_tree_scripts_directory(
137
+ module_file, all_environment={}
138
+ )
139
+ )
140
+ assert resolved_scripts_directory == expected_scripts_directory
141
+ assert not (
142
+ expected_scripts_directory / PROCESS_TREE_KILL_MODULE_FILENAME
143
+ ).is_file()
144
+
145
+
146
+ def test_generalized_resolver_serves_a_deeper_anchor(tmp_path: Path) -> None:
147
+ """A caller three levels under the root resolves its own sub-package.
148
+
149
+ This is the shape the pr-loop and codex-review scripts carry, which the
150
+ process-tree wrapper's own anchor of one level cannot reach.
151
+ """
152
+ installed_root = tmp_path / "root"
153
+ module_directory = installed_root / "_shared" / "pr-loop" / "scripts"
154
+ module_directory.mkdir(parents=True)
155
+ module_file = str(module_directory / "run_codex_review.py")
156
+ expected_scripts_directory = (
157
+ installed_root
158
+ / SHARED_PACKAGE_DIRECTORY_NAME
159
+ / PROCESS_TREE_DIRECTORY_NAME
160
+ / SCRIPTS_DIRECTORY_NAME
161
+ )
162
+ expected_scripts_directory.mkdir(parents=True)
163
+ (expected_scripts_directory / PROCESS_TREE_KILL_MODULE_FILENAME).write_text(
164
+ "", encoding="utf-8"
165
+ )
166
+ resolved_scripts_directory = shared_tree_paths.resolve_shared_scripts_directory(
167
+ module_file,
168
+ {},
169
+ PROCESS_TREE_DIRECTORY_NAME,
170
+ PROCESS_TREE_KILL_MODULE_FILENAME,
171
+ 3,
172
+ )
173
+ assert resolved_scripts_directory == expected_scripts_directory