claude-dev-env 2.2.1 → 2.4.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 (170) hide show
  1. package/CLAUDE.md +3 -2
  2. package/_shared/advisor/advisor-protocol.md +2 -2
  3. package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
  4. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -284
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
  6. package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +3 -3
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -0
  8. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +5 -0
  9. package/_shared/pr-loop/scripts/stale_worktree_rule_sweep.py +107 -0
  10. package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
  11. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +55 -73
  12. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +9 -15
  13. package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +91 -1
  14. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +88 -1
  15. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -0
  16. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -0
  17. package/_shared/pr-loop/worker-spawn.md +1 -1
  18. package/agents/CLAUDE.md +1 -0
  19. package/agents/code-verifier.md +4 -4
  20. package/agents/skill-writer-agent.md +84 -0
  21. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +141 -41
  22. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
  23. package/docs/CLAUDE.md +1 -0
  24. package/docs/references/CLAUDE.md +1 -0
  25. package/docs/references/code-review-enforcement.md +97 -0
  26. package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
  27. package/hooks/blocking/CLAUDE.md +8 -1
  28. package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
  29. package/hooks/blocking/code_review_gate_deny.py +74 -0
  30. package/hooks/blocking/code_review_pr_create_gate.py +194 -0
  31. package/hooks/blocking/code_review_push_gate.py +140 -0
  32. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +340 -0
  33. package/hooks/blocking/code_review_stamp_store.py +233 -0
  34. package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
  35. package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
  36. package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
  37. package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
  38. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
  39. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
  40. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
  41. package/hooks/blocking/config/code_review_enforcement_constants.py +110 -0
  42. package/hooks/blocking/config/test_code_review_enforcement_constants.py +108 -0
  43. package/hooks/blocking/config/verified_commit_constants.py +24 -9
  44. package/hooks/blocking/conftest.py +2 -0
  45. package/hooks/blocking/convergence_gate_blocker.py +112 -23
  46. package/hooks/blocking/destructive_command_blocker.py +19 -6
  47. package/hooks/blocking/pr_description_proof_of_work.py +52 -34
  48. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
  49. package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
  50. package/hooks/blocking/test_code_review_gate_deny.py +54 -0
  51. package/hooks/blocking/test_code_review_pr_create_gate.py +185 -0
  52. package/hooks/blocking/test_code_review_push_gate.py +189 -0
  53. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +180 -0
  54. package/hooks/blocking/test_code_review_stamp_store.py +205 -0
  55. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
  56. package/hooks/blocking/test_code_verifier_tools_contract.py +28 -0
  57. package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
  58. package/hooks/blocking/test_destructive_command_blocker.py +1 -1
  59. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
  60. package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
  61. package/hooks/blocking/test_pre_tool_use_dispatcher.py +8 -8
  62. package/hooks/blocking/test_verification_verdict_store.py +920 -810
  63. package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
  64. package/hooks/blocking/verification_verdict_store.py +118 -5
  65. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
  66. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
  67. package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
  68. package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
  69. package/hooks/git-hooks/pre_push.py +89 -2
  70. package/hooks/git-hooks/test_pre_push.py +103 -0
  71. package/hooks/hooks.json +16 -1
  72. package/hooks/hooks_constants/CLAUDE.md +1 -0
  73. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
  74. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  75. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
  76. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
  77. package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
  78. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
  79. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  80. package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
  81. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
  82. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
  83. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
  84. package/hooks/validators/CLAUDE.md +1 -0
  85. package/hooks/validators/mypy_integration.py +63 -50
  86. package/hooks/validators/pyproject_config_discovery.py +101 -0
  87. package/hooks/validators/ruff_integration.py +211 -23
  88. package/hooks/validators/run_all_validators.py +21 -14
  89. package/hooks/validators/test_mypy_integration.py +32 -0
  90. package/hooks/validators/test_pyproject_config_discovery.py +94 -0
  91. package/hooks/validators/test_ruff_integration.py +68 -1
  92. package/hooks/validators/test_run_all_validators.py +25 -0
  93. package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
  94. package/package.json +1 -1
  95. package/rules/docstring-prose-matches-implementation.md +20 -43
  96. package/rules/durable-post-artifacts.md +7 -0
  97. package/scripts/CLAUDE.md +2 -1
  98. package/scripts/claude-chain.example.json +15 -3
  99. package/scripts/claude_chain_runner.py +130 -27
  100. package/scripts/claude_chain_usage.py +346 -0
  101. package/scripts/codec_forwarding_test_support.py +83 -0
  102. package/scripts/conftest.py +8 -0
  103. package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -3
  104. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +57 -2
  105. package/scripts/dev_env_scripts_constants/claude_chain_usage_constants.py +58 -0
  106. package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
  107. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
  108. package/scripts/dev_env_scripts_constants/timing.py +1 -1
  109. package/scripts/invoke_code_review.py +550 -38
  110. package/scripts/resolve_worker_spawn.py +8 -1
  111. package/scripts/test_claude_chain_runner.py +412 -6
  112. package/scripts/test_claude_chain_usage.py +534 -0
  113. package/scripts/test_invoke_code_review.py +298 -4
  114. package/scripts/test_invoke_code_review_codec.py +77 -0
  115. package/scripts/test_resolve_worker_spawn_codec.py +101 -0
  116. package/skills/autoconverge/SKILL.md +9 -3
  117. package/skills/autoconverge/reference/convergence.md +2 -1
  118. package/skills/autoconverge/reference/multi-pr.md +6 -1
  119. package/skills/autoconverge/reference/stop-conditions.md +16 -10
  120. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +1 -1
  121. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
  122. package/skills/autoconverge/workflow/converge.contract.test.mjs +19 -0
  123. package/skills/autoconverge/workflow/converge.mjs +24 -8
  124. package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
  125. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
  126. package/skills/fresh-branch/CLAUDE.md +2 -0
  127. package/skills/fresh-branch/SKILL.md +2 -0
  128. package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
  129. package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
  130. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
  131. package/skills/fresh-branch/scripts/pytest.ini +4 -0
  132. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
  133. package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
  134. package/skills/orchestrator/SKILL.md +1 -20
  135. package/skills/orchestrator-refresh/SKILL.md +1 -1
  136. package/skills/pr-converge/SKILL.md +3 -3
  137. package/skills/pr-converge/reference/examples.md +3 -3
  138. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  139. package/skills/pr-converge/reference/ground-rules.md +3 -3
  140. package/skills/pr-converge/reference/per-tick.md +5 -5
  141. package/skills/pr-converge/reference/progress-checklist.md +1 -1
  142. package/skills/pr-converge/test_step5_host_branch.py +1 -1
  143. package/skills/prototype/SKILL.md +86 -0
  144. package/skills/prototype/reference/honest-limitations.md +23 -0
  145. package/skills/prototype/reference/promotion-tasks.md +23 -0
  146. package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
  147. package/skills/prototype/scripts/conftest.py +15 -0
  148. package/skills/prototype/scripts/launch_sandbox.py +205 -0
  149. package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
  150. package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
  151. package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
  152. package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
  153. package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
  154. package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
  155. package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
  156. package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
  157. package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
  158. package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
  159. package/skills/prototype/workflows/promotion.md +27 -0
  160. package/skills/prototype/workflows/sandbox.md +35 -0
  161. package/skills/skill-builder/CLAUDE.md +3 -3
  162. package/skills/skill-builder/SKILL.md +5 -5
  163. package/skills/skill-builder/references/CLAUDE.md +1 -1
  164. package/skills/skill-builder/references/delegation-map.md +3 -3
  165. package/skills/skill-builder/references/description-field.md +1 -1
  166. package/skills/skill-builder/references/skill-modularity.md +2 -3
  167. package/skills/skill-builder/workflows/CLAUDE.md +1 -1
  168. package/skills/skill-builder/workflows/improve-skill.md +1 -1
  169. package/skills/skill-builder/workflows/new-skill.md +2 -2
  170. package/skills/team-advisor/SKILL.md +5 -4
@@ -0,0 +1,189 @@
1
+ """Behavioral tests for the push code-review gate.
2
+
3
+ Each test drives the gate against a real git work tree with a change surface
4
+ and a real stamp store under an isolated home, so the deny/allow decision runs
5
+ the same hash-and-coverage path the hook runs in production.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import importlib.util
11
+ import io
12
+ import json
13
+ import subprocess
14
+ import sys
15
+ from pathlib import Path
16
+ from types import ModuleType
17
+
18
+ import pytest
19
+
20
+ _HOOK_DIR = Path(__file__).resolve().parent
21
+ if str(_HOOK_DIR) not in sys.path:
22
+ sys.path.insert(0, str(_HOOK_DIR))
23
+
24
+ GIT_TIMEOUT_SECONDS = 30
25
+ LOW_EFFORT = "low"
26
+ XHIGH_EFFORT = "xhigh"
27
+ CODE_SOURCE = "def add(a: int, b: int) -> int:\n return a + b\n"
28
+ CODE_CHANGE = "def add(a: int, b: int) -> int:\n return a - b\n"
29
+ DOCS_SOURCE = "# Notes\n\nfirst line\n"
30
+ DOCS_CHANGE = "# Notes\n\nsecond line\n"
31
+ BYPASS_MARKER = "# code-review-skip"
32
+
33
+
34
+ def _load_module(module_name: str) -> ModuleType:
35
+ module_spec = importlib.util.spec_from_file_location(
36
+ module_name, _HOOK_DIR / f"{module_name}.py"
37
+ )
38
+ assert module_spec is not None
39
+ assert module_spec.loader is not None
40
+ loaded_module = importlib.util.module_from_spec(module_spec)
41
+ module_spec.loader.exec_module(loaded_module)
42
+ return loaded_module
43
+
44
+
45
+ gate_module = _load_module("code_review_push_gate")
46
+ store_module = _load_module("code_review_stamp_store")
47
+
48
+
49
+ def _run_git(repository_directory: Path, *git_arguments: str) -> None:
50
+ subprocess.run(
51
+ ["git", "-C", str(repository_directory), *git_arguments],
52
+ check=True,
53
+ capture_output=True,
54
+ text=True,
55
+ timeout=GIT_TIMEOUT_SECONDS,
56
+ )
57
+
58
+
59
+ def _init_pushed_repo(tmp_path: Path, tracked_name: str, base_text: str) -> Path:
60
+ origin_directory = tmp_path / "origin.git"
61
+ work_directory = tmp_path / "work"
62
+ work_directory.mkdir()
63
+ subprocess.run(
64
+ ["git", "init", "--bare", "--initial-branch=main", str(origin_directory)],
65
+ check=True,
66
+ capture_output=True,
67
+ text=True,
68
+ timeout=GIT_TIMEOUT_SECONDS,
69
+ )
70
+ _run_git(work_directory, "init", "--initial-branch=main")
71
+ _run_git(work_directory, "config", "user.email", "tests@example.com")
72
+ _run_git(work_directory, "config", "user.name", "Reviewer")
73
+ (work_directory / tracked_name).write_text(base_text, encoding="utf-8")
74
+ _run_git(work_directory, "add", "-A")
75
+ _run_git(work_directory, "commit", "-m", "base")
76
+ _run_git(work_directory, "remote", "add", "origin", str(origin_directory))
77
+ _run_git(work_directory, "push", "-u", "origin", "main")
78
+ return work_directory
79
+
80
+
81
+ def _make_code_surface_repo(tmp_path: Path) -> Path:
82
+ work_directory = _init_pushed_repo(tmp_path, "app.py", CODE_SOURCE)
83
+ (work_directory / "app.py").write_text(CODE_CHANGE, encoding="utf-8")
84
+ return work_directory
85
+
86
+
87
+ def _make_docs_surface_repo(tmp_path: Path) -> Path:
88
+ work_directory = _init_pushed_repo(tmp_path, "notes.md", DOCS_SOURCE)
89
+ (work_directory / "notes.md").write_text(DOCS_CHANGE, encoding="utf-8")
90
+ return work_directory
91
+
92
+
93
+ def _isolate_home(monkeypatch: pytest.MonkeyPatch, fake_home: Path) -> None:
94
+ home_text = str(fake_home)
95
+ monkeypatch.setenv("HOME", home_text)
96
+ monkeypatch.setenv("USERPROFILE", home_text)
97
+ monkeypatch.delenv("HOMEDRIVE", raising=False)
98
+ monkeypatch.delenv("HOMEPATH", raising=False)
99
+
100
+
101
+ def _prepared_code_repo(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> Path:
102
+ fake_home = tmp_path / "home"
103
+ fake_home.mkdir()
104
+ _isolate_home(monkeypatch, fake_home)
105
+ return _make_code_surface_repo(tmp_path)
106
+
107
+
108
+ def _record_stamp(work_directory: Path, effort: str) -> None:
109
+ surface_hash = store_module.live_surface_hash(str(work_directory))
110
+ assert surface_hash is not None
111
+ store_module.record_clean_stamp(str(work_directory), surface_hash, effort)
112
+
113
+
114
+ def _run_main(monkeypatch: pytest.MonkeyPatch, command_text: str, work_directory: Path) -> str:
115
+ payload_text = json.dumps(
116
+ {
117
+ "tool_name": "Bash",
118
+ "tool_input": {"command": command_text},
119
+ "cwd": str(work_directory),
120
+ }
121
+ )
122
+ monkeypatch.setattr(sys, "stdin", io.StringIO(payload_text))
123
+ captured_stdout = io.StringIO()
124
+ monkeypatch.setattr(sys, "stdout", captured_stdout)
125
+ gate_module.main()
126
+ return captured_stdout.getvalue()
127
+
128
+
129
+ def test_push_without_low_stamp_denies(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
130
+ work_directory = _prepared_code_repo(monkeypatch, tmp_path)
131
+ deny_reason = gate_module.deny_reason_for_directory(str(work_directory))
132
+ assert deny_reason is not None
133
+ assert "PUSH_GATE" in deny_reason
134
+
135
+
136
+ def test_covering_low_stamp_allows_push(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
137
+ work_directory = _prepared_code_repo(monkeypatch, tmp_path)
138
+ _record_stamp(work_directory, LOW_EFFORT)
139
+ assert gate_module.deny_reason_for_directory(str(work_directory)) is None
140
+
141
+
142
+ def test_covering_xhigh_stamp_allows_push(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
143
+ work_directory = _prepared_code_repo(monkeypatch, tmp_path)
144
+ _record_stamp(work_directory, XHIGH_EFFORT)
145
+ assert gate_module.deny_reason_for_directory(str(work_directory)) is None
146
+
147
+
148
+ def test_docs_only_surface_is_exempt(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
149
+ fake_home = tmp_path / "home"
150
+ fake_home.mkdir()
151
+ _isolate_home(monkeypatch, fake_home)
152
+ work_directory = _make_docs_surface_repo(tmp_path)
153
+ assert gate_module.deny_reason_for_directory(str(work_directory)) is None
154
+
155
+
156
+ def test_git_push_payload_denies(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
157
+ work_directory = _prepared_code_repo(monkeypatch, tmp_path)
158
+ emitted = _run_main(monkeypatch, "git push origin main", work_directory)
159
+ assert "PUSH_GATE" in emitted
160
+
161
+
162
+ def test_git_push_with_leading_flags_denies(
163
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
164
+ ) -> None:
165
+ work_directory = _prepared_code_repo(monkeypatch, tmp_path)
166
+ emitted = _run_main(monkeypatch, "git push --force-with-lease origin main", work_directory)
167
+ assert "PUSH_GATE" in emitted
168
+
169
+
170
+ def test_git_commit_is_not_gated_by_push_hook(
171
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
172
+ ) -> None:
173
+ work_directory = _prepared_code_repo(monkeypatch, tmp_path)
174
+ emitted = _run_main(monkeypatch, "git commit -m change", work_directory)
175
+ assert emitted == ""
176
+
177
+
178
+ def test_git_stash_push_is_not_gated(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
179
+ work_directory = _prepared_code_repo(monkeypatch, tmp_path)
180
+ emitted = _run_main(monkeypatch, "git stash push", work_directory)
181
+ assert emitted == ""
182
+
183
+
184
+ def test_code_review_skip_marker_allows_push(
185
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
186
+ ) -> None:
187
+ work_directory = _prepared_code_repo(monkeypatch, tmp_path)
188
+ emitted = _run_main(monkeypatch, f"git push origin main {BYPASS_MARKER}", work_directory)
189
+ assert emitted == ""
@@ -0,0 +1,180 @@
1
+ """Behavioral tests for the code-review stamp-directory write blocker.
2
+
3
+ The blocker denies shell and file-tool access to
4
+ ``~/.claude/code-review-stamps/`` and shell references to the stamp store
5
+ module or its mint call, while the sanctioned invoker command and unrelated
6
+ paths pass.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import importlib.util
12
+ import io
13
+ import json
14
+ import sys
15
+ from pathlib import Path
16
+ from types import ModuleType
17
+
18
+ import pytest
19
+
20
+ _HOOK_DIR = Path(__file__).resolve().parent
21
+ if str(_HOOK_DIR) not in sys.path:
22
+ sys.path.insert(0, str(_HOOK_DIR))
23
+
24
+ STAMP_STORE_IMPORT_FORGE = (
25
+ 'python -c "from code_review_stamp_store import record_clean_stamp; '
26
+ "record_clean_stamp(root, live_hash, 'low')\""
27
+ )
28
+ MINT_CALL_FORGE = "python -c \"record_clean_stamp(root, live_hash, 'xhigh')\""
29
+ PYTEST_RUN = "python -m pytest test_code_review_stamp_store.py"
30
+ SANCTIONED_INVOKER = "python invoke_code_review.py --record-stamp --cwd . --session-model opus low"
31
+ ABSOLUTE_STAMP_ACCESS = "cat ~/.claude/code-review-stamps/abc1234567890abc.json"
32
+ DECOY_ECHO_THEN_FORGE = (
33
+ 'echo "invoke_code_review.py --record-stamp" >/dev/null; '
34
+ "echo x > ~/.claude/code-review-stamps/abc1234567890123.json"
35
+ )
36
+ DECOY_INVOKER_THEN_FORGE = (
37
+ "python invoke_code_review.py --record-stamp --cwd . low ; "
38
+ "echo pwned > ~/.claude/code-review-stamps/evil.json"
39
+ )
40
+
41
+
42
+ def _load_module(module_name: str) -> ModuleType:
43
+ module_spec = importlib.util.spec_from_file_location(
44
+ module_name, _HOOK_DIR / f"{module_name}.py"
45
+ )
46
+ assert module_spec is not None
47
+ assert module_spec.loader is not None
48
+ loaded_module = importlib.util.module_from_spec(module_spec)
49
+ module_spec.loader.exec_module(loaded_module)
50
+ return loaded_module
51
+
52
+
53
+ blocker_module = _load_module("code_review_stamp_directory_write_blocker")
54
+
55
+
56
+ def _isolate_home(monkeypatch: pytest.MonkeyPatch, fake_home: Path) -> None:
57
+ home_text = str(fake_home)
58
+ monkeypatch.setenv("HOME", home_text)
59
+ monkeypatch.setenv("USERPROFILE", home_text)
60
+ monkeypatch.delenv("HOMEDRIVE", raising=False)
61
+ monkeypatch.delenv("HOMEPATH", raising=False)
62
+
63
+
64
+ def test_absolute_stamp_path_is_blocked() -> None:
65
+ assert blocker_module.references_stamp_directory(ABSOLUTE_STAMP_ACCESS)
66
+
67
+
68
+ def test_store_module_import_forge_is_blocked() -> None:
69
+ assert blocker_module.references_stamp_directory(STAMP_STORE_IMPORT_FORGE)
70
+
71
+
72
+ def test_mint_call_forge_is_blocked() -> None:
73
+ assert blocker_module.references_stamp_directory(MINT_CALL_FORGE)
74
+
75
+
76
+ def test_pytest_run_naming_store_test_is_not_blocked() -> None:
77
+ assert not blocker_module.references_stamp_directory(PYTEST_RUN)
78
+
79
+
80
+ def test_sanctioned_invoker_command_is_not_blocked() -> None:
81
+ assert not blocker_module.references_stamp_directory(SANCTIONED_INVOKER)
82
+
83
+
84
+ def test_decoy_echo_of_invoker_then_stamp_forge_is_blocked() -> None:
85
+ assert blocker_module.references_stamp_directory(DECOY_ECHO_THEN_FORGE)
86
+
87
+
88
+ def test_decoy_invoker_run_then_stamp_forge_is_blocked() -> None:
89
+ assert blocker_module.references_stamp_directory(DECOY_INVOKER_THEN_FORGE)
90
+
91
+
92
+ def test_write_tool_to_stamp_directory_is_blocked(
93
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
94
+ ) -> None:
95
+ fake_home = tmp_path / "home"
96
+ fake_home.mkdir()
97
+ _isolate_home(monkeypatch, fake_home)
98
+ stamp_path = fake_home / ".claude" / "code-review-stamps" / "abc.json"
99
+ assert blocker_module.path_targets_stamp_directory(str(stamp_path))
100
+
101
+
102
+ def test_write_tool_elsewhere_is_allowed(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
103
+ fake_home = tmp_path / "home"
104
+ fake_home.mkdir()
105
+ _isolate_home(monkeypatch, fake_home)
106
+ other_path = tmp_path / "src" / "module.py"
107
+ assert not blocker_module.path_targets_stamp_directory(str(other_path))
108
+
109
+
110
+ def test_write_payload_to_stamp_directory_denies(
111
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
112
+ ) -> None:
113
+ fake_home = tmp_path / "home"
114
+ fake_home.mkdir()
115
+ _isolate_home(monkeypatch, fake_home)
116
+ stamp_path = fake_home / ".claude" / "code-review-stamps" / "abc.json"
117
+ payload = {
118
+ "tool_name": "Write",
119
+ "tool_input": {"file_path": str(stamp_path), "content": "{}"},
120
+ }
121
+ deny_decision = blocker_module.decision_for_payload(payload)
122
+ assert deny_decision is not None
123
+ assert deny_decision["hookSpecificOutput"]["permissionDecision"] == "deny"
124
+
125
+
126
+ def test_bash_forge_payload_denies() -> None:
127
+ payload = {
128
+ "tool_name": "Bash",
129
+ "tool_input": {"command": STAMP_STORE_IMPORT_FORGE},
130
+ }
131
+ deny_decision = blocker_module.decision_for_payload(payload)
132
+ assert deny_decision is not None
133
+ assert deny_decision["hookSpecificOutput"]["permissionDecision"] == "deny"
134
+
135
+
136
+ def test_sanctioned_invoker_payload_is_not_denied() -> None:
137
+ payload = {
138
+ "tool_name": "Bash",
139
+ "tool_input": {"command": SANCTIONED_INVOKER},
140
+ }
141
+ assert blocker_module.decision_for_payload(payload) is None
142
+
143
+
144
+ def test_main_emits_deny_for_bash_forge(monkeypatch: pytest.MonkeyPatch) -> None:
145
+ payload_text = json.dumps({"tool_name": "Bash", "tool_input": {"command": MINT_CALL_FORGE}})
146
+ monkeypatch.setattr(sys, "stdin", io.StringIO(payload_text))
147
+ captured_stdout = io.StringIO()
148
+ monkeypatch.setattr(sys, "stdout", captured_stdout)
149
+ blocker_module.main()
150
+ assert "STAMP_DIRECTORY_GUARD" in captured_stdout.getvalue()
151
+
152
+
153
+ STAMP_OBFUSCATION_PATH = "/.claude/code-review-stamps/a.json"
154
+ BENIGN_OBFUSCATION_PATH = "/tmp/x"
155
+ SPLIT_CD_FORGE = "cd ~/.claude && cd code-review-stamps && echo x > f.json"
156
+
157
+
158
+ def _hex_forge(path_text: str) -> str:
159
+ return f"python -c \"open(bytes.fromhex('{path_text.encode().hex()}'),'w').write('x')\""
160
+
161
+
162
+ def _chr_chain_forge(path_text: str) -> str:
163
+ chr_chain = "+".join(f"chr({ord(each_character)})" for each_character in path_text)
164
+ return f"python -c \"open({chr_chain},'w').write('x')\""
165
+
166
+
167
+ def test_split_directory_change_into_stamp_is_blocked() -> None:
168
+ assert blocker_module.references_stamp_directory(SPLIT_CD_FORGE)
169
+
170
+
171
+ def test_hex_obfuscated_stamp_path_is_blocked() -> None:
172
+ assert blocker_module.references_stamp_directory(_hex_forge(STAMP_OBFUSCATION_PATH))
173
+
174
+
175
+ def test_chr_chain_obfuscated_stamp_path_is_blocked() -> None:
176
+ assert blocker_module.references_stamp_directory(_chr_chain_forge(STAMP_OBFUSCATION_PATH))
177
+
178
+
179
+ def test_benign_decode_to_other_path_is_not_blocked() -> None:
180
+ assert not blocker_module.references_stamp_directory(_hex_forge(BENIGN_OBFUSCATION_PATH))
@@ -0,0 +1,205 @@
1
+ """Behavioral tests for the per-work-tree code-review stamp store.
2
+
3
+ Each test builds a real git repository with a real origin remote and records or
4
+ reads real stamp files under an isolated home, so it exercises the same store
5
+ surface the push and PR-create gates call: record a clean stamp, then ask
6
+ whether it covers the live branch surface at a given effort.
7
+ """
8
+
9
+ import importlib.util
10
+ import pathlib
11
+ import subprocess
12
+ import sys
13
+
14
+ import pytest
15
+
16
+ _HOOK_DIR = pathlib.Path(__file__).parent
17
+ if str(_HOOK_DIR) not in sys.path:
18
+ sys.path.insert(0, str(_HOOK_DIR))
19
+
20
+ _store_spec = importlib.util.spec_from_file_location(
21
+ "code_review_stamp_store",
22
+ _HOOK_DIR / "code_review_stamp_store.py",
23
+ )
24
+ assert _store_spec is not None
25
+ assert _store_spec.loader is not None
26
+ _store_module = importlib.util.module_from_spec(_store_spec)
27
+ _store_spec.loader.exec_module(_store_module)
28
+
29
+ record_clean_stamp = _store_module.record_clean_stamp
30
+ stamp_covers_surface = _store_module.stamp_covers_surface
31
+ live_surface_hash = _store_module.live_surface_hash
32
+ stamp_path_for_repo = _store_module.stamp_path_for_repo
33
+
34
+ _verdict_store_module = sys.modules["verification_verdict_store"]
35
+ root_key_for_repo = _verdict_store_module.root_key_for_repo
36
+
37
+ _constants_spec = importlib.util.spec_from_file_location(
38
+ "code_review_enforcement_constants",
39
+ _HOOK_DIR / "config" / "code_review_enforcement_constants.py",
40
+ )
41
+ assert _constants_spec is not None
42
+ assert _constants_spec.loader is not None
43
+ _constants_module = importlib.util.module_from_spec(_constants_spec)
44
+ _constants_spec.loader.exec_module(_constants_module)
45
+ PUSH_REQUIRED_EFFORT = _constants_module.PUSH_REQUIRED_EFFORT
46
+ PR_CREATE_REQUIRED_EFFORT = _constants_module.PR_CREATE_REQUIRED_EFFORT
47
+ ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER = _constants_module.ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER
48
+
49
+ PRODUCTION_SOURCE = "def add(left: int, right: int) -> int:\n return left + right\n"
50
+ CHANGED_SOURCE = "def add(left: int, right: int) -> int:\n return left - right\n"
51
+ FURTHER_CHANGED_SOURCE = "def add(left: int, right: int) -> int:\n return left * right\n"
52
+
53
+
54
+ def _run_git(repo_dir: pathlib.Path, *git_arguments: str) -> None:
55
+ subprocess.run(
56
+ ["git", "-C", str(repo_dir), *git_arguments],
57
+ check=True,
58
+ capture_output=True,
59
+ text=True,
60
+ )
61
+
62
+
63
+ def _make_repo_with_origin(tmp_path: pathlib.Path) -> pathlib.Path:
64
+ origin_dir = tmp_path / "origin.git"
65
+ work_dir = tmp_path / "work"
66
+ work_dir.mkdir()
67
+ subprocess.run(
68
+ ["git", "init", "--bare", "--initial-branch=main", str(origin_dir)],
69
+ check=True,
70
+ capture_output=True,
71
+ text=True,
72
+ )
73
+ empty_hooks_dir = tmp_path / "nohooks"
74
+ empty_hooks_dir.mkdir()
75
+ _run_git(work_dir, "init", "--initial-branch=main")
76
+ _run_git(work_dir, "config", "user.email", "tests@example.com")
77
+ _run_git(work_dir, "config", "user.name", "Stamp Store Tests")
78
+ _run_git(work_dir, "config", "core.hooksPath", str(empty_hooks_dir))
79
+ (work_dir / "src").mkdir()
80
+ (work_dir / "src" / "app.py").write_text(PRODUCTION_SOURCE, encoding="utf-8")
81
+ _run_git(work_dir, "add", "-A")
82
+ _run_git(work_dir, "commit", "-m", "base")
83
+ _run_git(work_dir, "remote", "add", "origin", str(origin_dir))
84
+ _run_git(work_dir, "push", "-u", "origin", "main")
85
+ return work_dir
86
+
87
+
88
+ def _isolate_home(monkeypatch: pytest.MonkeyPatch, fake_home: pathlib.Path) -> None:
89
+ home_text = str(fake_home)
90
+ monkeypatch.setenv("HOME", home_text)
91
+ monkeypatch.setenv("USERPROFILE", home_text)
92
+ monkeypatch.delenv("HOMEDRIVE", raising=False)
93
+ monkeypatch.delenv("HOMEPATH", raising=False)
94
+
95
+
96
+ def _repo_with_change(tmp_path: pathlib.Path, changed_source: str) -> pathlib.Path:
97
+ work_dir = _make_repo_with_origin(tmp_path)
98
+ (work_dir / "src" / "app.py").write_text(changed_source, encoding="utf-8")
99
+ return work_dir
100
+
101
+
102
+ def test_recorded_low_stamp_covers_the_push_surface(
103
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
104
+ ) -> None:
105
+ _isolate_home(monkeypatch, tmp_path / "home")
106
+ work_dir = _repo_with_change(tmp_path, CHANGED_SOURCE)
107
+ surface_hash = live_surface_hash(str(work_dir))
108
+ assert surface_hash is not None
109
+ record_clean_stamp(str(work_dir), surface_hash, PUSH_REQUIRED_EFFORT)
110
+ assert stamp_covers_surface(str(work_dir), surface_hash, PUSH_REQUIRED_EFFORT) is True
111
+
112
+
113
+ def test_low_stamp_does_not_cover_the_pr_create_surface(
114
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
115
+ ) -> None:
116
+ _isolate_home(monkeypatch, tmp_path / "home")
117
+ work_dir = _repo_with_change(tmp_path, CHANGED_SOURCE)
118
+ surface_hash = live_surface_hash(str(work_dir))
119
+ assert surface_hash is not None
120
+ record_clean_stamp(str(work_dir), surface_hash, PUSH_REQUIRED_EFFORT)
121
+ assert stamp_covers_surface(str(work_dir), surface_hash, PR_CREATE_REQUIRED_EFFORT) is False
122
+
123
+
124
+ def test_xhigh_stamp_covers_both_push_and_pr_create(
125
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
126
+ ) -> None:
127
+ _isolate_home(monkeypatch, tmp_path / "home")
128
+ work_dir = _repo_with_change(tmp_path, CHANGED_SOURCE)
129
+ surface_hash = live_surface_hash(str(work_dir))
130
+ assert surface_hash is not None
131
+ record_clean_stamp(str(work_dir), surface_hash, PR_CREATE_REQUIRED_EFFORT)
132
+ assert stamp_covers_surface(str(work_dir), surface_hash, PUSH_REQUIRED_EFFORT) is True
133
+ assert stamp_covers_surface(str(work_dir), surface_hash, PR_CREATE_REQUIRED_EFFORT) is True
134
+
135
+
136
+ def test_stamp_bound_to_one_hash_does_not_cover_a_different_hash(
137
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
138
+ ) -> None:
139
+ _isolate_home(monkeypatch, tmp_path / "home")
140
+ work_dir = _repo_with_change(tmp_path, CHANGED_SOURCE)
141
+ surface_hash = live_surface_hash(str(work_dir))
142
+ assert surface_hash is not None
143
+ record_clean_stamp(str(work_dir), surface_hash, PUSH_REQUIRED_EFFORT)
144
+ other_hash = "f" * len(surface_hash)
145
+ assert stamp_covers_surface(str(work_dir), other_hash, PUSH_REQUIRED_EFFORT) is False
146
+
147
+
148
+ def test_editing_a_file_after_minting_drops_coverage(
149
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
150
+ ) -> None:
151
+ _isolate_home(monkeypatch, tmp_path / "home")
152
+ work_dir = _repo_with_change(tmp_path, CHANGED_SOURCE)
153
+ minted_hash = live_surface_hash(str(work_dir))
154
+ assert minted_hash is not None
155
+ record_clean_stamp(str(work_dir), minted_hash, PUSH_REQUIRED_EFFORT)
156
+ (work_dir / "src" / "app.py").write_text(FURTHER_CHANGED_SOURCE, encoding="utf-8")
157
+ live_hash_after_edit = live_surface_hash(str(work_dir))
158
+ assert live_hash_after_edit is not None
159
+ assert live_hash_after_edit != minted_hash
160
+ assert stamp_covers_surface(str(work_dir), live_hash_after_edit, PUSH_REQUIRED_EFFORT) is False
161
+
162
+
163
+ def test_no_stamp_file_reads_as_not_covered(
164
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
165
+ ) -> None:
166
+ _isolate_home(monkeypatch, tmp_path / "home")
167
+ work_dir = _repo_with_change(tmp_path, CHANGED_SOURCE)
168
+ surface_hash = live_surface_hash(str(work_dir))
169
+ assert surface_hash is not None
170
+ assert not stamp_path_for_repo(str(work_dir)).exists()
171
+ assert stamp_covers_surface(str(work_dir), surface_hash, PUSH_REQUIRED_EFFORT) is False
172
+
173
+
174
+ def test_recording_a_second_effort_keeps_the_first(
175
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
176
+ ) -> None:
177
+ _isolate_home(monkeypatch, tmp_path / "home")
178
+ work_dir = _repo_with_change(tmp_path, CHANGED_SOURCE)
179
+ surface_hash = live_surface_hash(str(work_dir))
180
+ assert surface_hash is not None
181
+ record_clean_stamp(str(work_dir), surface_hash, PUSH_REQUIRED_EFFORT)
182
+ record_clean_stamp(str(work_dir), surface_hash, PR_CREATE_REQUIRED_EFFORT)
183
+ assert stamp_covers_surface(str(work_dir), surface_hash, PUSH_REQUIRED_EFFORT) is True
184
+ assert stamp_covers_surface(str(work_dir), surface_hash, PR_CREATE_REQUIRED_EFFORT) is True
185
+
186
+
187
+ def test_live_surface_hash_is_none_when_nothing_changed(
188
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
189
+ ) -> None:
190
+ _isolate_home(monkeypatch, tmp_path / "home")
191
+ work_dir = _make_repo_with_origin(tmp_path)
192
+ assert live_surface_hash(str(work_dir)) is None
193
+
194
+
195
+ def test_push_required_effort_is_the_lowest_known_token() -> None:
196
+ assert PUSH_REQUIRED_EFFORT == ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER[0]
197
+
198
+
199
+ def test_stamp_path_for_repo_stem_matches_the_shared_root_key(
200
+ monkeypatch: pytest.MonkeyPatch, tmp_path: pathlib.Path
201
+ ) -> None:
202
+ _isolate_home(monkeypatch, tmp_path / "home")
203
+ work_dir = tmp_path / "work-tree"
204
+ work_dir.mkdir()
205
+ assert stamp_path_for_repo(str(work_dir)).stem == root_key_for_repo(str(work_dir))