claude-dev-env 2.30.1 → 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/.agents/agents/pr-description-writer.md +1 -1
- package/AGENTS.md +6 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +37 -0
- package/bin/install.prune.test.mjs +4 -2
- package/docs/CODE_RULES.md +12 -0
- package/hooks/advisory/migration_safety_advisor.py +25 -1
- package/hooks/advisory/refactor_guard.py +85 -18
- package/hooks/advisory/test_migration_safety_advisor.py +89 -5
- package/hooks/advisory/test_refactor_guard.py +87 -0
- package/hooks/advisory/test_refactor_guard_advisory.py +72 -1
- package/hooks/blocking/code_rules_constants_config.py +7 -6
- package/hooks/blocking/code_rules_duplicate_body.py +272 -100
- package/hooks/blocking/code_rules_enforcer.py +89 -45
- package/hooks/blocking/code_rules_shared.py +64 -14
- package/hooks/blocking/codex_apply_patch.py +136 -21
- package/hooks/blocking/config/codex_apply_patch_constants.py +13 -0
- package/hooks/blocking/conftest.py +70 -0
- package/hooks/blocking/duplicate_rmtree_helper_blocker.py +26 -16
- package/hooks/blocking/hook_prose_detector_consistency.py +30 -20
- package/hooks/blocking/pii_payload_scan.py +61 -0
- package/hooks/blocking/pii_prevention_blocker.py +5 -0
- package/hooks/blocking/pr_description_writer_gate.py +256 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +1 -1
- package/hooks/blocking/sensitive_file_protector.py +18 -3
- package/hooks/blocking/shell_substitution_blocker.py +25 -47
- package/hooks/blocking/stale_comment_reference_blocker.py +94 -24
- package/hooks/blocking/state_description_blocker.py +39 -26
- package/hooks/blocking/subprocess_budget_completeness.py +17 -43
- package/hooks/blocking/tdd_enforcer.py +63 -16
- package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +11 -1
- package/hooks/blocking/tdd_enforcer_parts/content_hash_store.py +180 -0
- package/hooks/blocking/tdd_enforcer_parts/decisions.py +6 -2
- package/hooks/blocking/tdd_enforcer_parts/freshness.py +18 -3
- package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +66 -1
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_hash_store.py +143 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +12 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +63 -0
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +81 -2
- package/hooks/blocking/test_code_rules_enforcer_codex_apply_patch.py +35 -0
- package/hooks/blocking/test_code_rules_enforcer_config_path.py +29 -16
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +2 -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_multi_edit.py +118 -0
- package/hooks/blocking/test_code_rules_enforcer_narrow_edit.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_precheck_forecast.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +244 -0
- package/hooks/blocking/test_code_rules_enforcer_split_constants_config.py +29 -28
- package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +24 -0
- package/hooks/blocking/test_code_rules_enforcer_stage.py +2 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +2 -0
- package/hooks/blocking/test_codex_apply_patch.py +43 -0
- package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +68 -0
- package/hooks/blocking/test_hook_prose_detector_consistency.py +41 -0
- package/hooks/blocking/test_pii_payload_scan.py +65 -2
- package/hooks/blocking/test_pii_prevention_blocker.py +80 -10
- package/hooks/blocking/test_pr_description_writer_gate.py +234 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +215 -25
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +18 -6
- package/hooks/blocking/test_sensitive_file_protector.py +42 -2
- package/hooks/blocking/test_stale_comment_reference_blocker.py +71 -0
- package/hooks/blocking/test_state_description_blocker.py +163 -49
- package/hooks/blocking/test_subprocess_budget_completeness.py +67 -0
- package/hooks/blocking/test_tdd_enforcer.py +246 -2
- package/hooks/blocking/test_windows_rmtree_blocker.py +63 -0
- package/hooks/blocking/test_write_existing_file_blocker.py +122 -0
- package/hooks/blocking/windows_rmtree_blocker.py +21 -7
- package/hooks/blocking/write_existing_file_blocker.py +75 -29
- package/hooks/conftest.py +47 -0
- package/hooks/hooks.json +7 -12
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +3 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +4 -0
- package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +3 -0
- package/hooks/hooks_constants/hook_prose_detector_consistency_constants.py +7 -2
- package/hooks/hooks_constants/migration_safety_advisor_constants.py +7 -0
- package/hooks/hooks_constants/multi_edit_reconstruction.py +44 -3
- package/hooks/hooks_constants/pii_prevention_constants.py +9 -7
- package/hooks/hooks_constants/pr_description_writer_gate_constants.py +73 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +31 -14
- package/hooks/hooks_constants/session_edit_stage_gate_constants.py +15 -1
- package/hooks/hooks_constants/state_description_blocker_constants.py +1 -0
- package/hooks/hooks_constants/subprocess_budget_completeness_content.py +71 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +2 -0
- package/hooks/hooks_constants/test_code_rules_enforcer_constants.py +2 -0
- package/hooks/hooks_constants/test_multi_edit_reconstruction.py +103 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +139 -25
- package/hooks/hooks_constants/test_subprocess_budget_completeness_content.py +103 -0
- package/hooks/hooks_constants/validation_phase_constants.py +2 -0
- package/hooks/hooks_constants/workflow_substitution_slot_blocker_constants.py +7 -3
- package/hooks/observability/pr_description_writer_spawn_tracker.py +113 -0
- package/hooks/observability/session_file_edit_tracker.py +25 -4
- package/hooks/observability/test_pr_description_writer_spawn_tracker.py +150 -0
- package/hooks/observability/test_session_file_edit_tracker.py +32 -0
- package/hooks/validation/hook_format_validator.py +23 -9
- package/hooks/validation/test_hook_format_validator.py +58 -0
- package/hooks/validators/README.md +1 -1
- package/hooks/validators/abbreviation_checks.py +6 -8
- package/hooks/validators/code_quality_checks.py +4 -4
- package/hooks/validators/config/abbreviation_checks_constants.py +3 -0
- package/hooks/validators/config/baseline_identity_constants.py +12 -0
- package/hooks/validators/config/directory_exemption_constants.py +44 -1
- package/hooks/validators/config/fast_save_validator_constants.py +32 -0
- package/hooks/validators/config/hook_timing_harness_constants.py +3 -1
- package/hooks/validators/config/test_abbreviation_checks_constants.py +11 -0
- package/hooks/validators/config/validator_base_constants.py +9 -0
- package/hooks/validators/fast_save_validators.py +275 -0
- package/hooks/validators/hook_timing_harness.py +8 -4
- 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 +37 -62
- package/hooks/validators/python_style_helpers.py +16 -2
- package/hooks/validators/python_style_import_bootstrap.py +83 -0
- package/hooks/validators/react_checks.py +2 -9
- package/hooks/validators/run_all_validators.py +201 -155
- package/hooks/validators/security_checks.py +3 -3
- package/hooks/validators/test_abbreviation_checks.py +20 -0
- package/hooks/validators/test_directory_exemption_constants.py +30 -24
- package/hooks/validators/test_fast_save_validators.py +172 -0
- package/hooks/validators/test_hook_timing_harness.py +10 -1
- package/hooks/validators/test_python_style_checks.py +9 -0
- package/hooks/validators/test_python_style_checks_import_order.py +81 -0
- package/hooks/validators/test_python_style_helpers.py +35 -0
- package/hooks/validators/test_python_style_import_bootstrap.py +69 -0
- package/hooks/validators/test_run_all_validators.py +46 -0
- package/hooks/validators/test_run_all_validators_pretooluse.py +163 -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/docstring-prose-matches-implementation.md +1 -1
- package/rules/git-workflow.md +2 -0
- package/rules/testing.md +4 -0
- package/scripts/codex_compat_materializer.py +5 -0
- 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
- package/scripts/tests/test_codex_compat_materializer.py +37 -11
- package/hooks/blocking/tests/conftest.py +0 -10
|
@@ -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()
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import json
|
|
2
|
+
import os
|
|
2
3
|
import subprocess
|
|
3
4
|
import sys
|
|
4
5
|
from pathlib import Path
|
|
@@ -841,23 +842,35 @@ def test_build_plan_publishes_owned_agents_projection_and_tracks_drift(
|
|
|
841
842
|
assert drifted_projection != first_projection
|
|
842
843
|
|
|
843
844
|
|
|
844
|
-
def
|
|
845
|
+
def test_codex_hook_manifest_folds_apply_patch_into_the_dispatcher_matcher() -> None:
|
|
846
|
+
"""apply_patch's code_rules_enforcer coverage comes from the dispatcher's own matcher.
|
|
847
|
+
|
|
848
|
+
hooks.json once ran a separate apply_patch -> code_rules_enforcer.py
|
|
849
|
+
PreToolUse command after the dispatcher entry. That coverage is now
|
|
850
|
+
folded into the dispatcher's own Write|Edit|MultiEdit|apply_patch
|
|
851
|
+
matcher, so no standalone apply_patch entry remains.
|
|
852
|
+
"""
|
|
845
853
|
hooks_path = Path(__file__).parents[2] / "hooks" / "hooks.json"
|
|
846
854
|
hooks_configuration = json.loads(hooks_path.read_text(encoding="utf-8"))
|
|
847
855
|
pre_tool_use_entries = hooks_configuration["hooks"]["PreToolUse"]
|
|
848
|
-
matcher_names = [each_entry.get("matcher") for each_entry in pre_tool_use_entries]
|
|
849
856
|
|
|
850
|
-
|
|
851
|
-
|
|
857
|
+
dispatcher_entries = [
|
|
858
|
+
each_entry
|
|
859
|
+
for each_entry in pre_tool_use_entries
|
|
860
|
+
if any(
|
|
861
|
+
"/hooks/blocking/pre_tool_use_dispatcher.py" in each_hook.get("command", "")
|
|
862
|
+
for each_hook in each_entry.get("hooks", [])
|
|
863
|
+
)
|
|
864
|
+
]
|
|
865
|
+
assert len(dispatcher_entries) == 1
|
|
866
|
+
assert dispatcher_entries[0]["matcher"] == "Write|Edit|MultiEdit|apply_patch"
|
|
867
|
+
|
|
868
|
+
standalone_apply_patch_entries = [
|
|
852
869
|
each_entry
|
|
853
870
|
for each_entry in pre_tool_use_entries
|
|
854
871
|
if each_entry.get("matcher") == "apply_patch"
|
|
855
872
|
]
|
|
856
|
-
assert
|
|
857
|
-
assert matcher_names.index("apply_patch") > dispatcher_index
|
|
858
|
-
assert enforcer_entries[0]["hooks"][0]["command"].endswith(
|
|
859
|
-
"/hooks/blocking/code_rules_enforcer.py"
|
|
860
|
-
)
|
|
873
|
+
assert standalone_apply_patch_entries == []
|
|
861
874
|
|
|
862
875
|
|
|
863
876
|
def _prepare_projection_fixture(
|
|
@@ -1008,7 +1021,18 @@ def _assert_projection_and_manifest(
|
|
|
1008
1021
|
|
|
1009
1022
|
|
|
1010
1023
|
def _assert_detached_enforcer_behavior(target: Path) -> None:
|
|
1024
|
+
"""Prove the detached, materialized enforcer allows a clean add and denies an unnamed raise.
|
|
1025
|
+
|
|
1026
|
+
Disables the ephemeral-path exemption for both runs: without it, a target
|
|
1027
|
+
rooted under the OS temp directory (where this fixture lives) would skip
|
|
1028
|
+
code_rules_enforcer's checks entirely and both assertions would pass
|
|
1029
|
+
without the enforcer ever judging the payload.
|
|
1030
|
+
"""
|
|
1011
1031
|
target_entrypoint = target / materializer.codex_enforcer_script_relative_path
|
|
1032
|
+
subprocess_environment = {
|
|
1033
|
+
**os.environ,
|
|
1034
|
+
"CLAUDE_CODE_RULES_DISABLE_EPHEMERAL_EXEMPT": "1",
|
|
1035
|
+
}
|
|
1012
1036
|
allow_payload = {
|
|
1013
1037
|
"tool_name": "apply_patch",
|
|
1014
1038
|
"cwd": str(target),
|
|
@@ -1029,8 +1053,9 @@ def _assert_detached_enforcer_behavior(target: Path) -> None:
|
|
|
1029
1053
|
capture_output=True,
|
|
1030
1054
|
text=True,
|
|
1031
1055
|
check=False,
|
|
1056
|
+
env=subprocess_environment,
|
|
1032
1057
|
)
|
|
1033
|
-
assert allow_run.returncode == 0
|
|
1058
|
+
assert allow_run.returncode == 0, allow_run.stderr
|
|
1034
1059
|
assert allow_run.stdout == ""
|
|
1035
1060
|
|
|
1036
1061
|
allow_tool_input = allow_payload["tool_input"]
|
|
@@ -1050,8 +1075,9 @@ def _assert_detached_enforcer_behavior(target: Path) -> None:
|
|
|
1050
1075
|
capture_output=True,
|
|
1051
1076
|
text=True,
|
|
1052
1077
|
check=False,
|
|
1078
|
+
env=subprocess_environment,
|
|
1053
1079
|
)
|
|
1054
|
-
assert deny_run.returncode == 0
|
|
1080
|
+
assert deny_run.returncode == 0, deny_run.stderr
|
|
1055
1081
|
assert json.loads(deny_run.stdout)["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
1056
1082
|
assert target_entrypoint.is_file()
|
|
1057
1083
|
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
"""Put the blocking and hooks directories on sys.path for the blocking tests."""
|
|
2
|
-
|
|
3
|
-
import sys
|
|
4
|
-
from pathlib import Path
|
|
5
|
-
|
|
6
|
-
_blocking_directory = str(Path(__file__).resolve().parents[1])
|
|
7
|
-
_hooks_directory = str(Path(__file__).resolve().parents[2])
|
|
8
|
-
for each_bootstrap_directory in (_blocking_directory, _hooks_directory):
|
|
9
|
-
if each_bootstrap_directory not in sys.path:
|
|
10
|
-
sys.path.insert(0, each_bootstrap_directory)
|