claude-dev-env 2.31.0 → 2.31.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.
- package/docs/CODE_RULES.md +2 -1
- package/hooks/blocking/code_rules_duplicate_body.py +272 -100
- package/hooks/blocking/code_rules_enforcer.py +32 -14
- package/hooks/blocking/code_rules_shared.py +64 -14
- package/hooks/blocking/pr_description_writer_gate.py +18 -25
- package/hooks/blocking/tdd_enforcer.py +8 -4
- 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 +244 -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 +9 -2
- package/hooks/blocking/test_state_description_blocker.py +99 -53
- package/hooks/blocking/test_tdd_enforcer.py +64 -0
- package/hooks/hooks_constants/pr_description_writer_gate_constants.py +34 -0
- package/hooks/hooks_constants/subprocess_budget_completeness_content.py +2 -2
- 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/test_pr_description_writer_spawn_tracker.py +6 -2
- 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 +44 -1
- package/hooks/validators/config/validator_base_constants.py +9 -0
- package/hooks/validators/fast_save_validators.py +23 -80
- package/hooks/validators/pr_reference_checks.py +2 -2
- 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 +190 -145
- package/hooks/validators/security_checks.py +3 -3
- 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_pretooluse.py +161 -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/scripts/dev_env_scripts_constants/grok_worker_constants.py +15 -0
- package/scripts/grok_headless_runner.py +15 -12
- package/scripts/shared_tree_paths.py +70 -0
- package/scripts/test_shared_tree_paths.py +143 -0
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
"""Base classes for validators.
|
|
2
2
|
|
|
3
|
-
Provides shared dataclasses used across all validator modules
|
|
3
|
+
Provides shared dataclasses used across all validator modules, and the one
|
|
4
|
+
read and the one parse each of those modules shares for a given file.
|
|
4
5
|
"""
|
|
5
6
|
|
|
7
|
+
import ast
|
|
6
8
|
from dataclasses import dataclass
|
|
9
|
+
from functools import lru_cache
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
from .config.validator_base_constants import SOURCE_CACHE_ENTRIES, SOURCE_ENCODING
|
|
7
13
|
|
|
8
14
|
|
|
9
15
|
@dataclass(frozen=True)
|
|
@@ -17,3 +23,69 @@ class Violation:
|
|
|
17
23
|
def __str__(self) -> str:
|
|
18
24
|
"""Format as file:line: message."""
|
|
19
25
|
return f"{self.file}:{self.line}: {self.message}"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass(frozen=True)
|
|
29
|
+
class ValidatorResult:
|
|
30
|
+
"""Result from running a validator."""
|
|
31
|
+
|
|
32
|
+
name: str
|
|
33
|
+
checks: str
|
|
34
|
+
passed: bool
|
|
35
|
+
output: str
|
|
36
|
+
skipped: bool = False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@lru_cache(maxsize=SOURCE_CACHE_ENTRIES)
|
|
40
|
+
def _source_text_for_stat(
|
|
41
|
+
file_path_text: str, modified_nanoseconds: int, size_in_bytes: int
|
|
42
|
+
) -> str:
|
|
43
|
+
"""Read one file, keyed on the stat that identifies this version of it."""
|
|
44
|
+
return Path(file_path_text).read_text(encoding=SOURCE_ENCODING)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def source_text(file_path: Path) -> str:
|
|
48
|
+
"""Return the file's text, reading it once however many checks ask.
|
|
49
|
+
|
|
50
|
+
Every check the save-path gate runs reads the same file. The gate hands
|
|
51
|
+
each of them one freshly written target, so the read is repeated work.
|
|
52
|
+
The cache key carries the modification time and size, so an edited file
|
|
53
|
+
is read again.
|
|
54
|
+
|
|
55
|
+
That key has one blind spot: a rewrite that leaves the size unchanged
|
|
56
|
+
inside a single modification-time tick is served from the cache. A hook
|
|
57
|
+
process handles one payload, so it cannot happen there; a long CLI pass
|
|
58
|
+
over many files is where a reader needs to know.
|
|
59
|
+
|
|
60
|
+
Raises whatever ``read_text`` raises, so a caller's own error handling
|
|
61
|
+
is unchanged.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
file_path: The file to read.
|
|
65
|
+
|
|
66
|
+
Returns:
|
|
67
|
+
The file's decoded text.
|
|
68
|
+
"""
|
|
69
|
+
file_status = file_path.stat()
|
|
70
|
+
return _source_text_for_stat(
|
|
71
|
+
str(file_path), file_status.st_mtime_ns, file_status.st_size
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@lru_cache(maxsize=SOURCE_CACHE_ENTRIES)
|
|
76
|
+
def syntax_tree(source: str) -> ast.Module:
|
|
77
|
+
"""Return the parsed tree for source, parsing it once however many checks ask.
|
|
78
|
+
|
|
79
|
+
No check mutates the tree; each one walks it. So one parse serves all of
|
|
80
|
+
them, and the tree is shared rather than copied.
|
|
81
|
+
|
|
82
|
+
Raises ``SyntaxError`` exactly as ``ast.parse`` does, so a caller's own
|
|
83
|
+
error handling is unchanged.
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
source: The file's text.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
The parsed module.
|
|
90
|
+
"""
|
|
91
|
+
return ast.parse(source)
|
package/package.json
CHANGED
|
@@ -56,6 +56,21 @@ DEFAULT_PING_TIMEOUT_SECONDS: int = 120
|
|
|
56
56
|
CLAUDE_HOME_SUBDIRECTORY: str = ".claude"
|
|
57
57
|
"""Per-user directory under the home directory that holds Claude config."""
|
|
58
58
|
|
|
59
|
+
CLAUDE_CONFIG_DIR_ENV_VAR: str = "CLAUDE_CONFIG_DIR"
|
|
60
|
+
"""Environment variable naming the managed Claude config root."""
|
|
61
|
+
|
|
62
|
+
PROCESS_TREE_KILL_MODULE_FILENAME: str = "process_tree_kill.py"
|
|
63
|
+
"""Filename of the shared process-tree kill helper."""
|
|
64
|
+
|
|
65
|
+
SHARED_PACKAGE_DIRECTORY_NAME: str = "_shared"
|
|
66
|
+
"""Directory name of the shared install tree under the managed root."""
|
|
67
|
+
|
|
68
|
+
PROCESS_TREE_DIRECTORY_NAME: str = "process-tree"
|
|
69
|
+
"""Directory name of the process-tree helper package under ``_shared``."""
|
|
70
|
+
|
|
71
|
+
SCRIPTS_DIRECTORY_NAME: str = "scripts"
|
|
72
|
+
"""Directory name holding Python helpers under a process-tree package."""
|
|
73
|
+
|
|
59
74
|
MANIFEST_FILENAME: str = ".claude-dev-env-manifest.json"
|
|
60
75
|
"""Install manifest written by ``claude-dev-env`` into the user Claude home."""
|
|
61
76
|
|
|
@@ -25,24 +25,14 @@ Import ``run_headless_worker`` for the outcome object::
|
|
|
25
25
|
|
|
26
26
|
from __future__ import annotations
|
|
27
27
|
|
|
28
|
+
import os
|
|
28
29
|
import subprocess
|
|
29
30
|
import sys
|
|
30
31
|
import uuid
|
|
31
32
|
from dataclasses import dataclass
|
|
32
33
|
from pathlib import Path
|
|
33
34
|
|
|
34
|
-
|
|
35
|
-
Path(__file__).resolve().parents[1] / "_shared" / "process-tree" / "scripts"
|
|
36
|
-
)
|
|
37
|
-
if str(_shared_process_tree_scripts_directory) not in sys.path:
|
|
38
|
-
sys.path.insert(0, str(_shared_process_tree_scripts_directory))
|
|
39
|
-
|
|
40
|
-
from process_tree_kill import ( # noqa: E402
|
|
41
|
-
should_start_new_session,
|
|
42
|
-
terminate_process_tree,
|
|
43
|
-
)
|
|
44
|
-
|
|
45
|
-
from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
|
|
35
|
+
from dev_env_scripts_constants.grok_worker_constants import (
|
|
46
36
|
AGENT_FLAG,
|
|
47
37
|
ALL_AUTH_FAILURE_SIGNATURES,
|
|
48
38
|
ALL_USAGE_LIMIT_SIGNATURES,
|
|
@@ -81,6 +71,19 @@ from dev_env_scripts_constants.grok_worker_constants import ( # noqa: E402
|
|
|
81
71
|
UTF8_ENCODING,
|
|
82
72
|
WORKER_SPEC_TIMEOUT_KEY,
|
|
83
73
|
)
|
|
74
|
+
from shared_tree_paths import resolve_shared_process_tree_scripts_directory
|
|
75
|
+
|
|
76
|
+
_shared_process_tree_scripts_directory = resolve_shared_process_tree_scripts_directory(
|
|
77
|
+
__file__,
|
|
78
|
+
all_environment=os.environ,
|
|
79
|
+
)
|
|
80
|
+
if str(_shared_process_tree_scripts_directory) not in sys.path:
|
|
81
|
+
sys.path.insert(0, str(_shared_process_tree_scripts_directory))
|
|
82
|
+
|
|
83
|
+
from process_tree_kill import ( # noqa: E402
|
|
84
|
+
should_start_new_session,
|
|
85
|
+
terminate_process_tree,
|
|
86
|
+
)
|
|
84
87
|
|
|
85
88
|
runner_popen = subprocess.Popen
|
|
86
89
|
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""Resolve directories inside the installed shared tree."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from dev_env_scripts_constants.grok_worker_constants import (
|
|
9
|
+
CLAUDE_CONFIG_DIR_ENV_VAR,
|
|
10
|
+
PROCESS_TREE_DIRECTORY_NAME,
|
|
11
|
+
PROCESS_TREE_KILL_MODULE_FILENAME,
|
|
12
|
+
SCRIPTS_DIRECTORY_NAME,
|
|
13
|
+
SHARED_PACKAGE_DIRECTORY_NAME,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _process_tree_scripts_directory(from_root: Path) -> Path:
|
|
18
|
+
return (
|
|
19
|
+
from_root
|
|
20
|
+
/ SHARED_PACKAGE_DIRECTORY_NAME
|
|
21
|
+
/ PROCESS_TREE_DIRECTORY_NAME
|
|
22
|
+
/ SCRIPTS_DIRECTORY_NAME
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _has_process_tree_kill_module(from_directory: Path) -> bool:
|
|
27
|
+
return (from_directory / PROCESS_TREE_KILL_MODULE_FILENAME).is_file()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def resolve_shared_process_tree_scripts_directory(
|
|
31
|
+
module_file: str | Path,
|
|
32
|
+
all_environment: Mapping[str, str],
|
|
33
|
+
) -> Path:
|
|
34
|
+
"""Return the first process-tree scripts directory that holds the kill helper.
|
|
35
|
+
|
|
36
|
+
::
|
|
37
|
+
|
|
38
|
+
scripts dir is a junction; the kill helper sits beside the un-resolved parent
|
|
39
|
+
ok: Path(module_file).absolute().parent.parent then the _shared scripts dir
|
|
40
|
+
flag: Path(module_file).resolve().parents[1] walks the junction and misses it
|
|
41
|
+
|
|
42
|
+
Tries the un-resolved parent first, then the configured managed root, then
|
|
43
|
+
the resolved parent. When none hold the helper, return the un-resolved
|
|
44
|
+
candidate so import failure names a readable path.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
module_file: Path of the importing module (``__file__``).
|
|
48
|
+
all_environment: Mapping that may hold ``CLAUDE_CONFIG_DIR``.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Directory that should hold ``process_tree_kill.py``.
|
|
52
|
+
"""
|
|
53
|
+
un_resolved_candidate = _process_tree_scripts_directory(
|
|
54
|
+
Path(module_file).absolute().parent.parent
|
|
55
|
+
)
|
|
56
|
+
all_candidates = [un_resolved_candidate]
|
|
57
|
+
configured_root_text = all_environment.get(
|
|
58
|
+
CLAUDE_CONFIG_DIR_ENV_VAR, ""
|
|
59
|
+
).strip()
|
|
60
|
+
if configured_root_text:
|
|
61
|
+
all_candidates.append(
|
|
62
|
+
_process_tree_scripts_directory(Path(configured_root_text))
|
|
63
|
+
)
|
|
64
|
+
all_candidates.append(
|
|
65
|
+
_process_tree_scripts_directory(Path(module_file).resolve().parents[1])
|
|
66
|
+
)
|
|
67
|
+
for each_candidate in all_candidates:
|
|
68
|
+
if _has_process_tree_kill_module(each_candidate):
|
|
69
|
+
return each_candidate
|
|
70
|
+
return un_resolved_candidate
|
|
@@ -0,0 +1,143 @@
|
|
|
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()
|