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.
- package/bin/install.test.mjs +11 -3
- package/docs/CODE_RULES.md +2 -1
- package/hooks/blocking/bash_post_call_dispatcher.py +70 -0
- package/hooks/blocking/bash_pre_tool_use_dispatcher.py +8 -21
- package/hooks/blocking/code_rules_duplicate_body.py +280 -102
- package/hooks/blocking/code_rules_enforcer.py +32 -14
- package/hooks/blocking/code_rules_enforcer_test_support.py +39 -0
- package/hooks/blocking/code_rules_shared.py +64 -14
- package/hooks/blocking/pr_description_writer_gate.py +18 -25
- package/hooks/blocking/shell_substitution_blocker.py +175 -8
- package/hooks/blocking/stop_dispatcher.py +6 -9
- package/hooks/blocking/tdd_enforcer.py +11 -5
- package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +7 -0
- package/hooks/blocking/tdd_enforcer_parts/content_hash_store.py +141 -23
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_hash_store.py +114 -1
- package/hooks/blocking/test_bash_post_call_dispatcher.py +112 -0
- package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +12 -1
- package/hooks/blocking/test_code_rules_enforcer_ephemeral_repository_root.py +118 -0
- package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +251 -0
- package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +22 -0
- package/hooks/blocking/test_pr_description_writer_gate.py +21 -3
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +14 -2
- package/hooks/blocking/test_shell_substitution_blocker.py +105 -0
- package/hooks/blocking/test_state_description_blocker.py +84 -53
- package/hooks/blocking/test_tdd_enforcer.py +131 -1
- package/hooks/hooks.json +2 -2
- package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +22 -0
- package/hooks/hooks_constants/hosted_hook_runner.py +53 -0
- package/hooks/hooks_constants/piped_pytest_blocker_constants.py +9 -2
- package/hooks/hooks_constants/pr_description_writer_gate_constants.py +34 -0
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +10 -0
- package/hooks/hooks_constants/state_file_lock.py +76 -0
- package/hooks/hooks_constants/subprocess_budget_completeness_content.py +2 -2
- package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +27 -0
- package/hooks/hooks_constants/test_failure_recorder_constants.py +35 -0
- package/hooks/hooks_constants/test_hosted_hook_runner.py +55 -1
- package/hooks/hooks_constants/test_multi_edit_reconstruction.py +1 -0
- package/hooks/hooks_constants/test_subprocess_budget_completeness_content.py +33 -0
- package/hooks/observability/pr_description_writer_spawn_tracker.py +4 -12
- package/hooks/observability/session_file_edit_tracker.py +2 -55
- package/hooks/observability/test_failure_recorder.py +219 -0
- package/hooks/observability/test_pr_description_writer_spawn_tracker.py +6 -2
- package/hooks/observability/test_test_failure_recorder.py +173 -0
- package/hooks/validators/abbreviation_checks.py +3 -3
- package/hooks/validators/code_quality_checks.py +4 -4
- package/hooks/validators/config/baseline_identity_constants.py +12 -0
- package/hooks/validators/config/directory_exemption_constants.py +42 -8
- package/hooks/validators/config/system_temporary_roots_constants.py +15 -0
- package/hooks/validators/config/validator_base_constants.py +9 -0
- package/hooks/validators/fast_save_validators.py +23 -80
- package/hooks/validators/mypy_integration.py +10 -36
- package/hooks/validators/pr_reference_checks.py +2 -2
- package/hooks/validators/project_roots.py +58 -0
- package/hooks/validators/python_antipattern_checks.py +3 -3
- package/hooks/validators/python_style_checks.py +3 -2
- package/hooks/validators/react_checks.py +2 -9
- package/hooks/validators/run_all_validators.py +247 -148
- package/hooks/validators/security_checks.py +3 -3
- package/hooks/validators/system_temporary_roots.py +1 -1
- package/hooks/validators/test_directory_exemption_constants.py +30 -24
- package/hooks/validators/test_fast_save_validators.py +16 -16
- package/hooks/validators/test_hook_timing_harness.py +10 -1
- package/hooks/validators/test_run_all_validators.py +22 -0
- package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +249 -0
- package/hooks/validators/test_run_all_validators_pretooluse.py +161 -0
- package/hooks/validators/test_run_all_validators_reporting.py +66 -0
- package/hooks/validators/test_run_all_validators_repository_root.py +202 -0
- package/hooks/validators/test_validator_base.py +51 -2
- package/hooks/validators/todo_checks.py +2 -2
- package/hooks/validators/type_safety_checks.py +3 -3
- package/hooks/validators/useless_test_checks.py +3 -3
- package/hooks/validators/validator_base.py +73 -1
- package/package.json +1 -1
- package/rules/shell-invocation.md +1 -1
- package/rules/testing.md +4 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +19 -0
- package/scripts/dev_env_scripts_constants/shared_tree_constants.py +17 -0
- package/scripts/grok_headless_runner.py +15 -12
- package/scripts/shared_tree_paths.py +104 -0
- 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
|