claude-dev-env 2.31.1 → 2.33.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 (139) hide show
  1. package/.agents/agents/clean-coder.md +8 -7
  2. package/.agents/agents/code-quality-agent.md +4 -2
  3. package/.agents/agents/pr-description-writer.md +8 -3
  4. package/.agents/agents/reference/pr-description-illustrative-voice.md +6 -1
  5. package/.agents/agents/test_agent_frontmatter.py +0 -9
  6. package/.agents/agents/test_pr_description_writer_style.py +24 -1
  7. package/.agents/skills/e-simplify/SKILL.md +1 -15
  8. package/.agents/skills/grok-spawn/reference/worker-briefs.md +4 -0
  9. package/.agents/skills/pr-refinement/SKILL.md +2 -2
  10. package/.agents/skills/pr-shared-extraction/SKILL.md +2 -2
  11. package/.agents/skills/pr-title-description/SKILL.md +6 -2
  12. package/.agents/skills/review-tier/SKILL.md +1 -1
  13. package/.agents/skills/review-tier/reference/tier-policy.json +2 -2
  14. package/.agents/skills/review-tier/scripts/review_tier.py +31 -4
  15. package/.agents/skills/review-tier/scripts/test_generation.py +7 -19
  16. package/.agents/skills/review-tier/scripts/test_review_tier.py +11 -11
  17. package/AGENTS.md +83 -0
  18. package/_shared/pr-loop/code-rules-gate.md +5 -2
  19. package/_shared/pr-loop/scripts/code_rules_gate.py +7 -3
  20. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +13 -1
  21. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +7 -0
  22. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +47 -1
  23. package/audit-rubrics/audit-categories.json +2 -2
  24. package/audit-rubrics/category_rubrics/category-e-dead-code.md +1 -1
  25. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +5 -3
  26. package/audit-rubrics/category_rubrics/category-l-behavior-equivalence.md +6 -3
  27. package/audit-rubrics/prompts/category-e-dead-code.md +6 -6
  28. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +25 -19
  29. package/audit-rubrics/prompts/category-l-behavior-equivalence.md +28 -25
  30. package/bin/install.test.mjs +11 -3
  31. package/docs/CODE_RULES.md +8 -9
  32. package/hooks/blocking/bash_post_call_dispatcher.py +70 -0
  33. package/hooks/blocking/bash_pre_tool_use_dispatcher.py +8 -21
  34. package/hooks/blocking/code_rules_comments.py +118 -59
  35. package/hooks/blocking/code_rules_duplicate_body.py +13 -7
  36. package/hooks/blocking/code_rules_enforcer.py +19 -7
  37. package/hooks/blocking/code_rules_enforcer_test_support.py +39 -0
  38. package/hooks/blocking/code_rules_type_escape.py +16 -23
  39. package/hooks/blocking/javascript_comment_scanner.py +99 -0
  40. package/hooks/blocking/shell_substitution_blocker.py +175 -8
  41. package/hooks/blocking/stop_dispatcher.py +6 -9
  42. package/hooks/blocking/tdd_enforcer.py +3 -1
  43. package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +7 -0
  44. package/hooks/blocking/tdd_enforcer_parts/content_hash_store.py +141 -23
  45. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_hash_store.py +114 -1
  46. package/hooks/blocking/test_bash_post_call_dispatcher.py +112 -0
  47. package/hooks/blocking/test_code_rules_enforcer_any_type_ignore.py +24 -4
  48. package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +0 -1
  49. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +190 -3
  50. package/hooks/blocking/test_code_rules_enforcer_javascript_comments.py +44 -0
  51. package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +0 -1
  52. package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +8 -1
  53. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +7 -2
  54. package/hooks/blocking/test_shell_substitution_blocker.py +105 -0
  55. package/hooks/blocking/test_state_description_blocker.py +56 -71
  56. package/hooks/blocking/test_tdd_enforcer.py +67 -1
  57. package/hooks/git-hooks/pull_request_handoff.py +1 -1
  58. package/hooks/git-hooks/test_post_commit.py +8 -0
  59. package/hooks/hooks.json +2 -2
  60. package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +22 -0
  61. package/hooks/hooks_constants/hosted_hook_runner.py +53 -0
  62. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +9 -2
  63. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +10 -0
  64. package/hooks/hooks_constants/stale_comment_reference_blocker_constants.py +3 -4
  65. package/hooks/hooks_constants/state_file_lock.py +76 -0
  66. package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +27 -0
  67. package/hooks/hooks_constants/test_failure_recorder_constants.py +35 -0
  68. package/hooks/hooks_constants/test_hosted_hook_runner.py +55 -1
  69. package/hooks/hooks_constants/test_unused_module_import_constants.py +9 -1
  70. package/hooks/hooks_constants/unused_module_import_constants.py +1 -1
  71. package/hooks/observability/session_file_edit_tracker.py +2 -55
  72. package/hooks/observability/test_failure_recorder.py +219 -0
  73. package/hooks/observability/test_test_failure_recorder.py +173 -0
  74. package/hooks/validators/comment_checks.py +2 -2
  75. package/hooks/validators/config/directory_exemption_constants.py +0 -9
  76. package/hooks/validators/config/system_temporary_roots_constants.py +15 -0
  77. package/hooks/validators/fast_save_validators.py +0 -9
  78. package/hooks/validators/git_checks.py +0 -84
  79. package/hooks/validators/mypy_integration.py +10 -36
  80. package/hooks/validators/project_roots.py +58 -0
  81. package/hooks/validators/run_all_validators.py +89 -42
  82. package/hooks/validators/system_temporary_roots.py +1 -1
  83. package/hooks/validators/test_fast_save_validators.py +2 -8
  84. package/hooks/validators/test_git_checks.py +0 -249
  85. package/hooks/validators/test_run_all_validators.py +22 -0
  86. package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +249 -0
  87. package/hooks/validators/test_run_all_validators_reporting.py +66 -0
  88. package/hooks/validators/test_todo_checks.py +1 -0
  89. package/hooks/validators/todo_checks.py +1 -1
  90. package/package.json +1 -1
  91. package/rules/git-workflow.md +0 -8
  92. package/rules/shell-invocation.md +1 -1
  93. package/rules/testing.md +4 -0
  94. package/scripts/check.ps1 +35 -1
  95. package/scripts/codex_compat_materializer.py +1 -0
  96. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +9 -5
  97. package/scripts/dev_env_scripts_constants/shared_tree_constants.py +17 -0
  98. package/scripts/shared_tree_paths.py +59 -25
  99. package/scripts/test_shared_tree_paths.py +30 -0
  100. package/scripts/tests/test_rules.py +111 -0
  101. package/system-prompts/software-engineer.xml +6 -19
  102. package/.agents/skills-archived/split-pr/SKILL.md +0 -85
  103. package/.agents/skills-archived/split-pr/reference/path-layers.md +0 -16
  104. package/.agents/skills-archived/split-pr/reference/proposal-format.md +0 -15
  105. package/.agents/skills-archived/split-pr/reference/split-further-loop.md +0 -10
  106. package/.agents/skills-archived/split-pr/reference/splitting-principles.md +0 -26
  107. package/.agents/skills-archived/split-pr/scripts/analyze_pr.py +0 -279
  108. package/.agents/skills-archived/split-pr/scripts/categorize_files.py +0 -106
  109. package/.agents/skills-archived/split-pr/scripts/config/__init__.py +0 -1
  110. package/.agents/skills-archived/split-pr/scripts/config/dependency_constants.py +0 -14
  111. package/.agents/skills-archived/split-pr/scripts/config/git_operations_constants.py +0 -36
  112. package/.agents/skills-archived/split-pr/scripts/config/packing_constants.py +0 -61
  113. package/.agents/skills-archived/split-pr/scripts/config/plan_constants.py +0 -49
  114. package/.agents/skills-archived/split-pr/scripts/config/split_pr_constants.py +0 -110
  115. package/.agents/skills-archived/split-pr/scripts/execute_split_slices.py +0 -82
  116. package/.agents/skills-archived/split-pr/scripts/pack_files_into_slices.py +0 -212
  117. package/.agents/skills-archived/split-pr/scripts/split_pr_dependency_graph.py +0 -70
  118. package/.agents/skills-archived/split-pr/scripts/split_pr_git_operations.py +0 -184
  119. package/.agents/skills-archived/split-pr/scripts/split_pr_layer_order.py +0 -58
  120. package/.agents/skills-archived/split-pr/scripts/split_pr_paginate.py +0 -119
  121. package/.agents/skills-archived/split-pr/scripts/split_pr_process_runner.py +0 -52
  122. package/.agents/skills-archived/split-pr/scripts/split_pr_script_types.py +0 -126
  123. package/.agents/skills-archived/split-pr/scripts/split_pr_title.py +0 -41
  124. package/.agents/skills-archived/split-pr/scripts/test_analyze_pr.py +0 -228
  125. package/.agents/skills-archived/split-pr/scripts/test_categorize_files.py +0 -55
  126. package/.agents/skills-archived/split-pr/scripts/test_categorize_files_packing.py +0 -59
  127. package/.agents/skills-archived/split-pr/scripts/test_execute_split_slices.py +0 -99
  128. package/.agents/skills-archived/split-pr/scripts/test_split_pr_dependency_graph.py +0 -47
  129. package/.agents/skills-archived/split-pr/scripts/test_split_pr_git_operations.py +0 -125
  130. package/.agents/skills-archived/split-pr/scripts/test_split_pr_layer_order.py +0 -36
  131. package/.agents/skills-archived/split-pr/scripts/test_split_pr_paginate.py +0 -65
  132. package/.agents/skills-archived/split-pr/scripts/test_split_pr_script_types.py +0 -73
  133. package/.agents/skills-archived/split-pr/scripts/test_split_pr_title.py +0 -28
  134. package/.agents/skills-archived/split-pr/scripts/test_verify_dependency_graph.py +0 -46
  135. package/.agents/skills-archived/split-pr/scripts/test_verify_plan.py +0 -56
  136. package/.agents/skills-archived/split-pr/scripts/test_verify_plan_contract.py +0 -50
  137. package/.agents/skills-archived/split-pr/scripts/test_verify_plan_path_normalization.py +0 -45
  138. package/.agents/skills-archived/split-pr/scripts/verify_dependency_graph.py +0 -111
  139. package/.agents/skills-archived/split-pr/scripts/verify_plan.py +0 -139
@@ -0,0 +1,58 @@
1
+ """Resolve the project root that contains a path, if any.
2
+
3
+ A project root is the nearest ancestor holding a ``.git`` entry or a
4
+ ``pyproject.toml``. Callers need that anchor for unrelated reasons: binding a
5
+ first-party import, deciding which directory segments a project owns. One
6
+ module answers it rather than each re-coding the walk.
7
+
8
+ ::
9
+
10
+ target_repo/.git + target_repo/tools/x.py -> target_repo
11
+ /tmp/detached/x.py (home .git above temp) -> None
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import sys
17
+ from pathlib import Path
18
+
19
+ _validators_directory = str(Path(__file__).resolve().parent)
20
+ _hooks_directory = str(Path(__file__).resolve().parent.parent)
21
+ for each_directory in (_validators_directory, _hooks_directory):
22
+ if each_directory not in sys.path:
23
+ sys.path.insert(0, each_directory)
24
+
25
+ from pyproject_config_discovery import ancestor_directories # noqa: E402
26
+ from system_temporary_roots import ( # noqa: E402
27
+ enclosing_system_temporary_root,
28
+ )
29
+
30
+ from hooks_constants.mypy_integration_constants import ( # noqa: E402
31
+ GIT_DIRECTORY_NAME,
32
+ PYPROJECT_FILENAME,
33
+ )
34
+
35
+
36
+ def enclosing_project_root(starting_file: Path) -> Path | None:
37
+ """Return the nearest ancestor directory that roots a project, else None.
38
+
39
+ The walk stops at the system temp root. A staging copy lives under a temp
40
+ directory, and a ``.git`` in the user home above that root roots no
41
+ project of the staged file's.
42
+
43
+ Args:
44
+ starting_file: The file (or directory) the walk begins from.
45
+
46
+ Returns:
47
+ The nearest ancestor holding ``.git`` or ``pyproject.toml``, or None
48
+ when the walk reaches the temp root or runs out of ancestors.
49
+ """
50
+ enclosing_temporary_root = enclosing_system_temporary_root(starting_file)
51
+ for each_candidate_directory in ancestor_directories(starting_file):
52
+ has_git_entry = (each_candidate_directory / GIT_DIRECTORY_NAME).exists()
53
+ has_pyproject = (each_candidate_directory / PYPROJECT_FILENAME).is_file()
54
+ if has_git_entry or has_pyproject:
55
+ return each_candidate_directory
56
+ if each_candidate_directory == enclosing_temporary_root:
57
+ return None
58
+ return None
@@ -16,6 +16,7 @@ import tempfile
16
16
  import time
17
17
  from collections import Counter
18
18
  from dataclasses import dataclass
19
+ from functools import lru_cache
19
20
  from datetime import datetime
20
21
  from pathlib import Path
21
22
  from typing import Callable, Dict, List, Optional, Tuple
@@ -35,16 +36,24 @@ from .config.directory_exemption_constants import (
35
36
  )
36
37
  from .fast_save_validators import run_fast_save_validators
37
38
  from .health_check import get_system_health, get_validator_version, print_health_report
38
- from .mypy_integration import (
39
- check_mypy_available,
40
- find_module_resolution_root,
41
- run_mypy_check,
42
- )
39
+ from .mypy_integration import check_mypy_available, run_mypy_check
43
40
  from .output_formatter import OutputFormatter, OutputMode, ValidatorResultDict
41
+ from .project_roots import enclosing_project_root
44
42
  from .python_style_checks import fix_file
45
43
  from .ruff_integration import run_ruff_check
44
+ from .config.validator_base_constants import SOURCE_CACHE_ENTRIES
46
45
  from .validator_base import ValidatorResult
47
46
  from blocking.code_rules_shared import is_ephemeral_path
47
+ from blocking.codex_apply_patch import (
48
+ CODEX_DELETE_OPERATION,
49
+ CodexPatchError,
50
+ CodexPatchFile,
51
+ parse_codex_apply_patch,
52
+ payload_patch_command,
53
+ )
54
+ from hooks_constants.pre_tool_use_dispatcher_constants import (
55
+ APPLY_PATCH_TOOL_NAME,
56
+ )
48
57
  from hooks_constants.hook_block_logger import log_hook_block
49
58
  from hooks_constants.multi_edit_reconstruction import (
50
59
  apply_edits,
@@ -401,18 +410,12 @@ def run_git_checks() -> ValidatorResult:
401
410
 
402
411
 
403
412
  def run_comment_checks(files: List[Path]) -> ValidatorResult:
404
- """Comment preservation is enforced by code_rules_enforcer hook.
405
-
406
- The hook compares old vs new content to block NEW comments and
407
- print a stderr advisory when an existing comment is removed. This
408
- standalone validator is disabled because it flags ALL comments in
409
- existing files, which forces agents to remove them to pass validation.
410
- """
413
+ """Report that comment policy runs through the explicit diff-aware linter."""
411
414
  return ValidatorResult(
412
415
  name="No Comments",
413
416
  checks="26",
414
417
  passed=True,
415
- output="Handled by code_rules_enforcer hook (old vs new comparison)",
418
+ output="Run code_rules_gate.py with --comment-policy",
416
419
  )
417
420
 
418
421
 
@@ -793,8 +796,8 @@ def _path_parts_the_project_owns(destination_path: Path) -> tuple[str, ...]:
793
796
 
794
797
  ::
795
798
 
796
- ok: "/home/tests/proj/src/worker.py" (proj is the root) -> ("src", "worker.py")
797
- ok: "/home/dana/.claude/hooks/gate.py" (no root) -> (".claude", "hooks", "gate.py")
799
+ ok: "/srv/tests/proj/src/worker.py" (proj is the root) -> ("src", "worker.py")
800
+ ok: "/srv/anna/.claude/hooks/gate.py" (no root) -> (".claude", "hooks", "gate.py")
798
801
  ok: "config/x.py" (relative) -> ("config", "x.py")
799
802
 
800
803
  Directory names above the project root were chosen by whoever laid out the
@@ -819,13 +822,13 @@ def _path_parts_the_project_owns(destination_path: Path) -> tuple[str, ...]:
819
822
  if not destination_path.anchor:
820
823
  return all_path_parts
821
824
  all_path_parts = all_path_parts[1:]
822
- project_root = find_module_resolution_root(destination_path)
825
+ project_root = enclosing_project_root(destination_path)
823
826
  if project_root is None:
824
827
  return all_path_parts
825
- try:
826
- return destination_path.resolve().relative_to(project_root).parts
827
- except ValueError:
828
+ resolved_destination = destination_path.resolve()
829
+ if not resolved_destination.is_relative_to(project_root):
828
830
  return all_path_parts
831
+ return resolved_destination.relative_to(project_root).parts
829
832
 
830
833
 
831
834
  def _temporary_path_preserving_directory_signal(
@@ -1158,6 +1161,7 @@ def _line_identity(
1158
1161
  )
1159
1162
 
1160
1163
 
1164
+ @lru_cache(maxsize=SOURCE_CACHE_ENTRIES)
1161
1165
  def _scope_agnostic_message(identity_scope: str, violation_message: str) -> str:
1162
1166
  """Return the message with its own function's name and shape metrics masked.
1163
1167
 
@@ -1199,22 +1203,6 @@ def _scope_agnostic_message(identity_scope: str, violation_message: str) -> str:
1199
1203
  )
1200
1204
 
1201
1205
 
1202
- def _shape_key(violation_identity: ViolationIdentity) -> tuple[str, str]:
1203
- """Return an identity's ``(validator, scope-agnostic message)`` shape key.
1204
-
1205
- Args:
1206
- violation_identity: One violation's full identity key.
1207
-
1208
- Returns:
1209
- The key a violation keeps when only its enclosing function's name
1210
- changes, so a rename, a split, or a move still finds its baseline.
1211
- """
1212
- return (
1213
- violation_identity[0],
1214
- _scope_agnostic_message(violation_identity[1], violation_identity[2]),
1215
- )
1216
-
1217
-
1218
1206
  def _violation_identities(
1219
1207
  failed_results: List[ValidatorResult], content: str
1220
1208
  ) -> Counter[ViolationIdentity]:
@@ -1283,9 +1271,12 @@ def _compatible_baseline_identity(
1283
1271
  for each_identity in all_available_identities:
1284
1272
  if each_identity[1] == line_identity[1]:
1285
1273
  return each_identity
1286
- line_shape_key = _shape_key(line_identity)
1274
+ line_shape_message = _scope_agnostic_message(line_identity[1], line_identity[2])
1287
1275
  for each_identity in all_available_identities:
1288
- if _shape_key(each_identity) == line_shape_key:
1276
+ if (
1277
+ _scope_agnostic_message(each_identity[1], each_identity[2])
1278
+ == line_shape_message
1279
+ ):
1289
1280
  return each_identity
1290
1281
  return None
1291
1282
 
@@ -1448,26 +1439,80 @@ def _emit_pre_existing_warning(all_preexisting_results: List[ValidatorResult]) -
1448
1439
  sys.stderr.flush()
1449
1440
 
1450
1441
 
1451
- def _decide_pre_tool_use(file_path: str, proposed_content: str) -> None:
1442
+ def _decide_pre_tool_use(file_path: str, proposed_content: str) -> bool:
1452
1443
  """Deny only violations absent from the baseline; warn on the ones that persist.
1453
1444
 
1454
1445
  Args:
1455
1446
  file_path: The write's target path.
1456
1447
  proposed_content: The reconstructed post-edit content of that file.
1448
+
1449
+ Returns:
1450
+ True when this target denies, so a caller holding several targets stops
1451
+ at the first one and emits a single decision.
1457
1452
  """
1458
1453
  all_proposed_failed = _failed_results(
1459
1454
  validate_proposed_file(file_path, proposed_content)
1460
1455
  )
1461
1456
  if not all_proposed_failed:
1462
- return
1457
+ return False
1463
1458
  baseline_identities = _baseline_violation_identities(file_path)
1464
1459
  all_new_results, all_preexisting_results = _scope_new_and_preexisting(
1465
1460
  all_proposed_failed, proposed_content, baseline_identities
1466
1461
  )
1467
1462
  if all_preexisting_results:
1468
1463
  _emit_pre_existing_warning(all_preexisting_results)
1469
- if all_new_results:
1470
- _emit_pre_tool_use_deny(_proposed_content_deny_reason(all_new_results))
1464
+ if not all_new_results:
1465
+ return False
1466
+ _emit_pre_tool_use_deny(_proposed_content_deny_reason(all_new_results))
1467
+ return True
1468
+
1469
+
1470
+ def _decide_codex_patch_file(
1471
+ each_patch_file: CodexPatchFile, pre_tool_use_payload: dict
1472
+ ) -> bool:
1473
+ """Grade one path a Codex patch writes, skipping a deletion and an exemption.
1474
+
1475
+ Args:
1476
+ each_patch_file: One parsed path with its projected post-edit content.
1477
+ pre_tool_use_payload: The whole payload, read for its working directory.
1478
+
1479
+ Returns:
1480
+ True when this path denies the patch.
1481
+ """
1482
+ if each_patch_file.operation == CODEX_DELETE_OPERATION:
1483
+ return False
1484
+ if is_ephemeral_path(each_patch_file.file_path, pre_tool_use_payload):
1485
+ return False
1486
+ return _decide_pre_tool_use(each_patch_file.file_path, each_patch_file.post_content)
1487
+
1488
+
1489
+ def _evaluate_codex_apply_patch(pre_tool_use_payload: dict, tool_input: dict) -> None:
1490
+ """Grade every path a Codex apply_patch writes, stopping at the first denial.
1491
+
1492
+ ::
1493
+
1494
+ one patch adding two files, the second violating -> deny, names file two
1495
+ one patch deleting a file -> allow, nothing to grade
1496
+
1497
+ A patch carries several paths in one call, so each is graded on its own
1498
+ projected post-edit content and the first denial is the decision.
1499
+
1500
+ Args:
1501
+ pre_tool_use_payload: The whole PreToolUse payload.
1502
+ tool_input: The payload's tool input mapping.
1503
+ """
1504
+ patch_command, working_directory = payload_patch_command(
1505
+ pre_tool_use_payload, tool_input
1506
+ )
1507
+ if not patch_command:
1508
+ return
1509
+ try:
1510
+ all_patch_files = parse_codex_apply_patch(patch_command, working_directory)
1511
+ except CodexPatchError:
1512
+ return
1513
+ for each_patch_file in all_patch_files:
1514
+ if _decide_codex_patch_file(each_patch_file, pre_tool_use_payload):
1515
+ return
1471
1516
 
1472
1517
 
1473
1518
  def _evaluate_pre_tool_use_payload() -> None:
@@ -1491,6 +1536,9 @@ def _evaluate_pre_tool_use_payload() -> None:
1491
1536
  tool_input = pre_tool_use_payload.get("tool_input", {})
1492
1537
  if not isinstance(tool_name, str) or not isinstance(tool_input, dict):
1493
1538
  return
1539
+ if tool_name == APPLY_PATCH_TOOL_NAME:
1540
+ _evaluate_codex_apply_patch(pre_tool_use_payload, tool_input)
1541
+ return
1494
1542
  file_path = tool_input.get("file_path", "")
1495
1543
  if not isinstance(file_path, str) or not file_path:
1496
1544
  return
@@ -1621,7 +1669,6 @@ def main() -> int:
1621
1669
  ("Security", lambda: run_security_checks(files)),
1622
1670
  ("Code Quality", lambda: run_code_quality_checks(files)),
1623
1671
  ("Python Anti-patterns", lambda: run_python_antipattern_checks(files)),
1624
- ("TODO Tracking", lambda: run_todo_checks(files)),
1625
1672
  ("Type Safety", lambda: run_type_safety_checks(files)),
1626
1673
  ]
1627
1674
 
@@ -22,7 +22,7 @@ _hooks_directory = str(Path(__file__).resolve().parent.parent)
22
22
  if _hooks_directory not in sys.path:
23
23
  sys.path.insert(0, _hooks_directory)
24
24
 
25
- from validators.config.directory_exemption_constants import ( # noqa: E402
25
+ from validators.config.system_temporary_roots_constants import ( # noqa: E402
26
26
  ALL_SYSTEM_TEMPORARY_ROOT_ENVIRONMENT_VARIABLE_NAMES,
27
27
  )
28
28
 
@@ -9,7 +9,6 @@ from .fast_save_validators import run_fast_save_validators
9
9
  from .magic_value_checks import validate_file as read_magic_literal_violations
10
10
  from .react_checks import check_no_class_components
11
11
  from .test_safety_checks import check_no_skip_decorators
12
- from .todo_checks import validate_file as read_todo_violations
13
12
 
14
13
  MAGIC_LITERAL_SOURCE = "def compute_total(unit_price: int) -> int:\n return unit_price * 199\n"
15
14
  _UNTRACKED_TODO_MARKER = "#" + " TODO"
@@ -24,7 +23,6 @@ _ALL_EXPECTED_FAST_SAVE_VALIDATOR_NAMES = frozenset(
24
23
  "Python Style",
25
24
  "Abbreviations",
26
25
  "Magic Values",
27
- "TODO Tracking",
28
26
  "Security",
29
27
  "Code Quality",
30
28
  "Python Anti-patterns",
@@ -103,16 +101,12 @@ def test_magic_values_outcome_matches_direct_validate_file_call(
103
101
  assert magic_values_outcome.output == "\n".join(expected_lines)
104
102
 
105
103
 
106
- def test_todo_outcome_matches_direct_validate_file_call(tmp_path: Path) -> None:
104
+ def test_todo_comments_do_not_run_on_save_path(tmp_path: Path) -> None:
107
105
  target_file = tmp_path / "legacy_module.py"
108
106
  target_file.write_text(TODO_SOURCE, encoding="utf-8")
109
- expected_lines = [str(each) for each in read_todo_violations(target_file)]
110
- assert expected_lines != []
111
-
112
107
  all_outcomes = run_fast_save_validators([target_file])
113
108
 
114
- todo_outcome = next(each for each in all_outcomes if each.name == "TODO Tracking")
115
- assert todo_outcome.output == "\n".join(expected_lines)
109
+ assert all(each.name != "TODO Tracking" for each in all_outcomes)
116
110
 
117
111
 
118
112
  def test_abbreviations_outcome_reports_clean_for_a_clean_file(tmp_path: Path) -> None:
@@ -1,215 +1,17 @@
1
1
  """Tests for git and GitHub validation checks."""
2
2
 
3
- import json
4
3
  import subprocess
5
- from pathlib import Path
6
- from typing import List
7
4
  from unittest.mock import MagicMock, patch
8
5
 
9
6
  import pytest
10
7
 
11
8
  from .git_checks import (
12
9
  Violation,
13
- check_single_commit_when_pr_exists,
14
10
  check_draft_pr_state,
15
11
  main,
16
12
  )
17
13
 
18
14
 
19
- class TestSingleCommitWhenPrExists:
20
- """Test that PR branches have exactly 1 commit ahead of base."""
21
-
22
- @patch("validators.git_checks.subprocess.run")
23
- def test_no_pr_returns_empty(self, mock_run: MagicMock) -> None:
24
- """When no PR exists, check should return empty list."""
25
- mock_run.side_effect = [
26
- MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
27
- MagicMock(returncode=0, stdout="[]", stderr=""),
28
- ]
29
-
30
- violations = check_single_commit_when_pr_exists()
31
-
32
- assert violations == []
33
-
34
- @patch("validators.git_checks.subprocess.run")
35
- def test_single_commit_ahead_passes(self, mock_run: MagicMock) -> None:
36
- """Exactly 1 commit ahead should pass."""
37
- mock_run.side_effect = [
38
- MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
39
- MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
40
- MagicMock(returncode=0, stdout="1", stderr=""),
41
- ]
42
-
43
- violations = check_single_commit_when_pr_exists()
44
-
45
- assert violations == []
46
-
47
- @patch("validators.git_checks.subprocess.run")
48
- def test_zero_commits_ahead_fails(self, mock_run: MagicMock) -> None:
49
- """Zero commits ahead should fail."""
50
- mock_run.side_effect = [
51
- MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
52
- MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
53
- MagicMock(returncode=0, stdout="0", stderr=""),
54
- ]
55
-
56
- violations = check_single_commit_when_pr_exists()
57
-
58
- assert len(violations) == 1
59
- assert violations[0].file == ""
60
- assert violations[0].line == 0
61
- assert "exactly 1 commit" in violations[0].message
62
- assert "0 commits" in violations[0].message
63
-
64
- @patch("validators.git_checks.subprocess.run")
65
- def test_multiple_commits_ahead_fails(self, mock_run: MagicMock) -> None:
66
- """More than 1 commit ahead should fail."""
67
- mock_run.side_effect = [
68
- MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
69
- MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
70
- MagicMock(returncode=0, stdout="3", stderr=""),
71
- ]
72
-
73
- violations = check_single_commit_when_pr_exists()
74
-
75
- assert len(violations) == 1
76
- assert "exactly 1 commit" in violations[0].message
77
- assert "3 commits" in violations[0].message
78
-
79
- @patch("validators.git_checks.subprocess.run")
80
- def test_gh_cli_not_available_returns_empty(self, mock_run: MagicMock) -> None:
81
- """When gh CLI not available, should return empty (warning, not failure)."""
82
- mock_run.side_effect = [
83
- MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
84
- FileNotFoundError("gh not found"),
85
- ]
86
-
87
- violations = check_single_commit_when_pr_exists()
88
-
89
- assert violations == []
90
-
91
- @patch("validators.git_checks.subprocess.run")
92
- def test_git_not_available_returns_empty(self, mock_run: MagicMock) -> None:
93
- """When git not available, should return empty."""
94
- mock_run.side_effect = [
95
- MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
96
- MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
97
- FileNotFoundError("git not found"),
98
- ]
99
-
100
- violations = check_single_commit_when_pr_exists()
101
-
102
- assert violations == []
103
-
104
- @patch("validators.git_checks.subprocess.run")
105
- def test_extracts_base_branch_from_pr_info(self, mock_run: MagicMock) -> None:
106
- """Should extract base branch name from gh pr list JSON output, falling back to main when absent."""
107
- mock_run.side_effect = [
108
- MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
109
- MagicMock(returncode=0, stdout='[{"baseRefName": "develop", "number": 123}]', stderr=""),
110
- MagicMock(returncode=0, stdout="2", stderr=""),
111
- ]
112
-
113
- violations = check_single_commit_when_pr_exists()
114
-
115
- assert len(violations) == 1
116
- mock_run.assert_any_call(
117
- ["git", "rev-list", "--count", "develop..HEAD"],
118
- capture_output=True,
119
- text=True,
120
- check=True,
121
- timeout=30,
122
- )
123
-
124
- mock_run.reset_mock()
125
- mock_run.side_effect = [
126
- MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
127
- MagicMock(returncode=0, stdout='[{"number": 123}]', stderr=""),
128
- MagicMock(returncode=0, stdout="2", stderr=""),
129
- ]
130
-
131
- fallback_violations = check_single_commit_when_pr_exists()
132
-
133
- assert len(fallback_violations) == 1
134
- assert "main" in fallback_violations[0].message
135
- mock_run.assert_any_call(
136
- ["git", "rev-list", "--count", "main..HEAD"],
137
- capture_output=True,
138
- text=True,
139
- check=True,
140
- timeout=30,
141
- )
142
-
143
- @patch("validators.git_checks.subprocess.run")
144
- def test_non_numeric_commit_count_returns_empty(self, mock_run: MagicMock) -> None:
145
- """When git rev-list returns non-numeric output, should return empty."""
146
- mock_run.side_effect = [
147
- MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
148
- MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
149
- MagicMock(returncode=0, stdout="not a number\n", stderr=""),
150
- ]
151
-
152
- violations = check_single_commit_when_pr_exists()
153
-
154
- assert violations == []
155
-
156
- @patch("validators.git_checks.subprocess.run")
157
- def test_gh_timeout_returns_empty(self, mock_run: MagicMock) -> None:
158
- """When gh CLI times out, should return empty (warning, not failure)."""
159
- mock_run.side_effect = [
160
- MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
161
- subprocess.TimeoutExpired(cmd=["gh", "pr", "list"], timeout=30),
162
- ]
163
-
164
- violations = check_single_commit_when_pr_exists()
165
-
166
- assert violations == []
167
-
168
- @patch("validators.git_checks.subprocess.run")
169
- def test_git_timeout_returns_empty(self, mock_run: MagicMock) -> None:
170
- """When git times out, should return empty (warning, not failure)."""
171
- mock_run.side_effect = [
172
- MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
173
- MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
174
- subprocess.TimeoutExpired(cmd=["git", "rev-list"], timeout=30),
175
- ]
176
-
177
- violations = check_single_commit_when_pr_exists()
178
-
179
- assert violations == []
180
-
181
- @patch("validators.git_checks.subprocess.run")
182
- def test_passes_resolved_branch_name_to_gh(self, mock_run: MagicMock) -> None:
183
- """gh pr list must receive the resolved branch name, never the literal 'HEAD'."""
184
- mock_run.side_effect = [
185
- MagicMock(returncode=0, stdout="feature/my-branch\n", stderr=""),
186
- MagicMock(returncode=0, stdout='[{"baseRefName": "main", "number": 123}]', stderr=""),
187
- MagicMock(returncode=0, stdout="1", stderr=""),
188
- ]
189
-
190
- check_single_commit_when_pr_exists()
191
-
192
- mock_run.assert_any_call(
193
- ["gh", "pr", "list", "--head", "feature/my-branch", "--json", "baseRefName,number"],
194
- capture_output=True,
195
- text=True,
196
- check=True,
197
- timeout=30,
198
- )
199
-
200
- @patch("validators.git_checks.subprocess.run")
201
- def test_unresolved_branch_returns_empty(self, mock_run: MagicMock) -> None:
202
- """When current branch cannot be resolved, should return empty."""
203
- mock_run.side_effect = [
204
- MagicMock(returncode=0, stdout="\n", stderr=""),
205
- ]
206
-
207
- violations = check_single_commit_when_pr_exists()
208
-
209
- assert violations == []
210
- mock_run.assert_called_once()
211
-
212
-
213
15
  class TestDraftPrState:
214
16
  """Test that PR is in draft state when pushing review fixes."""
215
17
 
@@ -274,16 +76,13 @@ class TestDraftPrState:
274
76
  class TestMain:
275
77
  """Test main function integration."""
276
78
 
277
- @patch("validators.git_checks.check_single_commit_when_pr_exists")
278
79
  @patch("validators.git_checks.check_draft_pr_state")
279
80
  def test_main_no_violations_exits_zero(
280
81
  self,
281
82
  mock_draft: MagicMock,
282
- mock_commit: MagicMock,
283
83
  capsys,
284
84
  ) -> None:
285
85
  """main() should exit 0 when no violations found."""
286
- mock_commit.return_value = []
287
86
  mock_draft.return_value = []
288
87
 
289
88
  with pytest.raises(SystemExit) as exc_info:
@@ -293,37 +92,13 @@ class TestMain:
293
92
  captured = capsys.readouterr()
294
93
  assert captured.out == ""
295
94
 
296
- @patch("validators.git_checks.check_single_commit_when_pr_exists")
297
- @patch("validators.git_checks.check_draft_pr_state")
298
- def test_main_with_violations_exits_one(
299
- self,
300
- mock_draft: MagicMock,
301
- mock_commit: MagicMock,
302
- capsys,
303
- ) -> None:
304
- """main() should exit 1 and print violations when found."""
305
- mock_commit.return_value = [
306
- Violation(file="", line=0, message="Branch has 3 commits ahead")
307
- ]
308
- mock_draft.return_value = []
309
-
310
- with pytest.raises(SystemExit) as exc_info:
311
- main()
312
-
313
- assert exc_info.value.code == 1
314
- captured = capsys.readouterr()
315
- assert "Branch has 3 commits ahead" in captured.out
316
-
317
- @patch("validators.git_checks.check_single_commit_when_pr_exists")
318
95
  @patch("validators.git_checks.check_draft_pr_state")
319
96
  def test_main_prints_violations_without_file_line(
320
97
  self,
321
98
  mock_draft: MagicMock,
322
- mock_commit: MagicMock,
323
99
  capsys,
324
100
  ) -> None:
325
101
  """main() should print git violations without file:line: prefix."""
326
- mock_commit.return_value = []
327
102
  mock_draft.return_value = [
328
103
  Violation(file="", line=0, message="PR must be in draft state")
329
104
  ]
@@ -335,27 +110,3 @@ class TestMain:
335
110
  captured = capsys.readouterr()
336
111
  assert captured.out == "PR must be in draft state\n"
337
112
  assert ":0:" not in captured.out
338
-
339
- @patch("validators.git_checks.check_single_commit_when_pr_exists")
340
- @patch("validators.git_checks.check_draft_pr_state")
341
- def test_main_prints_all_violations(
342
- self,
343
- mock_draft: MagicMock,
344
- mock_commit: MagicMock,
345
- capsys,
346
- ) -> None:
347
- """main() should print all violations."""
348
- mock_commit.return_value = [
349
- Violation(file="", line=0, message="Branch has 2 commits")
350
- ]
351
- mock_draft.return_value = [
352
- Violation(file="", line=0, message="PR not in draft")
353
- ]
354
-
355
- with pytest.raises(SystemExit) as exc_info:
356
- main()
357
-
358
- assert exc_info.value.code == 1
359
- captured = capsys.readouterr()
360
- assert "Branch has 2 commits" in captured.out
361
- assert "PR not in draft" in captured.out
@@ -398,3 +398,25 @@ class TestHooksSubprocessWorkingDirectory:
398
398
  )
399
399
  assert working_directory_string == tempfile.gettempdir()
400
400
  assert not working_directory_string.startswith("\\\\")
401
+
402
+
403
+ class TestDocstringExamplePaths:
404
+ def test_module_source_carries_no_home_directory_example_paths(self) -> None:
405
+ """Example paths avoid /home so the privacy gate reads no personal data.
406
+
407
+ The gate allowlists a fixed set of placeholder home usernames, so an
408
+ invented name outside that set reads as somebody's real home directory
409
+ and denies the commit. Rooting an example somewhere other than /home
410
+ keeps the illustration and leaves nothing for the gate to flag.
411
+ """
412
+ module_source = (Path(__file__).parent / "run_all_validators.py").read_text(
413
+ encoding="utf-8"
414
+ )
415
+
416
+ all_home_example_lines = [
417
+ each_line
418
+ for each_line in module_source.splitlines()
419
+ if '"/home/' in each_line
420
+ ]
421
+
422
+ assert all_home_example_lines == []