claude-dev-env 1.94.0 → 2.0.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 (280) hide show
  1. package/_shared/advisor/CLAUDE.md +2 -2
  2. package/_shared/advisor/advisor-protocol.md +41 -33
  3. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +17 -13
  4. package/_shared/advisor/scripts/model_tier_run_validator.py +27 -18
  5. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +95 -25
  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/CLAUDE.md +10 -1
  42. package/bin/ever-shipped-skills.mjs +73 -0
  43. package/bin/expand_home_directory_tokens.mjs +1 -1
  44. package/bin/install.mjs +140 -7
  45. package/bin/install.prune.test.mjs +457 -0
  46. package/docs/CODE_RULES.md +1 -1
  47. package/hooks/advisory/refactor_guard.py +3 -4
  48. package/hooks/blocking/CLAUDE.md +7 -1
  49. package/hooks/blocking/block_main_commit.py +2 -2
  50. package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
  51. package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
  52. package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
  53. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
  54. package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
  55. package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
  56. package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
  57. package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
  58. package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
  59. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
  60. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
  61. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
  62. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
  63. package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
  64. package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
  65. package/hooks/blocking/code_rules_enforcer.py +4 -0
  66. package/hooks/blocking/code_rules_mock_completeness.py +1 -1
  67. package/hooks/blocking/code_rules_optional_params.py +2 -2
  68. package/hooks/blocking/code_rules_shared.py +83 -1
  69. package/hooks/blocking/code_rules_test_assertions.py +1 -1
  70. package/hooks/blocking/code_rules_test_layout.py +9 -3
  71. package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
  72. package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
  73. package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
  74. package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
  75. package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
  76. package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
  77. package/hooks/blocking/inventory_intent_records/records.py +271 -0
  78. package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
  79. package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
  80. package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
  81. package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
  82. package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
  83. package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
  84. package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
  85. package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
  86. package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
  87. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
  88. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
  89. package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
  90. package/hooks/blocking/pii_payload_scan.py +138 -42
  91. package/hooks/blocking/pii_prevention_blocker.py +185 -291
  92. package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
  93. package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
  94. package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
  95. package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
  96. package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
  97. package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
  98. package/hooks/blocking/plain_language_blocker.py +138 -4
  99. package/hooks/blocking/pr_description_command_parser.py +8 -4
  100. package/hooks/blocking/precommit_code_rules_gate.py +3 -3
  101. package/hooks/blocking/sensitive_file_protector.py +114 -48
  102. package/hooks/blocking/tdd_enforcer.py +97 -601
  103. package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
  104. package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
  105. package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
  106. package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
  107. package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
  108. package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
  109. package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
  110. package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
  111. package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
  112. package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
  113. package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
  114. package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
  115. package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
  116. package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
  117. package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
  118. package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
  119. package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
  120. package/hooks/blocking/test_code_rules_enforcer_scratchpad.py +105 -0
  121. package/hooks/blocking/test_code_rules_shared.py +181 -0
  122. package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
  123. package/hooks/blocking/test_pii_payload_scan.py +168 -0
  124. package/hooks/blocking/test_plain_language_blocker_allowlist.py +184 -0
  125. package/hooks/blocking/test_sensitive_file_protector.py +185 -0
  126. package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
  127. package/hooks/blocking/test_tdd_enforcer_scratchpad.py +105 -0
  128. package/hooks/blocking/tests/conftest.py +10 -0
  129. package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
  130. package/hooks/blocking/tests/test_repository_exemption.py +105 -0
  131. package/hooks/blocking/tests/test_repository_resolution.py +108 -0
  132. package/hooks/diagnostic/hook_log_extractor.py +12 -10
  133. package/hooks/git-hooks/post_commit.py +3 -4
  134. package/hooks/hooks_constants/CLAUDE.md +4 -2
  135. package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
  136. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  137. package/hooks/hooks_constants/harness_scratchpad_constants.py +17 -0
  138. package/hooks/hooks_constants/local_identity.py +59 -8
  139. package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
  140. package/hooks/hooks_constants/plain_language_blocker_constants.py +5 -0
  141. package/hooks/hooks_constants/sensitive_file_protector_constants.py +42 -0
  142. package/hooks/hooks_constants/test_local_identity.py +105 -3
  143. package/hooks/pyproject.toml +52 -4
  144. package/hooks/session/plugin_data_dir_cleanup.py +0 -1
  145. package/hooks/validation/mypy_validator.py +2 -2
  146. package/hooks/validators/CLAUDE.md +1 -1
  147. package/hooks/validators/README.md +2 -0
  148. package/hooks/validators/health_check.py +1 -0
  149. package/hooks/validators/mypy_integration.py +2 -0
  150. package/hooks/validators/python_style_checks.py +114 -136
  151. package/hooks/validators/python_style_helpers.py +95 -0
  152. package/hooks/validators/ruff_integration.py +3 -0
  153. package/hooks/validators/test_python_style_checks.py +0 -164
  154. package/hooks/validators/test_python_style_checks_decorator_gap.py +119 -0
  155. package/hooks/validators/test_python_style_fixes.py +251 -0
  156. package/hooks/validators/test_python_style_helpers.py +125 -0
  157. package/hooks/workflow/auto_formatter.py +5 -4
  158. package/package.json +1 -1
  159. package/rules/CLAUDE.md +1 -0
  160. package/rules/anti-corollary-tests.md +69 -0
  161. package/rules/bdd.md +1 -3
  162. package/rules/code-reviews.md +1 -1
  163. package/rules/gh-paginate.md +1 -1
  164. package/rules/plain-language.md +2 -0
  165. package/scripts/CLAUDE.md +4 -0
  166. package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
  167. package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
  168. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
  169. package/scripts/dev_env_scripts_constants/timing.py +7 -1
  170. package/scripts/grok_headless_runner.py +294 -0
  171. package/scripts/grok_worker_preflight.py +410 -0
  172. package/scripts/invoke_code_review.py +463 -0
  173. package/scripts/resolve_worker_spawn.py +619 -0
  174. package/scripts/spawn_grok_batch.py +672 -0
  175. package/scripts/test_grok_headless_runner.py +626 -0
  176. package/scripts/test_grok_worker_preflight.py +1054 -0
  177. package/scripts/test_invoke_code_review.py +672 -0
  178. package/scripts/test_resolve_worker_spawn.py +1014 -0
  179. package/scripts/test_spawn_grok_batch.py +1017 -0
  180. package/skills/CLAUDE.md +6 -3
  181. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
  182. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
  183. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
  184. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
  185. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
  186. package/skills/autoconverge/CLAUDE.md +3 -3
  187. package/skills/autoconverge/SKILL.md +9 -3
  188. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  189. package/skills/autoconverge/reference/convergence.md +33 -11
  190. package/skills/autoconverge/reference/stop-conditions.md +16 -5
  191. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  192. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
  193. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
  194. package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
  195. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
  196. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  197. package/skills/autoconverge/workflow/converge.mjs +202 -18
  198. package/skills/bugteam/CLAUDE.md +2 -2
  199. package/skills/bugteam/CONSTRAINTS.md +3 -2
  200. package/skills/bugteam/PROMPTS.md +7 -6
  201. package/skills/bugteam/SKILL.md +18 -13
  202. package/skills/bugteam/reference/README.md +2 -3
  203. package/skills/bugteam/reference/audit-and-teammates.md +215 -35
  204. package/skills/bugteam/reference/design-rationale.md +1 -1
  205. package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
  206. package/skills/bugteam/reference/team-setup.md +8 -2
  207. package/skills/closeout/SKILL.md +153 -0
  208. package/skills/closeout/reference/handoff-prompt-template.md +72 -0
  209. package/skills/closeout/reference/issue-body-templates.md +108 -0
  210. package/skills/closeout/reference/pii-redaction-checklist.md +36 -0
  211. package/skills/codex-review/CLAUDE.md +46 -0
  212. package/skills/codex-review/SKILL.md +181 -0
  213. package/skills/codex-review/reference/CLAUDE.md +15 -0
  214. package/skills/codex-review/reference/cli-contract.md +253 -0
  215. package/skills/codex-review/reference/loop-integration.md +118 -0
  216. package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
  217. package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
  218. package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
  219. package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
  220. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
  221. package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
  222. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
  223. package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
  224. package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
  225. package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
  226. package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
  227. package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
  228. package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
  229. package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
  230. package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
  231. package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
  232. package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
  233. package/skills/codex-review/scripts/run_codex_review.py +415 -0
  234. package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
  235. package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
  236. package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
  237. package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
  238. package/skills/codex-review/test_skill_scaffold.py +192 -0
  239. package/skills/grok-spawn/CLAUDE.md +28 -0
  240. package/skills/grok-spawn/SKILL.md +226 -0
  241. package/skills/grok-spawn/reference/flag-profiles.md +132 -0
  242. package/skills/grok-spawn/reference/worker-briefs.md +152 -0
  243. package/skills/grokify/SKILL.md +9 -1
  244. package/skills/grokify/capability-claims.test.mjs +28 -0
  245. package/skills/grokify/evals/README.md +72 -0
  246. package/skills/grokify/evals/parse-payload.test.mjs +171 -0
  247. package/skills/grokify/evals/run-capability-evals.mjs +545 -0
  248. package/skills/orchestrator/SKILL.md +32 -22
  249. package/skills/orchestrator-refresh/SKILL.md +12 -8
  250. package/skills/pr-converge/CLAUDE.md +1 -1
  251. package/skills/pr-converge/SKILL.md +34 -13
  252. package/skills/pr-converge/reference/convergence-gates.md +42 -15
  253. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  254. package/skills/pr-converge/reference/ground-rules.md +1 -1
  255. package/skills/pr-converge/reference/per-tick.md +130 -42
  256. package/skills/pr-converge/reference/state-schema.md +10 -0
  257. package/skills/pr-converge/scripts/CLAUDE.md +2 -0
  258. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
  259. package/skills/pr-converge/scripts/check_convergence.py +605 -29
  260. package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
  261. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
  262. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
  263. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
  264. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
  265. package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
  266. package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
  267. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
  268. package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
  269. package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
  270. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
  271. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
  272. package/skills/pr-converge/test_step5_host_branch.py +106 -0
  273. package/skills/pr-fix-protocol/SKILL.md +65 -0
  274. package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
  275. package/skills/reviewer-gates/SKILL.md +7 -5
  276. package/skills/skill-builder/references/skill-modularity.md +1 -1
  277. package/skills/team-advisor/SKILL.md +16 -12
  278. package/system-prompts/software-engineer.xml +7 -6
  279. package/hooks/validators/test_verify_paths.py +0 -32
  280. package/hooks/validators/verify_paths.py +0 -57
@@ -0,0 +1,63 @@
1
+ """Detect a write that restores a tracked-but-absent production file.
2
+
3
+ ::
4
+
5
+ remove orders.py (git-tracked) -> Write orders.py -> restore (allow)
6
+ Write brand_new.py (untracked, absent) -> new code (gate)
7
+
8
+ A full rewrite happens as remove-then-Write, so the file is absent on disk
9
+ between the two steps. Restoring committed code is not new production code, so
10
+ the gate exempts a Write whose target is absent yet tracked in git.
11
+ """
12
+
13
+ import subprocess
14
+ from pathlib import Path
15
+
16
+ from tdd_enforcer_parts.config.tdd_enforcer_constants import (
17
+ GIT_EXECUTABLE_NAME,
18
+ GIT_LS_FILES_SUBCOMMAND,
19
+ GIT_LS_FILES_TIMEOUT_SECONDS,
20
+ GIT_PATHSPEC_SEPARATOR,
21
+ )
22
+
23
+
24
+ def _git_ls_files_command(file_name: str) -> list[str]:
25
+ return [GIT_EXECUTABLE_NAME, GIT_LS_FILES_SUBCOMMAND, GIT_PATHSPEC_SEPARATOR, file_name]
26
+
27
+
28
+ def _git_tracks_path(path: Path) -> bool:
29
+ parent_directory = path.parent
30
+ if not parent_directory.is_dir():
31
+ return False
32
+ try:
33
+ completed = subprocess.run(
34
+ _git_ls_files_command(path.name),
35
+ cwd=str(parent_directory),
36
+ capture_output=True,
37
+ text=True,
38
+ check=False,
39
+ timeout=GIT_LS_FILES_TIMEOUT_SECONDS,
40
+ )
41
+ except (OSError, subprocess.SubprocessError):
42
+ return False
43
+ return completed.returncode == 0 and bool(completed.stdout.strip())
44
+
45
+
46
+ def is_absent_but_tracked(path: Path) -> bool:
47
+ """Return whether a write target is absent on disk yet tracked in git.
48
+
49
+ ::
50
+
51
+ path missing on disk + git ls-files finds it -> True (restore)
52
+ path present, or git does not track it -> False
53
+
54
+ Args:
55
+ path: The write target.
56
+
57
+ Returns:
58
+ True only when the path does not exist but git tracks it, marking the
59
+ write a restore of committed code rather than new production code.
60
+ """
61
+ if path.exists():
62
+ return False
63
+ return _git_tracks_path(path)
@@ -0,0 +1,119 @@
1
+ """Path and payload classification for the TDD-enforcer gate.
2
+
3
+ Decides which write targets the gate skips silently (docs, tests, files inside a
4
+ ``.claude`` tree) and pulls the written text out of a Write, Edit, or MultiEdit
5
+ payload so the gate can inspect it.
6
+ """
7
+
8
+ from tdd_enforcer_parts.config.tdd_enforcer_constants import (
9
+ ALL_DIRECTORY_SKIP_COMPONENTS,
10
+ ALL_DOTCLAUDE_PATH_SEGMENTS,
11
+ ALL_PRODUCTION_EXTENSIONS,
12
+ ALL_SKIP_EXTENSIONS,
13
+ ALL_SKIP_NAME_PATTERNS,
14
+ NEWLINE_JOIN_SEPARATOR,
15
+ )
16
+
17
+
18
+ def production_extensions() -> frozenset[str]:
19
+ """Return the source-file extensions the gate treats as production code."""
20
+ return ALL_PRODUCTION_EXTENSIONS
21
+
22
+
23
+ def skip_extensions() -> frozenset[str]:
24
+ """Return the config and documentation extensions the gate skips."""
25
+ return ALL_SKIP_EXTENSIONS
26
+
27
+
28
+ def _skip_patterns() -> frozenset[str]:
29
+ return ALL_SKIP_NAME_PATTERNS
30
+
31
+
32
+ def _dotclaude_path_segments() -> frozenset[str]:
33
+ return ALL_DOTCLAUDE_PATH_SEGMENTS
34
+
35
+
36
+ def _directory_skip_components() -> frozenset[str]:
37
+ return ALL_DIRECTORY_SKIP_COMPONENTS
38
+
39
+
40
+ def _is_inside_dotclaude_segment(file_path_string: str) -> bool:
41
+ """Return whether any path segment is exactly a ``.claude`` directory.
42
+
43
+ Args:
44
+ file_path_string: The write target path, in either separator style.
45
+
46
+ Returns:
47
+ True when a ``.claude`` directory sits on the path, so the gate leaves
48
+ agent-config edits alone.
49
+ """
50
+ normalized_path = file_path_string.replace("\\", "/")
51
+ dotclaude_segments = _dotclaude_path_segments()
52
+ for each_segment in normalized_path.split("/"):
53
+ if each_segment and each_segment in dotclaude_segments:
54
+ return True
55
+ return False
56
+
57
+
58
+ def _path_has_skip_directory_component(path_with_forward_slashes: str) -> bool:
59
+ all_components = [each_part for each_part in path_with_forward_slashes.split("/") if each_part]
60
+ directory_components = all_components[:-1]
61
+ skip_directory_components = _directory_skip_components()
62
+ return any(each in skip_directory_components for each in directory_components)
63
+
64
+
65
+ def _skip_pattern_matches(pattern: str, name_lower: str, path_with_forward_slashes: str) -> bool:
66
+ if pattern.endswith("/"):
67
+ return pattern in path_with_forward_slashes
68
+ return pattern in name_lower
69
+
70
+
71
+ def _name_or_path_matches_skip_pattern(name_lower: str, path_with_forward_slashes: str) -> bool:
72
+ for each_pattern in _skip_patterns():
73
+ if _skip_pattern_matches(each_pattern, name_lower, path_with_forward_slashes):
74
+ return True
75
+ return False
76
+
77
+
78
+ def _matches_any_skip_pattern(name_lower: str, path_with_forward_slashes: str) -> bool:
79
+ """Return whether a path names a test, fixture, mock, or stub file.
80
+
81
+ Args:
82
+ name_lower: The lower-cased file name.
83
+ path_with_forward_slashes: The lower-cased path with forward slashes.
84
+
85
+ Returns:
86
+ True when a skip directory component or a skip name pattern matches.
87
+ """
88
+ if _path_has_skip_directory_component(path_with_forward_slashes):
89
+ return True
90
+ return _name_or_path_matches_skip_pattern(name_lower, path_with_forward_slashes)
91
+
92
+
93
+ def _multiedit_new_strings(all_edits: list) -> list[str]:
94
+ all_new_strings: list[str] = []
95
+ for each_edit in all_edits:
96
+ if isinstance(each_edit, dict):
97
+ all_new_strings.append(each_edit.get("new_string", "") or "")
98
+ return all_new_strings
99
+
100
+
101
+ def _extract_written_content(tool_name: str, tool_input: dict) -> str:
102
+ """Return the text a Write, Edit, or MultiEdit payload would introduce.
103
+
104
+ Args:
105
+ tool_name: The intercepted tool name.
106
+ tool_input: The intercepted tool's input payload.
107
+
108
+ Returns:
109
+ The written content for Write, the replacement for Edit, the joined
110
+ replacements for MultiEdit, or an empty string for any other tool.
111
+ """
112
+ if tool_name == "Write":
113
+ return tool_input.get("content", "") or ""
114
+ if tool_name == "Edit":
115
+ return tool_input.get("new_string", "") or ""
116
+ if tool_name == "MultiEdit":
117
+ all_edits = tool_input.get("edits", []) or []
118
+ return NEWLINE_JOIN_SEPARATOR.join(_multiedit_new_strings(all_edits))
119
+ return ""
@@ -0,0 +1,10 @@
1
+ """Put the blocking and hooks directories on sys.path for the parts tests."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ _blocking_directory = str(Path(__file__).resolve().parents[2])
7
+ _hooks_directory = str(Path(__file__).resolve().parents[3])
8
+ for each_bootstrap_directory in (_blocking_directory, _hooks_directory):
9
+ if each_bootstrap_directory not in sys.path:
10
+ sys.path.insert(0, each_bootstrap_directory)
@@ -0,0 +1,31 @@
1
+ """Behavioral tests for the candidate_paths parts module."""
2
+
3
+ from pathlib import Path
4
+
5
+ from tdd_enforcer_parts import candidate_paths
6
+
7
+
8
+ def test_candidate_paths_offers_flat_stem_candidates_first(tmp_path: Path) -> None:
9
+ (tmp_path / ".git").mkdir()
10
+ all_candidates = candidate_paths.candidate_test_paths_for(tmp_path / "orders.py")
11
+ assert all_candidates[0] == tmp_path / "test_orders.py"
12
+ assert all_candidates[1] == tmp_path / "orders_test.py"
13
+
14
+
15
+ def test_candidate_paths_returns_empty_for_unknown_extension(tmp_path: Path) -> None:
16
+ assert candidate_paths.candidate_test_paths_for(tmp_path / "data.rs") == []
17
+
18
+
19
+ def test_candidate_paths_offers_javascript_test_siblings(tmp_path: Path) -> None:
20
+ all_candidates = candidate_paths.candidate_test_paths_for(tmp_path / "Button.tsx")
21
+ assert tmp_path / "Button.test.tsx" in all_candidates
22
+
23
+
24
+ def test_ancestor_tests_directories_finds_sibling_tests(tmp_path: Path) -> None:
25
+ (tmp_path / ".git").mkdir()
26
+ package = tmp_path / "pkg"
27
+ package.mkdir()
28
+ (package / "tests").mkdir()
29
+ all_pairs = candidate_paths._ancestor_tests_directories(package)
30
+ all_tests_directories = [each_tests_directory for _, each_tests_directory in all_pairs]
31
+ assert package / "tests" in all_tests_directories
@@ -0,0 +1,30 @@
1
+ """Behavioral tests for the content_analysis parts module."""
2
+
3
+ from tdd_enforcer_parts import content_analysis
4
+
5
+
6
+ def test_constants_only_accepts_module_docstring_and_assignment() -> None:
7
+ content = '"""doc."""\nMAX_ORDERS = 3\n'
8
+ assert content_analysis._is_constants_only_python_content(content) is True
9
+
10
+
11
+ def test_constants_only_rejects_function_definition() -> None:
12
+ assert content_analysis._is_constants_only_python_content("def f():\n return 1\n") is False
13
+
14
+
15
+ def test_post_edit_import_only_allows_import_removal() -> None:
16
+ existing = "import os\n\ndef run(): return 1\n"
17
+ tool_input = {"old_string": "import os\n", "new_string": ""}
18
+ assert content_analysis._is_post_edit_import_only(existing, "Edit", tool_input) is True
19
+
20
+
21
+ def test_post_edit_import_only_blocks_import_swap() -> None:
22
+ existing = "import os\n\ndef run(): return 1\n"
23
+ tool_input = {"old_string": "import os", "new_string": "import sys"}
24
+ assert content_analysis._is_post_edit_import_only(existing, "Edit", tool_input) is False
25
+
26
+
27
+ def test_post_edit_constants_only_allows_constant_value_change() -> None:
28
+ existing = '"""doc."""\nMAX_ORDERS = 3\n'
29
+ tool_input = {"old_string": "MAX_ORDERS = 3", "new_string": "MAX_ORDERS = 5"}
30
+ assert content_analysis._is_post_edit_constants_only(existing, "Edit", tool_input) is True
@@ -0,0 +1,34 @@
1
+ """Behavioral tests for the decisions parts module."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ import pytest
7
+ from tdd_enforcer_parts import decisions
8
+
9
+
10
+ def test_build_deny_reason_names_candidates_and_production_path() -> None:
11
+ reason = decisions.build_deny_reason(Path("pkg/orders.py"), [Path("pkg/test_orders.py")])
12
+ assert "orders.py" in reason
13
+ assert "test_orders.py" in reason
14
+ assert "propose" in reason.lower() or "enhancement" in reason.lower()
15
+
16
+
17
+ def test_build_deny_reason_points_exemption_guidance_at_entry_hook() -> None:
18
+ reason = decisions.build_deny_reason(Path("pkg/orders.py"), [])
19
+ assert "tdd_enforcer.py" in reason
20
+ assert "decisions.py" not in reason
21
+
22
+
23
+ def test_emit_allow_writes_allow_decision(capsys: pytest.CaptureFixture[str]) -> None:
24
+ decisions.emit_allow()
25
+ parsed = json.loads(capsys.readouterr().out)
26
+ assert parsed["hookSpecificOutput"]["permissionDecision"] == "allow"
27
+
28
+
29
+ def test_emit_deny_writes_deny_and_system_message(capsys: pytest.CaptureFixture[str]) -> None:
30
+ decisions.emit_deny("blocked because no fresh test")
31
+ parsed = json.loads(capsys.readouterr().out)
32
+ assert parsed["hookSpecificOutput"]["permissionDecision"] == "deny"
33
+ assert parsed["suppressOutput"] is True
34
+ assert isinstance(parsed["systemMessage"], str)
@@ -0,0 +1,28 @@
1
+ """Behavioral tests for the freshness parts module."""
2
+
3
+ import os
4
+ import time
5
+ from pathlib import Path
6
+
7
+ from tdd_enforcer_parts import freshness
8
+
9
+
10
+ def test_has_fresh_test_true_for_recent_test_file(tmp_path: Path) -> None:
11
+ test_file = tmp_path / "test_orders.py"
12
+ test_file.write_text("def test_x(): pass\n")
13
+ assert freshness.has_fresh_test([test_file], freshness._freshness_seconds()) is True
14
+
15
+
16
+ def test_has_fresh_test_false_for_stale_test_file(tmp_path: Path) -> None:
17
+ test_file = tmp_path / "test_orders.py"
18
+ test_file.write_text("def test_x(): pass\n")
19
+ window = freshness._freshness_seconds()
20
+ stale_timestamp = time.time() - window - 60
21
+ os.utime(test_file, (stale_timestamp, stale_timestamp))
22
+ assert freshness.has_fresh_test([test_file], window) is False
23
+
24
+
25
+ def test_has_fresh_test_false_when_no_test_evidence(tmp_path: Path) -> None:
26
+ test_file = tmp_path / "test_orders.py"
27
+ test_file.write_text("x = 1\n")
28
+ assert freshness.has_fresh_test([test_file], freshness._freshness_seconds()) is False
@@ -0,0 +1,48 @@
1
+ """Behavioral tests for the git_tracking parts module."""
2
+
3
+ import os
4
+ import subprocess
5
+ from pathlib import Path
6
+
7
+ from tdd_enforcer_parts import git_tracking
8
+
9
+
10
+ def _git(repository_root: Path, *arguments: str) -> None:
11
+ subprocess.run(
12
+ ["git", *arguments],
13
+ cwd=str(repository_root),
14
+ check=True,
15
+ capture_output=True,
16
+ env={**os.environ, "GIT_CONFIG_GLOBAL": os.devnull, "GIT_CONFIG_SYSTEM": os.devnull},
17
+ )
18
+
19
+
20
+ def _init_repository(repository_root: Path) -> None:
21
+ _git(repository_root, "init", "-q")
22
+ _git(repository_root, "config", "user.email", "test@example.com")
23
+ _git(repository_root, "config", "user.name", "Test")
24
+ _git(repository_root, "config", "commit.gpgsign", "false")
25
+
26
+
27
+ def test_absent_but_tracked_true_after_committed_file_removed(tmp_path: Path) -> None:
28
+ _init_repository(tmp_path)
29
+ tracked = tmp_path / "service.py"
30
+ tracked.write_text("def serve(): return 1\n")
31
+ _git(tmp_path, "add", "-A")
32
+ _git(tmp_path, "commit", "-q", "-m", "init")
33
+ tracked.unlink()
34
+ assert git_tracking.is_absent_but_tracked(tracked) is True
35
+
36
+
37
+ def test_absent_but_tracked_false_for_untracked_absent_file(tmp_path: Path) -> None:
38
+ _init_repository(tmp_path)
39
+ assert git_tracking.is_absent_but_tracked(tmp_path / "ghost.py") is False
40
+
41
+
42
+ def test_absent_but_tracked_false_when_file_present(tmp_path: Path) -> None:
43
+ _init_repository(tmp_path)
44
+ present = tmp_path / "present.py"
45
+ present.write_text("def here(): return 1\n")
46
+ _git(tmp_path, "add", "-A")
47
+ _git(tmp_path, "commit", "-q", "-m", "init")
48
+ assert git_tracking.is_absent_but_tracked(present) is False
@@ -0,0 +1,36 @@
1
+ """Behavioral tests for the path_classification parts module."""
2
+
3
+ from tdd_enforcer_parts import path_classification
4
+
5
+
6
+ def test_production_extensions_includes_python_and_typescript() -> None:
7
+ extensions = path_classification.production_extensions()
8
+ assert ".py" in extensions
9
+ assert ".tsx" in extensions
10
+
11
+
12
+ def test_skip_extensions_includes_markdown_and_json() -> None:
13
+ extensions = path_classification.skip_extensions()
14
+ assert ".md" in extensions
15
+ assert ".json" in extensions
16
+
17
+
18
+ def test_is_inside_dotclaude_segment_matches_exact_segment_only() -> None:
19
+ assert path_classification._is_inside_dotclaude_segment("/home/user/.claude/agent.py") is True
20
+ assert path_classification._is_inside_dotclaude_segment("/src/my.claude.helpers.py") is False
21
+
22
+
23
+ def test_matches_any_skip_pattern_flags_test_file_names() -> None:
24
+ assert (
25
+ path_classification._matches_any_skip_pattern("test_orders.py", "pkg/test_orders.py")
26
+ is True
27
+ )
28
+ assert path_classification._matches_any_skip_pattern("orders.py", "pkg/orders.py") is False
29
+
30
+
31
+ def test_extract_written_content_reads_write_and_multiedit() -> None:
32
+ assert path_classification._extract_written_content("Write", {"content": "x"}) == "x"
33
+ joined = path_classification._extract_written_content(
34
+ "MultiEdit", {"edits": [{"new_string": "a"}, {"new_string": "b"}]}
35
+ )
36
+ assert joined == "a\nb"
@@ -0,0 +1,105 @@
1
+ """Tests for the session-scratchpad exemption wired into code_rules_enforcer.main."""
2
+
3
+ import importlib.util
4
+ import io
5
+ import json
6
+ import os
7
+ import sys
8
+ import tempfile
9
+ from pathlib import Path
10
+ from types import ModuleType
11
+
12
+ import pytest
13
+
14
+ ENFORCER_PATH = Path(__file__).parent / "code_rules_enforcer.py"
15
+ FIXED_USER_ID = 6070
16
+ WORKING_DIRECTORY = "/home/user/project"
17
+ SESSION_ID = "enforcer-session-654"
18
+ VIOLATING_CONTENT = "def compute():\n data = 42\n return data\n"
19
+
20
+
21
+ def _load_enforcer() -> ModuleType:
22
+ module_spec = importlib.util.spec_from_file_location(
23
+ "code_rules_enforcer_scratchpad_under_test", ENFORCER_PATH
24
+ )
25
+ assert module_spec is not None and module_spec.loader is not None
26
+ loaded_module = importlib.util.module_from_spec(module_spec)
27
+ module_spec.loader.exec_module(loaded_module)
28
+ return loaded_module
29
+
30
+
31
+ _ENFORCER = _load_enforcer()
32
+
33
+
34
+ def _install_scratchpad_signals(monkeypatch: pytest.MonkeyPatch, temporary_root: Path) -> Path:
35
+ monkeypatch.setattr(os, "getuid", lambda: FIXED_USER_ID, raising=False)
36
+ monkeypatch.setattr(tempfile, "gettempdir", lambda: str(temporary_root))
37
+ mangled_working_directory = WORKING_DIRECTORY.replace("/", "-")
38
+ scratchpad_directory = (
39
+ temporary_root
40
+ / f"claude-{FIXED_USER_ID}"
41
+ / mangled_working_directory
42
+ / SESSION_ID
43
+ / "scratchpad"
44
+ )
45
+ scratchpad_directory.mkdir(parents=True)
46
+ return scratchpad_directory
47
+
48
+
49
+ def _write_payload(target: Path) -> dict[str, object]:
50
+ return {
51
+ "tool_name": "Write",
52
+ "cwd": WORKING_DIRECTORY,
53
+ "session_id": SESSION_ID,
54
+ "tool_input": {"file_path": str(target), "content": VIOLATING_CONTENT},
55
+ }
56
+
57
+
58
+ def _run_main(
59
+ monkeypatch: pytest.MonkeyPatch, payload: dict[str, object]
60
+ ) -> tuple[int | None, str]:
61
+ monkeypatch.setattr(sys, "stdin", io.StringIO(json.dumps(payload)))
62
+ captured_stdout = io.StringIO()
63
+ monkeypatch.setattr(sys, "stdout", captured_stdout)
64
+ exit_code: int | None = None
65
+ try:
66
+ _ENFORCER.main([])
67
+ except SystemExit as raised_exit:
68
+ raw_code = raised_exit.code
69
+ exit_code = raw_code if isinstance(raw_code, int) else None
70
+ return exit_code, captured_stdout.getvalue()
71
+
72
+
73
+ def _decision_from(stdout_text: str) -> str | None:
74
+ if not stdout_text.strip():
75
+ return None
76
+ parsed = json.loads(stdout_text)
77
+ return parsed.get("hookSpecificOutput", {}).get("permissionDecision")
78
+
79
+
80
+ def test_scratchpad_write_is_exempt_from_code_rules(
81
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
82
+ ) -> None:
83
+ scratchpad_directory = _install_scratchpad_signals(monkeypatch, tmp_path)
84
+ monkeypatch.setenv("CLAUDE_CODE_RULES_DISABLE_EPHEMERAL_EXEMPT", "1")
85
+ throwaway_script = scratchpad_directory / "one_off_probe.py"
86
+
87
+ exit_code, stdout_text = _run_main(monkeypatch, _write_payload(throwaway_script))
88
+
89
+ assert exit_code == 0
90
+ assert _decision_from(stdout_text) != "deny"
91
+
92
+
93
+ def test_identical_non_scratchpad_write_is_still_blocked(
94
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
95
+ ) -> None:
96
+ _install_scratchpad_signals(monkeypatch, tmp_path)
97
+ monkeypatch.setenv("CLAUDE_CODE_RULES_DISABLE_EPHEMERAL_EXEMPT", "1")
98
+ outside_directory = tmp_path / "project" / "orders"
99
+ outside_directory.mkdir(parents=True)
100
+ production_module = outside_directory / "one_off_probe.py"
101
+
102
+ exit_code, stdout_text = _run_main(monkeypatch, _write_payload(production_module))
103
+
104
+ assert exit_code == 0
105
+ assert _decision_from(stdout_text) == "deny"