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,179 @@
1
+ """Resolve a CLAUDE.md's scan root and find the referenced files absent from it.
2
+
3
+ The scan root is the CLAUDE.md directory's parent, covering the directory, its
4
+ subdirectories, and its siblings. A bounded walk collects the file basenames;
5
+ when the budget truncates the walk, a direct ``rglob`` probe resolves any name
6
+ the partial set missed, so a truncated slice never reports a present file as
7
+ missing.
8
+ """
9
+
10
+ from pathlib import Path
11
+
12
+ from claude_md_orphan_file_blocker_parts.references import (
13
+ find_referenced_filenames,
14
+ find_run_command_filenames,
15
+ )
16
+
17
+ from hooks_constants.claude_md_orphan_file_blocker_constants import (
18
+ ALL_NOISE_DIRECTORY_NAMES,
19
+ MAX_ORPHAN_FILE_ISSUES,
20
+ MAX_SUBTREE_FILES_SCANNED,
21
+ )
22
+
23
+
24
+ def _resolve_scan_root(claude_md_directory: Path) -> Path:
25
+ """Return the directory whose subtree bounds the filename existence search.
26
+
27
+ Args:
28
+ claude_md_directory: The directory that holds the target CLAUDE.md.
29
+
30
+ Returns:
31
+ The CLAUDE.md directory's parent, or the directory itself when it has no
32
+ distinct parent.
33
+ """
34
+ parent_directory = claude_md_directory.parent
35
+ if parent_directory == claude_md_directory:
36
+ return claude_md_directory
37
+ return parent_directory
38
+
39
+
40
+ class _SubtreeScan:
41
+ """The basenames a bounded subtree walk collected and whether it ran complete.
42
+
43
+ Attributes:
44
+ all_basenames: Each file basename the walk reached.
45
+ was_scan_complete: True when the walk visited the whole subtree within the
46
+ budget, so ``all_basenames`` is authoritative.
47
+ """
48
+
49
+ def __init__(self, all_basenames: set[str], was_scan_complete: bool) -> None:
50
+ self.all_basenames = all_basenames
51
+ self.was_scan_complete = was_scan_complete
52
+
53
+
54
+ def _is_under_noise_directory(scan_root: Path, candidate_path: Path) -> bool:
55
+ """Return whether *candidate_path* lies inside a pruned noise directory.
56
+
57
+ Args:
58
+ scan_root: The directory the walk descends from.
59
+ candidate_path: A path the walk yielded under the scan root.
60
+
61
+ Returns:
62
+ True when any path segment below *scan_root* names a noise directory.
63
+ """
64
+ try:
65
+ relative_segments = candidate_path.relative_to(scan_root).parts
66
+ except ValueError:
67
+ relative_segments = candidate_path.parts
68
+ return any(each_segment in ALL_NOISE_DIRECTORY_NAMES for each_segment in relative_segments)
69
+
70
+
71
+ def _is_countable_file(candidate_path: Path) -> bool:
72
+ """Return whether *candidate_path* is a readable regular file.
73
+
74
+ Args:
75
+ candidate_path: A path the walk yielded.
76
+
77
+ Returns:
78
+ True when the path is a file; a per-entry stat error reads as False.
79
+ """
80
+ try:
81
+ return candidate_path.is_file()
82
+ except OSError:
83
+ return False
84
+
85
+
86
+ def _scan_subtree_basenames(scan_root: Path) -> _SubtreeScan:
87
+ """Return the bounded basename scan of *scan_root*, skipping unreadable entries.
88
+
89
+ Args:
90
+ scan_root: The directory whose subtree bounds the existence search.
91
+
92
+ Returns:
93
+ The collected basenames paired with the scan-completeness flag.
94
+ """
95
+ all_basenames: set[str] = set()
96
+ scanned_count = 0
97
+ for each_path in scan_root.rglob("*"):
98
+ if _is_under_noise_directory(scan_root, each_path):
99
+ continue
100
+ if not _is_countable_file(each_path):
101
+ continue
102
+ all_basenames.add(each_path.name)
103
+ scanned_count += 1
104
+ if scanned_count >= MAX_SUBTREE_FILES_SCANNED:
105
+ return _SubtreeScan(all_basenames, was_scan_complete=False)
106
+ return _SubtreeScan(all_basenames, was_scan_complete=True)
107
+
108
+
109
+ def _filename_exists_under(scan_root: Path, filename: str) -> bool:
110
+ """Return whether a file with basename *filename* exists anywhere under root.
111
+
112
+ Args:
113
+ scan_root: The directory whose subtree bounds the existence search.
114
+ filename: The bare basename to look for.
115
+
116
+ Returns:
117
+ True when at least one matching file is reachable under the scan root.
118
+ """
119
+ for each_match in scan_root.rglob(filename):
120
+ if _is_under_noise_directory(scan_root, each_match):
121
+ continue
122
+ if _is_countable_file(each_match):
123
+ return True
124
+ return False
125
+
126
+
127
+ def _present_referenced_filenames(all_referenced_filenames: list[str], scan_root: Path) -> set[str]:
128
+ """Return the referenced filenames that exist under the scan root.
129
+
130
+ Args:
131
+ all_referenced_filenames: The bare filenames a CLAUDE.md table names.
132
+ scan_root: The directory whose subtree bounds the existence search.
133
+
134
+ Returns:
135
+ The subset of *all_referenced_filenames* that resolve to an existing file.
136
+ """
137
+ subtree_scan = _scan_subtree_basenames(scan_root)
138
+ present_filenames: set[str] = set()
139
+ for each_filename in all_referenced_filenames:
140
+ if each_filename in subtree_scan.all_basenames:
141
+ present_filenames.add(each_filename)
142
+ continue
143
+ if subtree_scan.was_scan_complete:
144
+ continue
145
+ if _filename_exists_under(scan_root, each_filename):
146
+ present_filenames.add(each_filename)
147
+ return present_filenames
148
+
149
+
150
+ def find_missing_filenames(content: str, claude_md_directory: Path) -> list[str]:
151
+ """Return the referenced filenames absent from the CLAUDE.md's scan root.
152
+
153
+ Args:
154
+ content: The CLAUDE.md content being written.
155
+ claude_md_directory: The directory that holds the target CLAUDE.md.
156
+
157
+ Returns:
158
+ Each referenced filename with no matching file under the scan root.
159
+ """
160
+ referenced_filenames = find_referenced_filenames(content) + find_run_command_filenames(content)
161
+ if not referenced_filenames:
162
+ return []
163
+ scan_root = _resolve_scan_root(claude_md_directory)
164
+ try:
165
+ present_filenames = _present_referenced_filenames(referenced_filenames, scan_root)
166
+ except OSError:
167
+ return []
168
+ missing_filenames: list[str] = []
169
+ already_reported: set[str] = set()
170
+ for each_filename in referenced_filenames:
171
+ if each_filename in already_reported:
172
+ continue
173
+ if each_filename in present_filenames:
174
+ continue
175
+ already_reported.add(each_filename)
176
+ missing_filenames.append(each_filename)
177
+ if len(missing_filenames) >= MAX_ORPHAN_FILE_ISSUES:
178
+ break
179
+ return missing_filenames
@@ -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,34 @@
1
+ """Tests for the CLAUDE.md orphan-file blocker decision parts module."""
2
+
3
+ import json
4
+
5
+ import pytest
6
+ from claude_md_orphan_file_blocker_parts import decision as decision_module
7
+ from claude_md_orphan_file_blocker_parts.config.orphan_blocker_constants import (
8
+ ROW_FIRST_RETRY_HINT,
9
+ )
10
+ from claude_md_orphan_file_blocker_parts.decision import (
11
+ build_block_payload,
12
+ deny_orphan_files,
13
+ )
14
+
15
+
16
+ def test_deny_orphan_files_emits_deny(
17
+ capsys: pytest.CaptureFixture[str], monkeypatch: pytest.MonkeyPatch
18
+ ) -> None:
19
+ monkeypatch.setattr(decision_module, "log_hook_block", lambda **each_argument: None)
20
+ deny_orphan_files("Write", "/pkg/CLAUDE.md", "/pkg", ["absent.py"])
21
+ emitted_payload = json.loads(capsys.readouterr().out)
22
+ assert emitted_payload["hookSpecificOutput"]["permissionDecision"] == "deny"
23
+
24
+
25
+ def test_build_block_payload_names_missing_files_and_denies() -> None:
26
+ block_payload = build_block_payload(["absent.py"], "/pkg")
27
+ hook_output = block_payload["hookSpecificOutput"]
28
+ assert hook_output["permissionDecision"] == "deny"
29
+ assert "absent.py" in hook_output["permissionDecisionReason"]
30
+
31
+
32
+ def test_build_block_payload_appends_retry_hint() -> None:
33
+ block_payload = build_block_payload(["absent.py"], "/pkg")
34
+ assert ROW_FIRST_RETRY_HINT in block_payload["hookSpecificOutput"]["permissionDecisionReason"]
@@ -0,0 +1,42 @@
1
+ """Tests for the CLAUDE.md reference-extraction parts module."""
2
+
3
+ from claude_md_orphan_file_blocker_parts.references import (
4
+ find_referenced_filenames,
5
+ find_run_command_filenames,
6
+ is_claude_md_file,
7
+ )
8
+
9
+
10
+ def test_is_claude_md_file_matches_basename() -> None:
11
+ assert is_claude_md_file("/pkg/CLAUDE.md") is True
12
+ assert is_claude_md_file("/pkg/README.md") is False
13
+
14
+
15
+ def test_find_referenced_filenames_reads_table_cell() -> None:
16
+ content = "# t\n\n| File | Note |\n|---|---|\n| `alpha.py` | a |\n"
17
+ assert find_referenced_filenames(content) == ["alpha.py"]
18
+
19
+
20
+ def test_find_referenced_filenames_skips_fenced_table() -> None:
21
+ content = "# t\n\n```\n| File | Note |\n|---|---|\n| `ghost.py` | a |\n```\n"
22
+ assert find_referenced_filenames(content) == []
23
+
24
+
25
+ def test_find_referenced_filenames_skips_relative_path_block() -> None:
26
+ content = "# t\n\nSourced from `../shared`:\n\n| File | Note |\n|---|---|\n| `alpha.py` | a |\n"
27
+ assert find_referenced_filenames(content) == []
28
+
29
+
30
+ def test_find_run_command_filenames_reads_fenced_command() -> None:
31
+ content = "# t\n\n```bash\npython tools/verify.py --flag\n```\n"
32
+ assert find_run_command_filenames(content) == ["verify.py"]
33
+
34
+
35
+ def test_find_run_command_filenames_ignores_prose_command() -> None:
36
+ content = "# t\n\nJust run python outside.py somewhere in prose.\n"
37
+ assert find_run_command_filenames(content) == []
38
+
39
+
40
+ def test_find_run_command_filenames_drops_trailing_comment() -> None:
41
+ content = "# t\n\n```bash\npython real.py # was python old.py\n```\n"
42
+ assert find_run_command_filenames(content) == ["real.py"]
@@ -0,0 +1,27 @@
1
+ """Tests for the CLAUDE.md scan-plan parts module."""
2
+
3
+ from pathlib import Path
4
+
5
+ from claude_md_orphan_file_blocker_parts.scan_plan import (
6
+ build_orphan_scan_plan,
7
+ collect_missing_filenames,
8
+ )
9
+
10
+
11
+ def test_build_orphan_scan_plan_uses_write_content(tmp_path: Path) -> None:
12
+ content = "# t\n\n| File | Note |\n|---|---|\n| `absent.py` | a |\n"
13
+ scan_plan = build_orphan_scan_plan(
14
+ "Write", {"content": content}, str(tmp_path / "CLAUDE.md"), tmp_path
15
+ )
16
+ assert scan_plan.candidate_contents == [content]
17
+ assert scan_plan.baseline_missing_filenames == set()
18
+
19
+
20
+ def test_collect_missing_filenames_reports_write_orphan(tmp_path: Path) -> None:
21
+ package_directory = tmp_path / "package"
22
+ package_directory.mkdir()
23
+ content = "# t\n\n| File | Note |\n|---|---|\n| `absent.py` | a |\n"
24
+ scan_plan = build_orphan_scan_plan(
25
+ "Write", {"content": content}, str(package_directory / "CLAUDE.md"), package_directory
26
+ )
27
+ assert collect_missing_filenames(scan_plan, package_directory) == ["absent.py"]
@@ -0,0 +1,30 @@
1
+ """Tests for the CLAUDE.md subtree-scan parts module."""
2
+
3
+ from pathlib import Path
4
+
5
+ from claude_md_orphan_file_blocker_parts.subtree_scan import find_missing_filenames
6
+
7
+
8
+ def test_find_missing_filenames_reports_absent_reference(tmp_path: Path) -> None:
9
+ package_directory = tmp_path / "package"
10
+ package_directory.mkdir()
11
+ content = "# t\n\n| File | Note |\n|---|---|\n| `absent.py` | a |\n"
12
+ assert find_missing_filenames(content, package_directory) == ["absent.py"]
13
+
14
+
15
+ def test_find_missing_filenames_allows_present_reference(tmp_path: Path) -> None:
16
+ package_directory = tmp_path / "package"
17
+ package_directory.mkdir()
18
+ (package_directory / "present.py").write_text("x = 1\n", encoding="utf-8")
19
+ content = "# t\n\n| File | Note |\n|---|---|\n| `present.py` | a |\n"
20
+ assert find_missing_filenames(content, package_directory) == []
21
+
22
+
23
+ def test_find_missing_filenames_finds_sibling_under_parent(tmp_path: Path) -> None:
24
+ package_directory = tmp_path / "package"
25
+ package_directory.mkdir()
26
+ sibling_directory = tmp_path / "sibling"
27
+ sibling_directory.mkdir()
28
+ (sibling_directory / "neighbor.py").write_text("x = 1\n", encoding="utf-8")
29
+ content = "# t\n\n| File | Note |\n|---|---|\n| `neighbor.py` | a |\n"
30
+ assert find_missing_filenames(content, package_directory) == []
@@ -154,7 +154,7 @@ def _bool_parameters_for_function(
154
154
  continue
155
155
  if _argument_is_boolean(each_argument, default_node):
156
156
  bool_parameters.append((each_argument.arg, each_argument.lineno))
157
- for each_argument, each_default in zip(arguments.kwonlyargs, arguments.kw_defaults):
157
+ for each_argument, each_default in zip(arguments.kwonlyargs, arguments.kw_defaults, strict=False):
158
158
  if each_argument.arg in ALL_SELF_AND_CLS_PARAMETER_NAMES:
159
159
  continue
160
160
  if _argument_is_boolean(each_argument, each_default):
@@ -276,7 +276,7 @@ def check_incomplete_mocks(content: str, file_path: str) -> None:
276
276
 
277
277
  all_scoped_definitions = _collect_scoped_mock_definitions(module_tree)
278
278
 
279
- for each_scope_id, each_mock_name, each_declared_keys, each_definition_line, each_scope_node in all_scoped_definitions:
279
+ for each_scope_id, each_mock_name, each_declared_keys, each_definition_line, each_scope_node in all_scoped_definitions: # noqa: B007
280
280
  assigned_attributes = _collect_mock_attribute_assignments_in_scope(each_scope_node, each_mock_name)
281
281
  all_known_fields = each_declared_keys | assigned_attributes
282
282
  field_accesses = _collect_mock_field_accesses_in_scope(each_scope_node, each_mock_name)
@@ -36,10 +36,10 @@ def _collect_optional_param_defaults(
36
36
  all_args = arguments.posonlyargs + arguments.args
37
37
  defaults_aligned = [None] * (len(all_args) - len(arguments.defaults)) + list(arguments.defaults)
38
38
  param_defaults: dict[str, ast.expr] = {}
39
- for each_arg, each_default in zip(all_args, defaults_aligned):
39
+ for each_arg, each_default in zip(all_args, defaults_aligned, strict=False):
40
40
  if each_default is not None:
41
41
  param_defaults[each_arg.arg] = each_default
42
- for each_kwarg, each_kwdefault in zip(arguments.kwonlyargs, arguments.kw_defaults):
42
+ for each_kwarg, each_kwdefault in zip(arguments.kwonlyargs, arguments.kw_defaults, strict=False):
43
43
  if each_kwdefault is not None:
44
44
  param_defaults[each_kwarg.arg] = each_kwdefault
45
45
  return param_defaults
@@ -183,7 +183,7 @@ def _collect_fixture_injection_arguments(
183
183
  undefaulted_keyword_only_arguments = [
184
184
  each_keyword_argument
185
185
  for each_keyword_argument, each_default in zip(
186
- arguments.kwonlyargs, arguments.kw_defaults
186
+ arguments.kwonlyargs, arguments.kw_defaults, strict=False
187
187
  )
188
188
  if each_default is None
189
189
  ]
@@ -557,7 +557,7 @@ def _leading_token_overlap(first_name: str, second_name: str) -> int:
557
557
  first_tokens = first_name.split("_")
558
558
  second_tokens = second_name.split("_")
559
559
  shared = 0
560
- for first_token, second_token in zip(first_tokens, second_tokens):
560
+ for first_token, second_token in zip(first_tokens, second_tokens, strict=False):
561
561
  if first_token != second_token:
562
562
  break
563
563
  shared += 1
@@ -180,7 +180,7 @@ def _forwards_parameters_unchanged(call_node: ast.Call, all_parameter_names: lis
180
180
  return False
181
181
  if len(call_node.args) != len(all_parameter_names):
182
182
  return False
183
- for each_argument, each_parameter_name in zip(call_node.args, all_parameter_names):
183
+ for each_argument, each_parameter_name in zip(call_node.args, all_parameter_names, strict=False):
184
184
  if not isinstance(each_argument, ast.Name) or each_argument.id != each_parameter_name:
185
185
  return False
186
186
  return True
@@ -149,7 +149,7 @@ def _write_swap_state(
149
149
  serialized_payload = json.dumps(swap_state).encode(STATE_FILE_PAYLOAD_TEXT_ENCODING_NAME)
150
150
  open_flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
151
151
  if hasattr(os, OS_O_NOFOLLOW_ATTRIBUTE_NAME):
152
- open_flags |= os.O_NOFOLLOW # type: ignore[attr-defined] # POSIX-only flag, guarded by hasattr above
152
+ open_flags |= getattr(os, OS_O_NOFOLLOW_ATTRIBUTE_NAME)
153
153
  file_descriptor = _open_state_file_with_retry(state_file, open_flags)
154
154
  if file_descriptor is None:
155
155
  return False
@@ -0,0 +1,26 @@
1
+ # inventory_intent_records
2
+
3
+ The shared pending-intent store that lets `package_inventory_stale_blocker.py`
4
+ and `claude_md_orphan_file_blocker.py` add a new file and its inventory row in
5
+ one change, in either order. When one blocker denies, it records a note; the
6
+ sibling blocker reads that note to allow the matching second write.
7
+
8
+ ## Modules
9
+
10
+ | File | Purpose |
11
+ |---|---|
12
+ | `records.py` | Read, record, peek, and consume the per-session file and row intents; a missing or corrupt store reads as no notes |
13
+ | `__init__.py` | Package marker |
14
+
15
+ ## Subdirectories
16
+
17
+ | Entry | Description |
18
+ |---|---|
19
+ | `config/` | The records-file name shape, the freshness window, the list keys, and the note field names (`intent_records_constants.py`) |
20
+ | `tests/` | pytest suite for the records store |
21
+
22
+ ## Running tests
23
+
24
+ ```bash
25
+ python -m pytest packages/claude-dev-env/hooks/blocking/inventory_intent_records/tests/
26
+ ```
@@ -0,0 +1 @@
1
+ """Shared pending-intent notes both inventory blockers read to break the deadlock."""
@@ -0,0 +1 @@
1
+ """Configuration home for the shared intent-records constants."""
@@ -0,0 +1,20 @@
1
+ """Constants for the shared inventory/orphan pending-intent records.
2
+
3
+ Holds the per-session records-file name shape, the freshness window a pending
4
+ note lives inside, the two list keys the file stores under, and the field names
5
+ of one stored note.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ INTENT_FILE_PREFIX: str = "claude-inventory-intent-"
11
+ INTENT_FILE_SUFFIX: str = ".json"
12
+
13
+ INTENT_FRESHNESS_WINDOW_SECONDS: float = 900.0
14
+
15
+ ALL_FILE_INTENTS_KEY: str = "pending_file_intents"
16
+ ALL_ROW_INTENTS_KEY: str = "pending_row_intents"
17
+
18
+ INTENT_DIRECTORY_FIELD: str = "directory"
19
+ INTENT_FILENAME_FIELD: str = "filename"
20
+ INTENT_RECORDED_AT_FIELD: str = "recorded_at"