claude-dev-env 1.95.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 (240) 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/tests/conftest.py +10 -0
  117. package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
  118. package/hooks/blocking/tests/test_repository_exemption.py +105 -0
  119. package/hooks/blocking/tests/test_repository_resolution.py +108 -0
  120. package/hooks/diagnostic/hook_log_extractor.py +12 -10
  121. package/hooks/git-hooks/post_commit.py +3 -4
  122. package/hooks/hooks_constants/CLAUDE.md +2 -2
  123. package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
  124. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
  125. package/hooks/hooks_constants/local_identity.py +59 -8
  126. package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
  127. package/hooks/hooks_constants/test_local_identity.py +105 -3
  128. package/hooks/pyproject.toml +13 -36
  129. package/hooks/session/plugin_data_dir_cleanup.py +0 -1
  130. package/hooks/validation/mypy_validator.py +2 -2
  131. package/hooks/validators/health_check.py +1 -0
  132. package/hooks/validators/mypy_integration.py +2 -0
  133. package/hooks/validators/ruff_integration.py +3 -0
  134. package/hooks/workflow/auto_formatter.py +5 -4
  135. package/package.json +1 -1
  136. package/scripts/CLAUDE.md +4 -0
  137. package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
  138. package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
  139. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
  140. package/scripts/dev_env_scripts_constants/timing.py +7 -1
  141. package/scripts/grok_headless_runner.py +294 -0
  142. package/scripts/grok_worker_preflight.py +410 -0
  143. package/scripts/invoke_code_review.py +463 -0
  144. package/scripts/resolve_worker_spawn.py +619 -0
  145. package/scripts/spawn_grok_batch.py +672 -0
  146. package/scripts/test_grok_headless_runner.py +626 -0
  147. package/scripts/test_grok_worker_preflight.py +1054 -0
  148. package/scripts/test_invoke_code_review.py +672 -0
  149. package/scripts/test_resolve_worker_spawn.py +1014 -0
  150. package/scripts/test_spawn_grok_batch.py +1017 -0
  151. package/skills/CLAUDE.md +5 -3
  152. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
  153. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
  154. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
  155. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
  156. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
  157. package/skills/autoconverge/CLAUDE.md +3 -3
  158. package/skills/autoconverge/SKILL.md +9 -3
  159. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  160. package/skills/autoconverge/reference/convergence.md +33 -11
  161. package/skills/autoconverge/reference/stop-conditions.md +16 -5
  162. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  163. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
  164. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
  165. package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
  166. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
  167. package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
  168. package/skills/autoconverge/workflow/converge.mjs +200 -16
  169. package/skills/bugteam/CLAUDE.md +2 -2
  170. package/skills/bugteam/CONSTRAINTS.md +3 -2
  171. package/skills/bugteam/PROMPTS.md +7 -6
  172. package/skills/bugteam/SKILL.md +18 -13
  173. package/skills/bugteam/reference/audit-and-teammates.md +215 -35
  174. package/skills/bugteam/reference/design-rationale.md +1 -1
  175. package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
  176. package/skills/bugteam/reference/team-setup.md +8 -2
  177. package/skills/codex-review/CLAUDE.md +46 -0
  178. package/skills/codex-review/SKILL.md +181 -0
  179. package/skills/codex-review/reference/CLAUDE.md +15 -0
  180. package/skills/codex-review/reference/cli-contract.md +253 -0
  181. package/skills/codex-review/reference/loop-integration.md +118 -0
  182. package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
  183. package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
  184. package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
  185. package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
  186. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
  187. package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
  188. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
  189. package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
  190. package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
  191. package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
  192. package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
  193. package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
  194. package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
  195. package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
  196. package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
  197. package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
  198. package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
  199. package/skills/codex-review/scripts/run_codex_review.py +415 -0
  200. package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
  201. package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
  202. package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
  203. package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
  204. package/skills/codex-review/test_skill_scaffold.py +192 -0
  205. package/skills/grok-spawn/CLAUDE.md +28 -0
  206. package/skills/grok-spawn/SKILL.md +226 -0
  207. package/skills/grok-spawn/reference/flag-profiles.md +132 -0
  208. package/skills/grok-spawn/reference/worker-briefs.md +152 -0
  209. package/skills/grokify/SKILL.md +9 -1
  210. package/skills/grokify/capability-claims.test.mjs +28 -0
  211. package/skills/grokify/evals/README.md +72 -0
  212. package/skills/grokify/evals/parse-payload.test.mjs +171 -0
  213. package/skills/grokify/evals/run-capability-evals.mjs +545 -0
  214. package/skills/orchestrator/SKILL.md +15 -11
  215. package/skills/orchestrator-refresh/SKILL.md +5 -5
  216. package/skills/pr-converge/SKILL.md +34 -13
  217. package/skills/pr-converge/reference/convergence-gates.md +42 -15
  218. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  219. package/skills/pr-converge/reference/ground-rules.md +1 -1
  220. package/skills/pr-converge/reference/per-tick.md +130 -42
  221. package/skills/pr-converge/reference/state-schema.md +10 -0
  222. package/skills/pr-converge/scripts/CLAUDE.md +2 -0
  223. package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
  224. package/skills/pr-converge/scripts/check_convergence.py +605 -29
  225. package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
  226. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
  227. package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
  228. package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
  229. package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
  230. package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
  231. package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
  232. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
  233. package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
  234. package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
  235. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
  236. package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
  237. package/skills/pr-converge/test_step5_host_branch.py +106 -0
  238. package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
  239. package/skills/reviewer-gates/SKILL.md +7 -5
  240. package/skills/team-advisor/SKILL.md +7 -7
@@ -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,154 @@
1
+ """End-to-end tests for the inventory/orphan pending-intent deadlock resolution.
2
+
3
+ Each test drives the two real hook binaries through their production ``main()``
4
+ stdin path over a temp directory tree whose ``CLAUDE.md`` inventory names two
5
+ sibling files. A shared ``session_id`` and an isolated ``TMPDIR`` make the two
6
+ hooks read one shared intent-records file, so a deny by one hook opens the gate
7
+ for the sibling's second write — in either add order.
8
+ """
9
+
10
+ import json
11
+ import os
12
+ import subprocess
13
+ import sys
14
+ from pathlib import Path
15
+
16
+ BLOCKING_DIRECTORY = Path(__file__).resolve().parent
17
+
18
+ INVENTORY_HOOK = "package_inventory_stale_blocker.py"
19
+ ORPHAN_HOOK = "claude_md_orphan_file_blocker.py"
20
+
21
+ SESSION_ID = "deadlock-session"
22
+
23
+ INVENTORY_TABLE_HEADER = "# pkg\n\n| File | Purpose |\n|---|---|\n"
24
+ ALPHA_ROW = "| `alpha.py` | Does A |\n"
25
+ BETA_ROW = "| `beta.py` | Does B |\n"
26
+ GAMMA_ROW = "| `gamma.py` | Does G |\n"
27
+ EPSILON_ROW = "| `epsilon.py` | Does E |\n"
28
+
29
+ BASE_INVENTORY = INVENTORY_TABLE_HEADER + ALPHA_ROW + BETA_ROW
30
+ INVENTORY_WITH_GAMMA = BASE_INVENTORY + GAMMA_ROW
31
+ INVENTORY_WITH_EPSILON = BASE_INVENTORY + EPSILON_ROW
32
+
33
+ MODULE_BODY = "x = 1\n"
34
+
35
+
36
+ def _package_directory(tmp_path: Path) -> Path:
37
+ """Return a package directory holding a CLAUDE.md table naming two siblings."""
38
+ package_directory = tmp_path / "pkg"
39
+ package_directory.mkdir()
40
+ (package_directory / "CLAUDE.md").write_text(BASE_INVENTORY, encoding="utf-8")
41
+ (package_directory / "alpha.py").write_text(MODULE_BODY, encoding="utf-8")
42
+ (package_directory / "beta.py").write_text(MODULE_BODY, encoding="utf-8")
43
+ return package_directory
44
+
45
+
46
+ def _run_hook(
47
+ hook_filename: str, tool_name: str, tool_input: dict, temp_directory: Path
48
+ ) -> subprocess.CompletedProcess:
49
+ """Run one hook binary over a payload, sharing an isolated intent-records store."""
50
+ payload = json.dumps(
51
+ {"tool_name": tool_name, "tool_input": tool_input, "session_id": SESSION_ID}
52
+ )
53
+ hook_environment = dict(os.environ)
54
+ hook_environment["TMPDIR"] = str(temp_directory)
55
+ return subprocess.run(
56
+ [sys.executable, str(BLOCKING_DIRECTORY / hook_filename)],
57
+ input=payload,
58
+ capture_output=True,
59
+ text=True,
60
+ check=False,
61
+ env=hook_environment,
62
+ )
63
+
64
+
65
+ def _decision(completed: subprocess.CompletedProcess) -> str | None:
66
+ """Return the permission decision a hook emitted, or None when it allowed."""
67
+ if not completed.stdout.strip():
68
+ return None
69
+ return json.loads(completed.stdout)["hookSpecificOutput"]["permissionDecision"]
70
+
71
+
72
+ def _reason(completed: subprocess.CompletedProcess) -> str:
73
+ """Return the deny reason a hook emitted."""
74
+ return json.loads(completed.stdout)["hookSpecificOutput"]["permissionDecisionReason"]
75
+
76
+
77
+ def test_file_first_order_resolves_after_one_deny(tmp_path: Path) -> None:
78
+ package_directory = _package_directory(tmp_path)
79
+ new_file = package_directory / "gamma.py"
80
+ claude_md = package_directory / "CLAUDE.md"
81
+
82
+ first_write = _run_hook(
83
+ INVENTORY_HOOK, "Write", {"file_path": str(new_file), "content": MODULE_BODY}, tmp_path
84
+ )
85
+ assert _decision(first_write) == "deny"
86
+ assert "gamma.py" in _reason(first_write)
87
+ assert "recorded" in _reason(first_write)
88
+
89
+ row_write = _run_hook(
90
+ ORPHAN_HOOK,
91
+ "Write",
92
+ {"file_path": str(claude_md), "content": INVENTORY_WITH_GAMMA},
93
+ tmp_path,
94
+ )
95
+ assert _decision(row_write) is None
96
+
97
+ claude_md.write_text(INVENTORY_WITH_GAMMA, encoding="utf-8")
98
+ retry_write = _run_hook(
99
+ INVENTORY_HOOK, "Write", {"file_path": str(new_file), "content": MODULE_BODY}, tmp_path
100
+ )
101
+ assert _decision(retry_write) is None
102
+
103
+
104
+ def test_row_first_order_resolves_after_one_deny(tmp_path: Path) -> None:
105
+ package_directory = _package_directory(tmp_path)
106
+ new_file = package_directory / "gamma.py"
107
+ claude_md = package_directory / "CLAUDE.md"
108
+
109
+ first_row = _run_hook(
110
+ ORPHAN_HOOK,
111
+ "Write",
112
+ {"file_path": str(claude_md), "content": INVENTORY_WITH_GAMMA},
113
+ tmp_path,
114
+ )
115
+ assert _decision(first_row) == "deny"
116
+ assert "gamma.py" in _reason(first_row)
117
+ assert "recorded" in _reason(first_row)
118
+
119
+ file_write = _run_hook(
120
+ INVENTORY_HOOK, "Write", {"file_path": str(new_file), "content": MODULE_BODY}, tmp_path
121
+ )
122
+ assert _decision(file_write) is None
123
+
124
+ new_file.write_text(MODULE_BODY, encoding="utf-8")
125
+ retry_row = _run_hook(
126
+ ORPHAN_HOOK,
127
+ "Write",
128
+ {"file_path": str(claude_md), "content": INVENTORY_WITH_GAMMA},
129
+ tmp_path,
130
+ )
131
+ assert _decision(retry_row) is None
132
+
133
+
134
+ def test_new_file_with_no_row_attempt_stays_blocked(tmp_path: Path) -> None:
135
+ package_directory = _package_directory(tmp_path)
136
+ new_file = package_directory / "delta.py"
137
+ file_write = _run_hook(
138
+ INVENTORY_HOOK, "Write", {"file_path": str(new_file), "content": MODULE_BODY}, tmp_path
139
+ )
140
+ assert _decision(file_write) == "deny"
141
+ assert "delta.py" in _reason(file_write)
142
+
143
+
144
+ def test_row_naming_uncreated_file_stays_blocked(tmp_path: Path) -> None:
145
+ package_directory = _package_directory(tmp_path)
146
+ claude_md = package_directory / "CLAUDE.md"
147
+ row_write = _run_hook(
148
+ ORPHAN_HOOK,
149
+ "Write",
150
+ {"file_path": str(claude_md), "content": INVENTORY_WITH_EPSILON},
151
+ tmp_path,
152
+ )
153
+ assert _decision(row_write) == "deny"
154
+ assert "epsilon.py" in _reason(row_write)