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.
Files changed (47) hide show
  1. package/docs/CODE_RULES.md +2 -1
  2. package/hooks/blocking/code_rules_duplicate_body.py +272 -100
  3. package/hooks/blocking/code_rules_enforcer.py +32 -14
  4. package/hooks/blocking/code_rules_shared.py +64 -14
  5. package/hooks/blocking/pr_description_writer_gate.py +18 -25
  6. package/hooks/blocking/tdd_enforcer.py +8 -4
  7. package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +12 -1
  8. package/hooks/blocking/test_code_rules_enforcer_ephemeral_repository_root.py +118 -0
  9. package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +244 -0
  10. package/hooks/blocking/test_code_rules_enforcer_split_entry_2.py +22 -0
  11. package/hooks/blocking/test_pr_description_writer_gate.py +21 -3
  12. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +9 -2
  13. package/hooks/blocking/test_state_description_blocker.py +99 -53
  14. package/hooks/blocking/test_tdd_enforcer.py +64 -0
  15. package/hooks/hooks_constants/pr_description_writer_gate_constants.py +34 -0
  16. package/hooks/hooks_constants/subprocess_budget_completeness_content.py +2 -2
  17. package/hooks/hooks_constants/test_multi_edit_reconstruction.py +1 -0
  18. package/hooks/hooks_constants/test_subprocess_budget_completeness_content.py +33 -0
  19. package/hooks/observability/pr_description_writer_spawn_tracker.py +4 -12
  20. package/hooks/observability/test_pr_description_writer_spawn_tracker.py +6 -2
  21. package/hooks/validators/abbreviation_checks.py +3 -3
  22. package/hooks/validators/code_quality_checks.py +4 -4
  23. package/hooks/validators/config/baseline_identity_constants.py +12 -0
  24. package/hooks/validators/config/directory_exemption_constants.py +44 -1
  25. package/hooks/validators/config/validator_base_constants.py +9 -0
  26. package/hooks/validators/fast_save_validators.py +23 -80
  27. package/hooks/validators/pr_reference_checks.py +2 -2
  28. package/hooks/validators/python_antipattern_checks.py +3 -3
  29. package/hooks/validators/python_style_checks.py +3 -2
  30. package/hooks/validators/react_checks.py +2 -9
  31. package/hooks/validators/run_all_validators.py +190 -145
  32. package/hooks/validators/security_checks.py +3 -3
  33. package/hooks/validators/test_directory_exemption_constants.py +30 -24
  34. package/hooks/validators/test_fast_save_validators.py +16 -16
  35. package/hooks/validators/test_hook_timing_harness.py +10 -1
  36. package/hooks/validators/test_run_all_validators_pretooluse.py +161 -0
  37. package/hooks/validators/test_run_all_validators_repository_root.py +202 -0
  38. package/hooks/validators/test_validator_base.py +51 -2
  39. package/hooks/validators/todo_checks.py +2 -2
  40. package/hooks/validators/type_safety_checks.py +3 -3
  41. package/hooks/validators/useless_test_checks.py +3 -3
  42. package/hooks/validators/validator_base.py +73 -1
  43. package/package.json +1 -1
  44. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +15 -0
  45. package/scripts/grok_headless_runner.py +15 -12
  46. package/scripts/shared_tree_paths.py +70 -0
  47. package/scripts/test_shared_tree_paths.py +143 -0
@@ -251,10 +251,50 @@ def _extract_fstring_literal_parts(
251
251
  return "".join(display_segments), "".join(shape_segments)
252
252
 
253
253
 
254
- def is_ephemeral_script_path(file_path: str) -> bool:
254
+ def _normalized_absolute_comparison_path(raw_path: str) -> str:
255
+ """Return raw_path as an absolute, lowercase, forward-slash path for root comparison.
256
+
257
+ Strips a leading Windows drive letter after normalizing, so a POSIX root
258
+ check and a Windows root check compare on the same shape.
259
+
260
+ Args:
261
+ raw_path: The path to normalize.
262
+
263
+ Returns:
264
+ The absolute, lowercase, forward-slash path with any leading drive
265
+ letter removed.
266
+ """
267
+ return LEADING_DRIVE_LETTER_PATTERN.sub("", os.path.abspath(raw_path).replace("\\", "/").lower())
268
+
269
+
270
+ def _is_at_or_under_root(normalized_candidate: str, normalized_root: str) -> bool:
271
+ """Return whether normalized_candidate sits at or under normalized_root.
272
+
273
+ Requires a path-segment boundary (an exact match or a ``/`` before the
274
+ remainder), so ``/tmp/repo-sibling`` never counts as under ``/tmp/repo``.
275
+
276
+ Args:
277
+ normalized_candidate: A path already run through
278
+ ``_normalized_absolute_comparison_path``.
279
+ normalized_root: A root path already run through
280
+ ``_normalized_absolute_comparison_path``.
281
+
282
+ Returns:
283
+ True when normalized_candidate equals normalized_root or names a
284
+ descendant of it.
285
+ """
286
+ return normalized_candidate == normalized_root or normalized_candidate.startswith(normalized_root + "/")
287
+
288
+
289
+ def is_ephemeral_script_path(file_path: str, repository_root: str = "") -> bool:
255
290
  """Return True when the path is rooted at a throwaway scratch directory.
256
291
 
257
292
  Checks these sources in order:
293
+ - ``repository_root`` — when given, a path inside it is never ephemeral,
294
+ whatever directory the repository itself is checked out under. This
295
+ keeps a repository placed under ``/tmp`` fully gated: the exemption
296
+ below is for scratch outside every repository, not for the repository's
297
+ own files.
258
298
  - ``$CLAUDE_JOB_DIR/tmp`` — only when ``CLAUDE_JOB_DIR`` is set.
259
299
  - Root-anchored ``/tmp`` and ``/temp`` (drive-letter tolerant).
260
300
 
@@ -262,20 +302,28 @@ def is_ephemeral_script_path(file_path: str) -> bool:
262
302
  its sandbox fixtures there, so matching it would exempt the suite's own
263
303
  targets. Returns False when ``CLAUDE_CODE_RULES_DISABLE_EPHEMERAL_EXEMPT``
264
304
  is truthy, when ``file_path`` is empty, and when no root matches. Path
265
- classification is string-only; the file need not exist.
305
+ classification is string-only; neither path need exist.
266
306
 
267
307
  Args:
268
308
  file_path: The candidate path to classify.
309
+ repository_root: The session's working directory — typically the
310
+ PreToolUse payload's ``cwd`` — or empty when no such directory is
311
+ known. A path outside this directory is unaffected by it.
269
312
 
270
313
  Returns:
271
- True when the path is rooted at a recognized ephemeral scratch directory.
314
+ True when the path is rooted at a recognized ephemeral scratch
315
+ directory and does not sit inside ``repository_root``.
272
316
  """
273
317
  if not file_path:
274
318
  return False
275
319
  disable_value = os.environ.get(EPHEMERAL_EXEMPT_DISABLE_ENVIRONMENT_VARIABLE_NAME, "").strip().lower()
276
320
  if disable_value in ALL_EPHEMERAL_EXEMPT_DISABLE_TRUTHY_VALUES:
277
321
  return False
278
- normalized = LEADING_DRIVE_LETTER_PATTERN.sub("", os.path.abspath(file_path).replace("\\", "/").lower())
322
+ normalized = _normalized_absolute_comparison_path(file_path)
323
+ if repository_root and _is_at_or_under_root(
324
+ normalized, _normalized_absolute_comparison_path(repository_root)
325
+ ):
326
+ return False
279
327
  all_temp_roots: list[str] = []
280
328
  job_dir = os.environ.get(CLAUDE_JOB_DIR_ENVIRONMENT_VARIABLE_NAME)
281
329
  if job_dir:
@@ -286,7 +334,7 @@ def is_ephemeral_script_path(file_path: str) -> bool:
286
334
  for each_root in ALL_ROOT_ANCHORED_EPHEMERAL_DIRECTORIES:
287
335
  all_temp_roots.append(each_root)
288
336
  for each_temp_root in all_temp_roots:
289
- if normalized == each_temp_root or normalized.startswith(each_temp_root + "/"):
337
+ if _is_at_or_under_root(normalized, each_temp_root):
290
338
  return True
291
339
  return False
292
340
 
@@ -426,23 +474,25 @@ def is_ephemeral_path(file_path: str, hook_payload: dict | None = None) -> bool:
426
474
  Combines the path families a repo gate skips: the root-anchored ephemeral
427
475
  scratch directories (``/tmp`` and ``$CLAUDE_JOB_DIR/tmp``), the harness
428
476
  session scratchpad, and a coding agent's own home-directory tooling such as
429
- ``~/.grok/runs/``. The session scratchpad match reads the session id from the
430
- payload when one is supplied, and from the harness environment variable
431
- otherwise, so a caller that holds no payload still gets the match. The
432
- run_all_validators PreToolUse gate calls this predicate to skip an exempt
433
- target before it validates. The code-rules and TDD gates call the underlying
434
- predicates ``is_ephemeral_script_path``, ``is_agent_home_tooling``, and
477
+ ``~/.grok/runs/``. A path inside the payload's own working directory
478
+ (``cwd``) is never treated as scratch, so a repository checked out under
479
+ ``/tmp`` still receives full enforcement. The scratchpad match reads the
480
+ session id from the payload when supplied, else the harness environment
481
+ variable. The code-rules and TDD gates call the underlying predicates
482
+ ``is_ephemeral_script_path``, ``is_agent_home_tooling``, and
435
483
  ``is_under_session_scratchpad`` directly.
436
484
 
437
485
  Args:
438
486
  file_path: The candidate path to classify.
439
- hook_payload: The PreToolUse payload carrying the session id, or None to
440
- read the session id from the environment alone.
487
+ hook_payload: The PreToolUse payload carrying the session id and the
488
+ working directory (``cwd``), or None to read the session id from
489
+ the environment alone.
441
490
 
442
491
  Returns:
443
492
  True when the path is scratch, agent tooling, or the session scratchpad.
444
493
  """
445
- if is_ephemeral_script_path(file_path):
494
+ repository_root = str((hook_payload or {}).get("cwd") or "")
495
+ if is_ephemeral_script_path(file_path, repository_root):
446
496
  return True
447
497
  if is_agent_home_tooling(file_path):
448
498
  return True
@@ -26,7 +26,6 @@ from __future__ import annotations
26
26
  import json
27
27
  import shlex
28
28
  import sys
29
- import tempfile
30
29
  from pathlib import Path
31
30
 
32
31
  _blocking_dir = str(Path(__file__).resolve().parent)
@@ -41,6 +40,9 @@ from hooks_constants.bash_pre_tool_use_dispatcher_constants import ( # noqa: E4
41
40
  ALL_BASH_AND_POWERSHELL_TOOL_NAMES,
42
41
  )
43
42
  from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
43
+ from hooks_constants.pre_tool_use_stdin import ( # noqa: E402
44
+ read_hook_input_dictionary_from_stdin,
45
+ )
44
46
  from hooks_constants.pr_description_writer_gate_constants import ( # noqa: E402
45
47
  ALL_GH_EXECUTABLE_BASENAMES,
46
48
  COMMAND_FIELD_NAME,
@@ -49,15 +51,14 @@ from hooks_constants.pr_description_writer_gate_constants import ( # noqa: E402
49
51
  GH_PR_CREATE_MINIMUM_TOKEN_COUNT,
50
52
  PR_SUBCOMMAND_TOKEN,
51
53
  SPAWN_BYPASS_MARKER,
52
- SPAWN_MARKER_FILE_PREFIX,
53
- SPAWN_MARKER_FILE_SUFFIX,
54
54
  TOOL_INPUT_FIELD_NAME,
55
55
  TOOL_NAME_FIELD_NAME,
56
+ spawn_marker_path,
56
57
  )
57
58
  from hooks_constants.session_edit_stage_gate_constants import ( # noqa: E402
58
59
  ALL_COMMAND_SEPARATOR_TOKENS,
59
- SESSION_ID_UNSAFE_CHARACTERS_PATTERN,
60
60
  )
61
+ from sensitive_file_protector import build_deny_response # noqa: E402
61
62
 
62
63
 
63
64
  def _all_command_tokens(shell_command: str) -> list[str] | None:
@@ -149,7 +150,9 @@ def _all_pull_request_creation_segment_indexes(all_segments: list[list[str]]) ->
149
150
 
150
151
 
151
152
  def _only_final_create_carries_bypass(
152
- all_tokens: list[str], all_creation_segment_indexes: list[int]
153
+ all_tokens: list[str],
154
+ all_segments: list[list[str]],
155
+ all_creation_segment_indexes: list[int],
153
156
  ) -> bool:
154
157
  """Report whether one final gh pr create carries the final bypass comment.
155
158
 
@@ -159,6 +162,7 @@ def _only_final_create_carries_bypass(
159
162
 
160
163
  Args:
161
164
  all_tokens: The tokens of the whole command.
165
+ all_segments: The command's token segments, already split by the caller.
162
166
  all_creation_segment_indexes: Indexes of segments that create pull requests.
163
167
 
164
168
  Returns:
@@ -166,7 +170,7 @@ def _only_final_create_carries_bypass(
166
170
  """
167
171
  if not _carries_trailing_bypass_marker(all_tokens):
168
172
  return False
169
- final_segment_index = len(_all_command_segments(all_tokens)) - 1
173
+ final_segment_index = len(all_segments) - 1
170
174
  return all_creation_segment_indexes == [final_segment_index]
171
175
 
172
176
 
@@ -182,11 +186,8 @@ def _spawn_is_recorded(session_id: str) -> bool:
182
186
  Returns:
183
187
  True when this session's marker file is present.
184
188
  """
185
- sanitized_session_id = SESSION_ID_UNSAFE_CHARACTERS_PATTERN.sub("", session_id)
186
- if not sanitized_session_id:
187
- return False
188
- file_name = f"{SPAWN_MARKER_FILE_PREFIX}{sanitized_session_id}{SPAWN_MARKER_FILE_SUFFIX}"
189
- return (Path(tempfile.gettempdir()) / file_name).exists()
189
+ marker_file = spawn_marker_path(session_id)
190
+ return marker_file is not None and marker_file.exists()
190
191
 
191
192
 
192
193
  def _shell_command_or_none(all_hook_payload: dict[str, object]) -> str | None:
@@ -211,19 +212,12 @@ def _shell_command_or_none(all_hook_payload: dict[str, object]) -> str | None:
211
212
 
212
213
  def _emit_denial() -> None:
213
214
  """Write the PreToolUse deny payload to stdout and log the block."""
214
- deny_payload = {
215
- "hookSpecificOutput": {
216
- "hookEventName": "PreToolUse",
217
- "permissionDecision": "deny",
218
- "permissionDecisionReason": CORRECTIVE_MESSAGE,
219
- }
220
- }
221
215
  log_hook_block(
222
216
  calling_hook_name="pr_description_writer_gate.py",
223
217
  hook_event="PreToolUse",
224
218
  block_reason=CORRECTIVE_MESSAGE,
225
219
  )
226
- sys.stdout.write(json.dumps(deny_payload) + "\n")
220
+ sys.stdout.write(json.dumps(build_deny_response(CORRECTIVE_MESSAGE)) + "\n")
227
221
  sys.stdout.flush()
228
222
 
229
223
 
@@ -235,11 +229,8 @@ def main() -> None:
235
229
  tokenize, a command opening no pull request, a trailing bypass marker, and
236
230
  a recorded spawn each pass through untouched.
237
231
  """
238
- try:
239
- hook_payload = json.load(sys.stdin)
240
- except json.JSONDecodeError:
241
- sys.exit(0)
242
- if not isinstance(hook_payload, dict):
232
+ hook_payload = read_hook_input_dictionary_from_stdin()
233
+ if hook_payload is None:
243
234
  sys.exit(0)
244
235
  shell_command = _shell_command_or_none(hook_payload)
245
236
  if shell_command is None:
@@ -251,7 +242,9 @@ def main() -> None:
251
242
  all_creation_segment_indexes = _all_pull_request_creation_segment_indexes(all_segments)
252
243
  if not all_creation_segment_indexes:
253
244
  sys.exit(0)
254
- if _only_final_create_carries_bypass(all_tokens, all_creation_segment_indexes):
245
+ if _only_final_create_carries_bypass(
246
+ all_tokens, all_segments, all_creation_segment_indexes
247
+ ):
255
248
  sys.exit(0)
256
249
  if _spawn_is_recorded(str(hook_payload.get("session_id") or "")):
257
250
  sys.exit(0)
@@ -80,8 +80,12 @@ def _resolve_payload(input_data: dict) -> tuple[str, dict, str]:
80
80
  return input_data.get("tool_name", ""), tool_input, tool_input.get("file_path", "")
81
81
 
82
82
 
83
- def _is_silently_skipped(file_path: str, extension: str, path: Path) -> bool:
84
- if _is_inside_dotclaude_segment(file_path) or is_ephemeral_script_path(file_path):
83
+ def _is_silently_skipped(
84
+ file_path: str, extension: str, path: Path, repository_root: str = ""
85
+ ) -> bool:
86
+ if _is_inside_dotclaude_segment(file_path) or is_ephemeral_script_path(
87
+ file_path, repository_root
88
+ ):
85
89
  return True
86
90
  if _is_agent_home_tooling_write(file_path):
87
91
  return True
@@ -135,13 +139,13 @@ def _decide_for_target(
135
139
  return None
136
140
  path = Path(file_path)
137
141
  extension = path.suffix.lower()
138
- if _is_silently_skipped(file_path, extension, path):
142
+ repository_root = str(input_data.get("cwd") or "")
143
+ if _is_silently_skipped(file_path, extension, path, repository_root):
139
144
  return None
140
145
  if _write_is_exempt(tool_name, tool_input, path, extension):
141
146
  return True, ""
142
147
  all_candidates = candidate_test_paths_for(path)
143
148
  session_id = str(input_data.get("session_id") or "")
144
- repository_root = str(input_data.get("cwd") or "")
145
149
  is_recorded_or_fresh = has_recorded_or_fresh_test(
146
150
  all_candidates, session_id, repository_root, _freshness_seconds()
147
151
  )
@@ -3,6 +3,7 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import json
6
+ import os
6
7
  import subprocess
7
8
  import sys
8
9
  import tempfile
@@ -168,8 +169,18 @@ def test_should_return_true_for_root_anchored_tmp_and_temp(
168
169
  raw_path: str,
169
170
  monkeypatch: pytest.MonkeyPatch,
170
171
  ) -> None:
171
- """B4: classifier returns True for root-anchored /tmp and /temp paths."""
172
+ """B4: classifier returns True for root-anchored /tmp and /temp paths.
173
+
174
+ A Windows drive-letter literal is absolute only on Windows. On POSIX,
175
+ os.path.abspath() joins a non-absolute path with the process cwd, so a
176
+ drive-letter case's pass or fail state depends on whatever directory the
177
+ runner happens to sit in. Skip it on a host that does not treat the
178
+ literal as absolute, so the assertion checks its own precondition rather
179
+ than the runner's ambient cwd.
180
+ """
172
181
  monkeypatch.delenv("CLAUDE_CODE_RULES_DISABLE_EPHEMERAL_EXEMPT", raising=False)
182
+ if not os.path.isabs(raw_path):
183
+ pytest.skip(f"{raw_path!r} is not absolute on this host")
173
184
  assert is_ephemeral_script_path(raw_path) is True
174
185
 
175
186
 
@@ -0,0 +1,118 @@
1
+ """Repository-root exemption tests for code_rules_enforcer and the classifier.
2
+
3
+ A path inside the session's repository root is never ephemeral, whatever
4
+ directory the repository itself is checked out under. These tests cover the
5
+ classifier directly and the enforcer's real PreToolUse Write path, plus the
6
+ guard cases a too-broad fix could break: an absent cwd, a cwd that covers no
7
+ repository, and an unrelated sibling worktree that merely shares a name
8
+ prefix under /tmp.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import json
14
+
15
+ from blocking import _path_setup # noqa: F401 (pins this checkout ahead of sibling worktrees)
16
+ from blocking.code_rules_enforcer import main as enforcer_main
17
+ from blocking.code_rules_shared import is_ephemeral_script_path
18
+ from code_rules_enforcer_test_support import run_serialized_payload_entrypoint
19
+
20
+ _VIOLATING_PRODUCTION_SOURCE = "def process_data(payload: str) -> None:\n print(payload)\n"
21
+
22
+
23
+ def _build_write_payload_with_cwd(file_path: str, content: str, cwd: str) -> str:
24
+ """Build the JSON payload for a Write tool call that also carries a cwd."""
25
+ return json.dumps(
26
+ {
27
+ "tool_name": "Write",
28
+ "tool_input": {"file_path": file_path, "content": content},
29
+ "cwd": cwd,
30
+ }
31
+ )
32
+
33
+
34
+ def _run_main_with_write_payload_and_cwd(
35
+ file_path: str,
36
+ content: str,
37
+ cwd: str,
38
+ ) -> tuple[str, int]:
39
+ """Drive enforcer_main through its stdin entry point for a Write payload carrying cwd."""
40
+ captured_stdout, exit_code = run_serialized_payload_entrypoint(
41
+ enforcer_main, _build_write_payload_with_cwd(file_path, content, cwd)
42
+ )
43
+ return captured_stdout, int(exit_code or 0)
44
+
45
+
46
+ def test_should_return_false_for_file_inside_repository_root_under_tmp() -> None:
47
+ """R1: a path inside repository_root is never ephemeral, even under /tmp."""
48
+ repository_root = "/tmp/pr-alpha"
49
+ file_path = "/tmp/pr-alpha/hooks/blocking/orders.py"
50
+ assert is_ephemeral_script_path(file_path, repository_root) is False
51
+
52
+
53
+ def test_should_return_true_for_sibling_directory_outside_repository_root() -> None:
54
+ """R2: a /tmp sibling that merely shares repository_root's name prefix stays ephemeral."""
55
+ repository_root = "/tmp/pr-alpha"
56
+ sibling_file_path = "/tmp/pr-alpha-sibling/hooks/orders.py"
57
+ assert is_ephemeral_script_path(sibling_file_path, repository_root) is True
58
+
59
+
60
+ def test_should_return_true_when_repository_root_does_not_cover_target() -> None:
61
+ """R3: an unrelated cwd must not blanket-exempt a /tmp target outside it."""
62
+ unrelated_repository_root = "/tmp/scratch-workspace"
63
+ scratch_target = "/tmp/scratch-elsewhere/notes.py"
64
+ assert is_ephemeral_script_path(scratch_target, unrelated_repository_root) is True
65
+
66
+
67
+ def test_should_deny_pretooluse_target_inside_repository_root_under_tmp() -> None:
68
+ """B22: a production file inside a repository checked out under /tmp is gated for real."""
69
+ repository_root = "/tmp/pr-alpha"
70
+ file_path = f"{repository_root}/hooks/blocking/orders.py"
71
+ captured_stdout, exit_code = _run_main_with_write_payload_and_cwd(
72
+ file_path,
73
+ _VIOLATING_PRODUCTION_SOURCE,
74
+ repository_root,
75
+ )
76
+ assert "deny" in captured_stdout.lower(), (
77
+ f"enforcer must gate a repository file under /tmp, got exit_code={exit_code}, "
78
+ f"stdout={captured_stdout!r}"
79
+ )
80
+
81
+
82
+ def test_should_stay_exempt_for_scratch_outside_repository_root() -> None:
83
+ """B23: a genuine scratch file outside the repository stays exempt."""
84
+ repository_root = "/tmp/pr-alpha"
85
+ scratch_target = "/tmp/scratch/one_off.py"
86
+ captured_stdout, exit_code = _run_main_with_write_payload_and_cwd(
87
+ scratch_target,
88
+ _VIOLATING_PRODUCTION_SOURCE,
89
+ repository_root,
90
+ )
91
+ assert exit_code == 0
92
+ assert "deny" not in captured_stdout.lower()
93
+
94
+
95
+ def test_should_stay_exempt_when_working_directory_covers_no_repository() -> None:
96
+ """B24: a cwd that covers no repository must not exempt an unrelated /tmp target."""
97
+ unrelated_working_directory = "/tmp/scratch-workspace"
98
+ scratch_target = "/tmp/scratch-elsewhere/notes.py"
99
+ captured_stdout, exit_code = _run_main_with_write_payload_and_cwd(
100
+ scratch_target,
101
+ _VIOLATING_PRODUCTION_SOURCE,
102
+ unrelated_working_directory,
103
+ )
104
+ assert exit_code == 0
105
+ assert "deny" not in captured_stdout.lower()
106
+
107
+
108
+ def test_should_stay_exempt_for_unrelated_worktree_sibling_under_tmp() -> None:
109
+ """B25: a sibling worktree sharing repository_root's name prefix stays exempt."""
110
+ repository_root = "/tmp/pr-alpha"
111
+ unrelated_sibling_target = "/tmp/pr-alpha-sibling/hooks/orders.py"
112
+ captured_stdout, exit_code = _run_main_with_write_payload_and_cwd(
113
+ unrelated_sibling_target,
114
+ _VIOLATING_PRODUCTION_SOURCE,
115
+ repository_root,
116
+ )
117
+ assert exit_code == 0
118
+ assert "deny" not in captured_stdout.lower()