claude-dev-env 1.95.0 → 2.0.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 (241) hide show
  1. package/_shared/advisor/CLAUDE.md +2 -2
  2. package/_shared/advisor/advisor-protocol.md +20 -20
  3. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +15 -12
  4. package/_shared/advisor/scripts/model_tier_run_validator.py +11 -10
  5. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +25 -19
  6. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +17 -17
  7. package/_shared/advisor/scripts/tier_model_ids.py +18 -18
  8. package/_shared/pr-loop/CLAUDE.md +1 -0
  9. package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
  10. package/_shared/pr-loop/scripts/README.md +1 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate.py +253 -1980
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +32 -0
  13. package/_shared/pr-loop/scripts/code_rules_gate_parts/__init__.py +7 -0
  14. package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +268 -0
  15. package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +172 -0
  16. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +70 -0
  17. package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +326 -0
  18. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +85 -0
  19. package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +331 -0
  20. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +369 -0
  21. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/conftest.py +14 -0
  22. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_added_line_maps.py +118 -0
  23. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +17 -0
  24. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +29 -0
  25. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +99 -0
  26. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_blob_readers.py +69 -0
  27. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_file_sets.py +137 -0
  28. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +116 -0
  29. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_violation_scoping.py +75 -0
  30. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_wrapper_plumb_check.py +49 -0
  31. package/_shared/pr-loop/scripts/code_rules_gate_parts/violation_scoping.py +328 -0
  32. package/_shared/pr-loop/scripts/code_rules_gate_parts/wrapper_plumb_check.py +206 -0
  33. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +24 -17
  34. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
  35. package/_shared/pr-loop/scripts/reviews_disabled.py +19 -2
  36. package/_shared/pr-loop/scripts/test_code_rules_gate.py +278 -0
  37. package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +6 -39
  38. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +43 -0
  39. package/_shared/pr-loop/worker-spawn.md +186 -0
  40. package/agents/code-verifier.md +1 -1
  41. package/bin/ever-shipped-skills.mjs +3 -0
  42. package/bin/expand_home_directory_tokens.mjs +1 -1
  43. package/bin/install.mjs +5 -2
  44. package/hooks/advisory/refactor_guard.py +3 -4
  45. package/hooks/blocking/CLAUDE.md +7 -1
  46. package/hooks/blocking/block_main_commit.py +2 -2
  47. package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
  48. package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
  49. package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
  50. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
  51. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
  52. package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
  53. package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
  54. package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
  55. package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
  56. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
  57. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
  58. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
  59. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
  60. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
  61. package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
  62. package/hooks/blocking/code_rules_mock_completeness.py +1 -1
  63. package/hooks/blocking/code_rules_optional_params.py +2 -2
  64. package/hooks/blocking/code_rules_shared.py +1 -1
  65. package/hooks/blocking/code_rules_test_assertions.py +1 -1
  66. package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
  67. package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
  68. package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
  69. package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
  70. package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
  71. package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
  72. package/hooks/blocking/inventory_intent_records/records.py +271 -0
  73. package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
  74. package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
  75. package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
  76. package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
  77. package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
  78. package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
  79. package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
  80. package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
  81. package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
  82. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
  83. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
  84. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
  85. package/hooks/blocking/pii_payload_scan.py +138 -42
  86. package/hooks/blocking/pii_prevention_blocker.py +185 -291
  87. package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
  88. package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
  89. package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
  90. package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
  91. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
  92. package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
  93. package/hooks/blocking/pr_description_command_parser.py +8 -4
  94. package/hooks/blocking/precommit_code_rules_gate.py +3 -3
  95. package/hooks/blocking/tdd_enforcer.py +97 -608
  96. package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
  97. package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
  98. package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
  99. package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
  100. package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
  101. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
  102. package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
  103. package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
  104. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
  105. package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
  106. package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
  107. package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
  108. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
  109. package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
  110. package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
  111. package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
  112. package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
  113. package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
  114. package/hooks/blocking/test_pii_payload_scan.py +168 -0
  115. package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
  116. package/hooks/blocking/test_verifier_verdict_minter.py +55 -158
  117. package/hooks/blocking/tests/conftest.py +10 -0
  118. package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
  119. package/hooks/blocking/tests/test_repository_exemption.py +105 -0
  120. package/hooks/blocking/tests/test_repository_resolution.py +108 -0
  121. package/hooks/diagnostic/hook_log_extractor.py +12 -10
  122. package/hooks/git-hooks/post_commit.py +3 -4
  123. package/hooks/hooks_constants/CLAUDE.md +2 -2
  124. package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
  125. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  126. package/hooks/hooks_constants/local_identity.py +59 -8
  127. package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
  128. package/hooks/hooks_constants/test_local_identity.py +105 -3
  129. package/hooks/pyproject.toml +13 -36
  130. package/hooks/session/plugin_data_dir_cleanup.py +0 -1
  131. package/hooks/validation/mypy_validator.py +2 -2
  132. package/hooks/validators/health_check.py +1 -0
  133. package/hooks/validators/mypy_integration.py +2 -0
  134. package/hooks/validators/ruff_integration.py +3 -0
  135. package/hooks/workflow/auto_formatter.py +5 -4
  136. package/package.json +1 -1
  137. package/scripts/CLAUDE.md +4 -0
  138. package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
  139. package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
  140. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
  141. package/scripts/dev_env_scripts_constants/timing.py +7 -1
  142. package/scripts/grok_headless_runner.py +294 -0
  143. package/scripts/grok_worker_preflight.py +410 -0
  144. package/scripts/invoke_code_review.py +463 -0
  145. package/scripts/resolve_worker_spawn.py +619 -0
  146. package/scripts/spawn_grok_batch.py +672 -0
  147. package/scripts/test_grok_headless_runner.py +626 -0
  148. package/scripts/test_grok_worker_preflight.py +1054 -0
  149. package/scripts/test_invoke_code_review.py +672 -0
  150. package/scripts/test_resolve_worker_spawn.py +1014 -0
  151. package/scripts/test_spawn_grok_batch.py +1017 -0
  152. package/skills/CLAUDE.md +5 -3
  153. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
  154. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
  155. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
  156. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
  157. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
  158. package/skills/autoconverge/CLAUDE.md +3 -3
  159. package/skills/autoconverge/SKILL.md +9 -3
  160. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  161. package/skills/autoconverge/reference/convergence.md +33 -11
  162. package/skills/autoconverge/reference/stop-conditions.md +16 -5
  163. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  164. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
  165. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
  166. package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
  167. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
  168. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  169. package/skills/autoconverge/workflow/converge.mjs +200 -16
  170. package/skills/bugteam/CLAUDE.md +2 -2
  171. package/skills/bugteam/CONSTRAINTS.md +3 -2
  172. package/skills/bugteam/PROMPTS.md +7 -6
  173. package/skills/bugteam/SKILL.md +18 -13
  174. package/skills/bugteam/reference/audit-and-teammates.md +215 -35
  175. package/skills/bugteam/reference/design-rationale.md +1 -1
  176. package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
  177. package/skills/bugteam/reference/team-setup.md +8 -2
  178. package/skills/codex-review/CLAUDE.md +46 -0
  179. package/skills/codex-review/SKILL.md +181 -0
  180. package/skills/codex-review/reference/CLAUDE.md +15 -0
  181. package/skills/codex-review/reference/cli-contract.md +253 -0
  182. package/skills/codex-review/reference/loop-integration.md +118 -0
  183. package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
  184. package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
  185. package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
  186. package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
  187. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
  188. package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
  189. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
  190. package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
  191. package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
  192. package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
  193. package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
  194. package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
  195. package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
  196. package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
  197. package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
  198. package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
  199. package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
  200. package/skills/codex-review/scripts/run_codex_review.py +415 -0
  201. package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
  202. package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
  203. package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
  204. package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
  205. package/skills/codex-review/test_skill_scaffold.py +192 -0
  206. package/skills/grok-spawn/CLAUDE.md +28 -0
  207. package/skills/grok-spawn/SKILL.md +226 -0
  208. package/skills/grok-spawn/reference/flag-profiles.md +132 -0
  209. package/skills/grok-spawn/reference/worker-briefs.md +152 -0
  210. package/skills/grokify/SKILL.md +9 -1
  211. package/skills/grokify/capability-claims.test.mjs +28 -0
  212. package/skills/grokify/evals/README.md +72 -0
  213. package/skills/grokify/evals/parse-payload.test.mjs +171 -0
  214. package/skills/grokify/evals/run-capability-evals.mjs +545 -0
  215. package/skills/orchestrator/SKILL.md +42 -29
  216. package/skills/orchestrator-refresh/SKILL.md +17 -9
  217. package/skills/pr-converge/SKILL.md +34 -13
  218. package/skills/pr-converge/reference/convergence-gates.md +42 -15
  219. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  220. package/skills/pr-converge/reference/ground-rules.md +1 -1
  221. package/skills/pr-converge/reference/per-tick.md +130 -42
  222. package/skills/pr-converge/reference/state-schema.md +10 -0
  223. package/skills/pr-converge/scripts/CLAUDE.md +2 -0
  224. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
  225. package/skills/pr-converge/scripts/check_convergence.py +605 -29
  226. package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
  227. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
  228. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
  229. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
  230. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
  231. package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
  232. package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
  233. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
  234. package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
  235. package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
  236. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
  237. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
  238. package/skills/pr-converge/test_step5_host_branch.py +106 -0
  239. package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
  240. package/skills/reviewer-gates/SKILL.md +7 -5
  241. package/skills/team-advisor/SKILL.md +7 -7
@@ -0,0 +1,32 @@
1
+ # code_rules_gate_parts
2
+
3
+ The modules `code_rules_gate.py` wires together to run the code-rules gate. Each
4
+ module owns one concern; the entry script imports them and re-exports their
5
+ surface for the test suite.
6
+
7
+ ## Modules
8
+
9
+ | File | Purpose |
10
+ |---|---|
11
+ | `enforcer_loading.py` | Locates and loads `code_rules_enforcer.validate_content` from disk with the hooks directory on `sys.path` |
12
+ | `git_file_sets.py` | Resolves the diff, staged, and untracked file sets from git, plus prefix filtering and staged line-span helpers |
13
+ | `git_blob_readers.py` | Reads the committed and staged content of one file, and probes staged-index presence |
14
+ | `added_line_maps.py` | Maps each changed file to the line numbers the current diff added, resolving renames and new files |
15
+ | `violation_scoping.py` | Recovers a violation's line span from the enforcer message and partitions violations into blocking versus advisory |
16
+ | `wrapper_plumb_check.py` | Flags a public wrapper that drops a same-file delegate's optional keyword arguments; holds the code-path and test-path classifiers |
17
+ | `gate_running.py` | Validates the eligible file set, reports the inspected-file count, and prints the partitioned violation report |
18
+ | `staged_test_running.py` | Runs the staged Python test files, grouped by their owning pytest config, in command-line-length-safe batches |
19
+ | `gate_arguments.py` | Parses the gate's command-line arguments |
20
+ | `__init__.py` | Package marker |
21
+
22
+ ## Subdirectory
23
+
24
+ | Entry | Description |
25
+ |---|---|
26
+ | `tests/` | pytest suite with one test module per module above |
27
+
28
+ ## Running tests
29
+
30
+ ```bash
31
+ python -m pytest packages/claude-dev-env/_shared/pr-loop/scripts/code_rules_gate_parts/tests/
32
+ ```
@@ -0,0 +1,7 @@
1
+ """Decomposed modules that make up the code-rules gate entry point.
2
+
3
+ Each module owns one concern of the gate: enforcer loading, git file-set
4
+ resolution, added-line maps, violation scoping, wrapper plumb-through
5
+ detection, gate execution, and staged-test running. The ``code_rules_gate``
6
+ entry module in the parent directory wires them together.
7
+ """
@@ -0,0 +1,268 @@
1
+ """Map each changed file to the line numbers the current diff added.
2
+
3
+ The gate scopes a violation by whether its span meets an added line, so it needs
4
+ the added-line set per file. This module resolves that set for an ordinary
5
+ change, a brand-new file (its whole line span), and a rename (only the lines new
6
+ relative to the source blob).
7
+ """
8
+
9
+ import subprocess
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ from pr_loop_shared_constants.code_rules_gate_constants import (
14
+ EXPECTED_NON_RENAME_COLUMN_COUNT,
15
+ EXPECTED_RENAME_COLUMN_COUNT,
16
+ GIT_NAME_STATUS_RENAMED_PREFIX,
17
+ )
18
+ from terminology_sweep import repository_environment
19
+
20
+ from code_rules_gate_parts.git_file_sets import (
21
+ _git_bytes_or_exit,
22
+ _git_text_or_exit,
23
+ resolve_merge_base,
24
+ )
25
+ from code_rules_gate_parts.violation_scoping import parse_added_line_numbers
26
+
27
+
28
+ def _run_git_text_capture(
29
+ repository_root: Path, all_git_arguments: list[str]
30
+ ) -> subprocess.CompletedProcess[str]:
31
+ """Run a git command in text mode and return the completed process."""
32
+ return subprocess.run(
33
+ all_git_arguments,
34
+ cwd=str(repository_root),
35
+ capture_output=True,
36
+ text=True,
37
+ encoding="utf-8",
38
+ errors="replace",
39
+ check=False,
40
+ env=repository_environment(),
41
+ )
42
+
43
+
44
+ def is_file_new_at_base(repository_root: Path, merge_base: str, relative_path_posix: str) -> bool:
45
+ """Check whether *relative_path_posix* did not exist at *merge_base*.
46
+
47
+ Args:
48
+ repository_root: Repository root used as the ``git -C`` target.
49
+ merge_base: The merge-base SHA against which to check existence.
50
+ relative_path_posix: Repository-relative POSIX path to inspect.
51
+
52
+ Returns:
53
+ True when the blob is absent at the merge base (added on the HEAD side).
54
+ """
55
+ completed = _run_git_text_capture(
56
+ repository_root,
57
+ ["git", "cat-file", "-e", f"{merge_base}:{relative_path_posix}"],
58
+ )
59
+ return completed.returncode != 0
60
+
61
+
62
+ def added_lines_for_file(
63
+ repository_root: Path, merge_base: str, relative_path_posix: str
64
+ ) -> set[int]:
65
+ """Return added line numbers for *relative_path_posix* since *merge_base*.
66
+
67
+ Args:
68
+ repository_root: Repository root used as the ``git -C`` target.
69
+ merge_base: The merge-base SHA against which to diff.
70
+ relative_path_posix: Repository-relative POSIX path to inspect.
71
+
72
+ Returns:
73
+ The line numbers added on the HEAD side of the diff.
74
+
75
+ Raises:
76
+ SystemExit: When the diff command returns non-zero.
77
+ """
78
+ diff_text = _git_text_or_exit(
79
+ repository_root,
80
+ ["git", "diff", "--unified=0", f"{merge_base}..HEAD", "--", relative_path_posix],
81
+ f"code_rules_gate: git diff --unified=0 failed for {relative_path_posix}",
82
+ )
83
+ if not diff_text.strip():
84
+ return set()
85
+ return parse_added_line_numbers(diff_text)
86
+
87
+
88
+ def whole_file_line_set(file_path: Path) -> set[int]:
89
+ """Return the set of line numbers covering an entire file.
90
+
91
+ Args:
92
+ file_path: Path to the file whose line span is summarized.
93
+
94
+ Returns:
95
+ Every line number in *file_path*, or an empty set when it is unreadable
96
+ or empty.
97
+ """
98
+ try:
99
+ total_lines = len(file_path.read_text(encoding="utf-8").splitlines())
100
+ except (OSError, UnicodeDecodeError) as read_error:
101
+ sys.stderr.write(f"code_rules_gate: skipping unreadable file {file_path}: {read_error}\n")
102
+ return set()
103
+ if total_lines <= 0:
104
+ return set()
105
+ return set(range(1, total_lines + 1))
106
+
107
+
108
+ def _rename_pairs_from_tokens(all_tokens: list[str]) -> dict[str, str]:
109
+ """Walk null-separated name-status tokens into destination-to-source pairs.
110
+
111
+ Args:
112
+ all_tokens: The decoded name-status tokens, in stream order.
113
+
114
+ Returns:
115
+ A mapping from rename-destination path to rename-source path.
116
+ """
117
+ rename_source_by_destination: dict[str, str] = {}
118
+ next_token_index = 0
119
+ while next_token_index < len(all_tokens):
120
+ status_code = all_tokens[next_token_index]
121
+ if not status_code.startswith(GIT_NAME_STATUS_RENAMED_PREFIX):
122
+ next_token_index += EXPECTED_NON_RENAME_COLUMN_COUNT
123
+ continue
124
+ if next_token_index + EXPECTED_RENAME_COLUMN_COUNT > len(all_tokens):
125
+ break
126
+ rename_slice = all_tokens[
127
+ next_token_index + 1 : next_token_index + EXPECTED_RENAME_COLUMN_COUNT
128
+ ]
129
+ destination_path = rename_slice[1].replace("\\", "/")
130
+ rename_source_by_destination[destination_path] = rename_slice[0].replace("\\", "/")
131
+ next_token_index += EXPECTED_RENAME_COLUMN_COUNT
132
+ return rename_source_by_destination
133
+
134
+
135
+ def renamed_file_source_map_since(repository_root: Path, merge_base: str) -> dict[str, str]:
136
+ """Return a mapping from rename-destination path to rename-source path.
137
+
138
+ ::
139
+
140
+ ok: a path holding a tab byte round-trips through the -z stream unmangled
141
+
142
+ Args:
143
+ repository_root: Repository root used as the ``git -C`` target.
144
+ merge_base: The merge-base SHA against which to diff.
145
+
146
+ Returns:
147
+ A mapping from rename-destination POSIX path to rename-source path.
148
+
149
+ Raises:
150
+ SystemExit: When ``git diff --name-status`` returns non-zero.
151
+ """
152
+ raw_stdout = _git_bytes_or_exit(
153
+ repository_root,
154
+ ["git", "diff", "--name-status", "-M", "-z", f"{merge_base}..HEAD"],
155
+ "code_rules_gate: git diff --name-status -M -z failed",
156
+ )
157
+ all_tokens = [
158
+ each_token.decode("utf-8", errors="replace")
159
+ for each_token in raw_stdout.split(b"\x00")
160
+ if each_token
161
+ ]
162
+ return _rename_pairs_from_tokens(all_tokens)
163
+
164
+
165
+ def added_lines_for_renamed_file(
166
+ repository_root: Path,
167
+ merge_base: str,
168
+ source_posix: str,
169
+ destination_posix: str,
170
+ ) -> set[int]:
171
+ """Return added line numbers for a renamed file via blob comparison.
172
+
173
+ Args:
174
+ repository_root: Repository root used as the ``git -C`` target.
175
+ merge_base: The merge-base SHA against which to compare blobs.
176
+ source_posix: Rename-source POSIX path at the merge base.
177
+ destination_posix: Rename-destination POSIX path at HEAD.
178
+
179
+ Returns:
180
+ The line numbers added on the HEAD side; empty on diff failure.
181
+ """
182
+ source_reference = f"{merge_base}:{source_posix}"
183
+ destination_reference = f"HEAD:{destination_posix}"
184
+ completed = _run_git_text_capture(
185
+ repository_root,
186
+ ["git", "diff", "--unified=0", source_reference, destination_reference],
187
+ )
188
+ if completed.returncode != 0:
189
+ sys.stderr.write(f"code_rules_gate: rename diff failed: {completed.stderr.strip()}\n")
190
+ return set()
191
+ if not completed.stdout.strip():
192
+ return set()
193
+ return parse_added_line_numbers(completed.stdout)
194
+
195
+
196
+ def _resolved_under_root(each_path: Path, resolved_root: Path) -> Path | None:
197
+ """Return *each_path* resolved when it sits under *resolved_root*, else None."""
198
+ try:
199
+ resolved = each_path.resolve()
200
+ except OSError:
201
+ return None
202
+ try:
203
+ resolved.relative_to(resolved_root)
204
+ except ValueError:
205
+ return None
206
+ return resolved
207
+
208
+
209
+ def _added_lines_for_one_path(
210
+ resolved_root: Path,
211
+ merge_base: str,
212
+ all_rename_sources: dict[str, str],
213
+ resolved: Path,
214
+ ) -> set[int]:
215
+ """Resolve added lines for one path, honoring renames and new files.
216
+
217
+ Args:
218
+ resolved_root: The resolved repository root.
219
+ merge_base: The merge-base SHA against which to diff.
220
+ all_rename_sources: Destination-to-source rename map for the range.
221
+ resolved: The resolved absolute path to inspect.
222
+
223
+ Returns:
224
+ The added line numbers for the path.
225
+ """
226
+ relative_posix = str(resolved.relative_to(resolved_root)).replace("\\", "/")
227
+ if relative_posix in all_rename_sources:
228
+ return added_lines_for_renamed_file(
229
+ resolved_root, merge_base, all_rename_sources[relative_posix], relative_posix
230
+ )
231
+ added_numbers = added_lines_for_file(resolved_root, merge_base, relative_posix)
232
+ if (
233
+ not added_numbers
234
+ and resolved.is_file()
235
+ and is_file_new_at_base(resolved_root, merge_base, relative_posix)
236
+ ):
237
+ return whole_file_line_set(resolved)
238
+ return added_numbers
239
+
240
+
241
+ def added_lines_by_file(
242
+ repository_root: Path,
243
+ base_reference: str,
244
+ all_file_paths: list[Path],
245
+ ) -> dict[Path, set[int]]:
246
+ """Build a per-file map of added line numbers across the branch.
247
+
248
+ Args:
249
+ repository_root: Repository root for diff invocations.
250
+ base_reference: The git reference to merge-base against.
251
+ all_file_paths: File paths whose added lines are collected.
252
+
253
+ Returns:
254
+ A mapping from resolved file path to its added line numbers, with
255
+ renames resolved against the original source path.
256
+ """
257
+ merge_base = resolve_merge_base(repository_root, base_reference)
258
+ resolved_root = repository_root.resolve()
259
+ all_rename_sources = renamed_file_source_map_since(resolved_root, merge_base)
260
+ added_by_path: dict[Path, set[int]] = {}
261
+ for each_path in all_file_paths:
262
+ resolved = _resolved_under_root(each_path, resolved_root)
263
+ if resolved is None:
264
+ continue
265
+ added_by_path[resolved] = _added_lines_for_one_path(
266
+ resolved_root, merge_base, all_rename_sources, resolved
267
+ )
268
+ return added_by_path
@@ -0,0 +1,172 @@
1
+ """Load the code-rules enforcer's ``validate_content`` for in-process use.
2
+
3
+ The gate runs the same ``validate_content`` the PreToolUse enforcer runs, so it
4
+ locates the ``hooks/blocking/code_rules_enforcer.py`` module from disk, executes
5
+ it with the hooks directory on ``sys.path``, and hands back its callable.
6
+ """
7
+
8
+ import importlib.machinery
9
+ import importlib.util
10
+ import sys
11
+ from collections.abc import Callable
12
+ from pathlib import Path
13
+ from types import ModuleType
14
+
15
+ from pr_loop_shared_constants.code_rules_gate_constants import GATE_ERROR_EXIT_CODE
16
+
17
+ ValidateContentCallable = Callable[..., list[str]]
18
+
19
+ _ENFORCER_RELATIVE_PATH = Path("hooks") / "blocking" / "code_rules_enforcer.py"
20
+
21
+
22
+ def _first_ancestor_with_enforcer(starting_form: Path) -> Path | None:
23
+ """Return the first ancestor of *starting_form* that holds the enforcer.
24
+
25
+ Args:
26
+ starting_form: A resolved or absolute path to climb from.
27
+
28
+ Returns:
29
+ The ancestor directory containing the enforcer file, or None.
30
+ """
31
+ for each_candidate in [starting_form, *starting_form.parents]:
32
+ if (each_candidate / _ENFORCER_RELATIVE_PATH).is_file():
33
+ return each_candidate
34
+ return None
35
+
36
+
37
+ def resolve_claude_dev_env_root(starting_path: Path) -> Path:
38
+ """Walk up from *starting_path* to the claude-dev-env package root.
39
+
40
+ Args:
41
+ starting_path: A path inside the worktree; the function climbs to the
42
+ ancestor holding ``hooks/blocking/code_rules_enforcer.py``.
43
+
44
+ Returns:
45
+ The resolved package root that contains the enforcer file.
46
+
47
+ Raises:
48
+ SystemExit: When no ancestor contains the enforcer.
49
+ """
50
+ starting = Path(starting_path).resolve()
51
+ found_root = _first_ancestor_with_enforcer(starting)
52
+ if found_root is not None:
53
+ return found_root
54
+ sys.stderr.write(
55
+ f"code_rules_gate: could not locate {_ENFORCER_RELATIVE_PATH} above {starting}\n"
56
+ )
57
+ raise SystemExit(GATE_ERROR_EXIT_CODE)
58
+
59
+
60
+ def _resolve_package_root_absolute(starting_path: Path) -> Path:
61
+ """Return the enforcer-bearing root from an absolute or resolved form.
62
+
63
+ Args:
64
+ starting_path: The path to climb from, tried both absolute and resolved.
65
+
66
+ Returns:
67
+ The ancestor directory containing the enforcer file.
68
+
69
+ Raises:
70
+ SystemExit: When neither form finds the enforcer.
71
+ """
72
+ for each_starting_form in (
73
+ Path(starting_path).absolute(),
74
+ Path(starting_path).resolve(),
75
+ ):
76
+ found_root = _first_ancestor_with_enforcer(each_starting_form)
77
+ if found_root is not None:
78
+ return found_root
79
+ raise SystemExit(GATE_ERROR_EXIT_CODE)
80
+
81
+
82
+ def _hooks_constants_module_names() -> list[str]:
83
+ """Return the loaded module names that belong to the hooks constants package."""
84
+ return [
85
+ each_key
86
+ for each_key in list(sys.modules)
87
+ if each_key == "hooks_constants" or each_key.startswith("hooks_constants.")
88
+ ]
89
+
90
+
91
+ def _load_enforcer_specification(
92
+ enforcer_path: Path,
93
+ ) -> importlib.machinery.ModuleSpec:
94
+ """Return an import spec for the enforcer module at *enforcer_path*.
95
+
96
+ Args:
97
+ enforcer_path: The on-disk path of ``code_rules_enforcer.py``.
98
+
99
+ Returns:
100
+ A loadable module specification for the enforcer.
101
+
102
+ Raises:
103
+ SystemExit: When the file is missing or a spec cannot be built.
104
+ """
105
+ if not enforcer_path.is_file():
106
+ sys.stderr.write(f"code_rules_gate: missing enforcer at {enforcer_path}\n")
107
+ raise SystemExit(GATE_ERROR_EXIT_CODE)
108
+ specification = importlib.util.spec_from_file_location("code_rules_enforcer", enforcer_path)
109
+ if specification is None or specification.loader is None:
110
+ sys.stderr.write("code_rules_gate: could not load code_rules_enforcer.\n")
111
+ raise SystemExit(GATE_ERROR_EXIT_CODE)
112
+ return specification
113
+
114
+
115
+ def _exec_enforcer_with_hooks_on_path(
116
+ specification: importlib.machinery.ModuleSpec, module: ModuleType
117
+ ) -> None:
118
+ """Execute *module* with the hooks directory temporarily on ``sys.path``.
119
+
120
+ Args:
121
+ specification: The loadable spec whose loader executes the module.
122
+ module: The freshly created enforcer module object to populate.
123
+ """
124
+ loader = specification.loader
125
+ assert loader is not None
126
+ package_root_for_imports = _resolve_package_root_absolute(Path(__file__).absolute())
127
+ hooks_root_path = str(package_root_for_imports / "hooks")
128
+ while hooks_root_path in sys.path:
129
+ sys.path.remove(hooks_root_path)
130
+ if hooks_root_path not in sys.path:
131
+ sys.path.insert(0, hooks_root_path)
132
+ all_saved_modules: dict[str, ModuleType] = {
133
+ each_name: sys.modules.pop(each_name) for each_name in _hooks_constants_module_names()
134
+ }
135
+ try:
136
+ loader.exec_module(module)
137
+ finally:
138
+ _restore_hooks_constants_modules(all_saved_modules, hooks_root_path)
139
+
140
+
141
+ def load_validate_content() -> ValidateContentCallable:
142
+ """Load ``code_rules_enforcer.validate_content`` for in-process use.
143
+
144
+ Returns:
145
+ The ``validate_content`` callable from the enforcer module.
146
+
147
+ Raises:
148
+ SystemExit: When the package root or the enforcer module cannot load.
149
+ """
150
+ package_root = resolve_claude_dev_env_root(Path(__file__).resolve())
151
+ specification = _load_enforcer_specification(
152
+ package_root / "hooks" / "blocking" / "code_rules_enforcer.py"
153
+ )
154
+ module = importlib.util.module_from_spec(specification)
155
+ _exec_enforcer_with_hooks_on_path(specification, module)
156
+ return module.validate_content
157
+
158
+
159
+ def _restore_hooks_constants_modules(
160
+ all_saved_modules: dict[str, ModuleType], hooks_root_path: str
161
+ ) -> None:
162
+ """Restore the sys.path and hooks-constants state after loading the enforcer.
163
+
164
+ Args:
165
+ all_saved_modules: The hooks-constants modules captured before loading.
166
+ hooks_root_path: The hooks directory added to sys.path for the load.
167
+ """
168
+ while hooks_root_path in sys.path:
169
+ sys.path.remove(hooks_root_path)
170
+ for each_name in _hooks_constants_module_names():
171
+ sys.modules.pop(each_name, None)
172
+ sys.modules.update(all_saved_modules)
@@ -0,0 +1,70 @@
1
+ """Parse the command-line arguments for the code-rules gate."""
2
+
3
+ import argparse
4
+ from pathlib import Path
5
+
6
+
7
+ def _add_source_arguments(parser: argparse.ArgumentParser) -> None:
8
+ """Add the repo-root, base-ref, and staged-mode arguments to *parser*."""
9
+ parser.add_argument(
10
+ "--repo-root",
11
+ type=Path,
12
+ default=None,
13
+ help="Repository root (default: cwd).",
14
+ )
15
+ parser.add_argument(
16
+ "--base",
17
+ default="origin/main",
18
+ help="Merge-base ref for git diff (default: origin/main).",
19
+ )
20
+ parser.add_argument(
21
+ "--staged",
22
+ action="store_true",
23
+ default=False,
24
+ help="Scope to staged changes only (git diff --cached).",
25
+ )
26
+
27
+
28
+ def _add_filter_arguments(parser: argparse.ArgumentParser) -> None:
29
+ """Add the only-under filter and the explicit-paths arguments to *parser*."""
30
+ parser.add_argument(
31
+ "--only-under",
32
+ action="append",
33
+ default=[],
34
+ dest="only_under",
35
+ metavar="PREFIX",
36
+ help="Keep only files whose repo-relative POSIX path starts with or "
37
+ "equals PREFIX (repeatable).",
38
+ )
39
+ parser.add_argument(
40
+ "paths",
41
+ nargs="*",
42
+ type=Path,
43
+ help="Optional explicit files; if set, git diff is not used.",
44
+ )
45
+
46
+
47
+ def _build_argument_parser() -> argparse.ArgumentParser:
48
+ """Return the argument parser with every gate option registered."""
49
+ parser = argparse.ArgumentParser(
50
+ description=(
51
+ "Run the code-rules validators on files in the working tree. Default "
52
+ "file set: git diff since the merge-base joined with untracked files."
53
+ ),
54
+ )
55
+ _add_source_arguments(parser)
56
+ _add_filter_arguments(parser)
57
+ return parser
58
+
59
+
60
+ def parse_arguments(all_arguments: list[str]) -> argparse.Namespace:
61
+ """Parse the command-line arguments for the code-rules gate.
62
+
63
+ Args:
64
+ all_arguments: Command-line argument list forwarded to argparse.
65
+
66
+ Returns:
67
+ The parsed namespace with ``repo_root``, ``base``, ``staged``,
68
+ ``only_under``, and ``paths`` attributes.
69
+ """
70
+ return _build_argument_parser().parse_args(all_arguments)