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.
- package/_shared/advisor/CLAUDE.md +2 -2
- package/_shared/advisor/advisor-protocol.md +20 -20
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +15 -12
- package/_shared/advisor/scripts/model_tier_run_validator.py +11 -10
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +25 -19
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +17 -17
- package/_shared/advisor/scripts/tier_model_ids.py +18 -18
- package/_shared/pr-loop/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/CLAUDE.md +2 -1
- package/_shared/pr-loop/scripts/README.md +1 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +253 -1980
- package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +32 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/__init__.py +7 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/added_line_maps.py +268 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/enforcer_loading.py +172 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_arguments.py +70 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +326 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +85 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_file_sets.py +331 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +369 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/conftest.py +14 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_added_line_maps.py +118 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_enforcer_loading.py +17 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_arguments.py +29 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_gate_running.py +99 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_blob_readers.py +69 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_git_file_sets.py +137 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +116 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_violation_scoping.py +75 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_wrapper_plumb_check.py +49 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/violation_scoping.py +328 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/wrapper_plumb_check.py +206 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +24 -17
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +1 -0
- package/_shared/pr-loop/scripts/reviews_disabled.py +19 -2
- package/_shared/pr-loop/scripts/test_code_rules_gate.py +278 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate_constants.py +6 -39
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +43 -0
- package/_shared/pr-loop/worker-spawn.md +186 -0
- package/agents/code-verifier.md +1 -1
- package/bin/ever-shipped-skills.mjs +3 -0
- package/bin/expand_home_directory_tokens.mjs +1 -1
- package/bin/install.mjs +5 -2
- package/hooks/advisory/refactor_guard.py +3 -4
- package/hooks/blocking/CLAUDE.md +7 -1
- package/hooks/blocking/block_main_commit.py +2 -2
- package/hooks/blocking/claude_md_orphan_file_blocker.py +75 -699
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +28 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/config/orphan_blocker_constants.py +18 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/decision.py +81 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/references.py +307 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/scan_plan.py +124 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/subtree_scan.py +179 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/conftest.py +10 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_decision.py +34 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_references.py +42 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_scan_plan.py +27 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/tests/test_subtree_scan.py +30 -0
- package/hooks/blocking/code_rules_boolean_mustcheck.py +1 -1
- package/hooks/blocking/code_rules_mock_completeness.py +1 -1
- package/hooks/blocking/code_rules_optional_params.py +2 -2
- package/hooks/blocking/code_rules_shared.py +1 -1
- package/hooks/blocking/code_rules_test_assertions.py +1 -1
- package/hooks/blocking/code_rules_typeddict_stub.py +1 -1
- package/hooks/blocking/gh_pr_author_enforcer.py +1 -1
- package/hooks/blocking/inventory_intent_records/CLAUDE.md +26 -0
- package/hooks/blocking/inventory_intent_records/__init__.py +1 -0
- package/hooks/blocking/inventory_intent_records/config/__init__.py +1 -0
- package/hooks/blocking/inventory_intent_records/config/intent_records_constants.py +20 -0
- package/hooks/blocking/inventory_intent_records/records.py +271 -0
- package/hooks/blocking/inventory_intent_records/tests/conftest.py +10 -0
- package/hooks/blocking/inventory_intent_records/tests/test_records.py +80 -0
- package/hooks/blocking/package_inventory_stale_blocker.py +54 -384
- package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +26 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/config/inventory_blocker_constants.py +16 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/decision.py +84 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/inventory_detection.py +307 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/conftest.py +10 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_decision.py +38 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/tests/test_inventory_detection.py +61 -0
- package/hooks/blocking/pii_payload_scan.py +138 -42
- package/hooks/blocking/pii_prevention_blocker.py +185 -291
- package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +24 -0
- package/hooks/blocking/pii_prevention_blocker_parts/__init__.py +1 -0
- package/hooks/blocking/pii_prevention_blocker_parts/config/__init__.py +1 -0
- package/hooks/blocking/pii_prevention_blocker_parts/config/repository_resolution_constants.py +28 -0
- package/hooks/blocking/pii_prevention_blocker_parts/repository_exemption.py +214 -0
- package/hooks/blocking/pii_prevention_blocker_parts/repository_resolution.py +208 -0
- package/hooks/blocking/pr_description_command_parser.py +8 -4
- package/hooks/blocking/precommit_code_rules_gate.py +3 -3
- package/hooks/blocking/tdd_enforcer.py +97 -608
- package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +30 -0
- package/hooks/blocking/tdd_enforcer_parts/__init__.py +1 -0
- package/hooks/blocking/tdd_enforcer_parts/candidate_paths.py +142 -0
- package/hooks/blocking/tdd_enforcer_parts/config/__init__.py +1 -0
- package/hooks/blocking/tdd_enforcer_parts/config/tdd_enforcer_constants.py +32 -0
- package/hooks/blocking/tdd_enforcer_parts/content_analysis.py +268 -0
- package/hooks/blocking/tdd_enforcer_parts/decisions.py +92 -0
- package/hooks/blocking/tdd_enforcer_parts/freshness.py +80 -0
- package/hooks/blocking/tdd_enforcer_parts/git_tracking.py +63 -0
- package/hooks/blocking/tdd_enforcer_parts/path_classification.py +119 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/conftest.py +10 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_candidate_paths.py +31 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_content_analysis.py +30 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_decisions.py +34 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_freshness.py +28 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_git_tracking.py +48 -0
- package/hooks/blocking/tdd_enforcer_parts/tests/test_path_classification.py +36 -0
- package/hooks/blocking/test_inventory_deadlock_resolution.py +154 -0
- package/hooks/blocking/test_pii_payload_scan.py +168 -0
- package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +55 -158
- package/hooks/blocking/tests/conftest.py +10 -0
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +260 -0
- package/hooks/blocking/tests/test_repository_exemption.py +105 -0
- package/hooks/blocking/tests/test_repository_resolution.py +108 -0
- package/hooks/diagnostic/hook_log_extractor.py +12 -10
- package/hooks/git-hooks/post_commit.py +3 -4
- package/hooks/hooks_constants/CLAUDE.md +2 -2
- package/hooks/hooks_constants/banned_identifiers_constants.py +0 -1
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -1
- package/hooks/hooks_constants/local_identity.py +59 -8
- package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
- package/hooks/hooks_constants/test_local_identity.py +105 -3
- package/hooks/pyproject.toml +13 -36
- package/hooks/session/plugin_data_dir_cleanup.py +0 -1
- package/hooks/validation/mypy_validator.py +2 -2
- package/hooks/validators/health_check.py +1 -0
- package/hooks/validators/mypy_integration.py +2 -0
- package/hooks/validators/ruff_integration.py +3 -0
- package/hooks/workflow/auto_formatter.py +5 -4
- package/package.json +1 -1
- package/scripts/CLAUDE.md +4 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +6 -4
- package/scripts/dev_env_scripts_constants/code_review_constants.py +71 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +435 -0
- package/scripts/dev_env_scripts_constants/timing.py +7 -1
- package/scripts/grok_headless_runner.py +294 -0
- package/scripts/grok_worker_preflight.py +410 -0
- package/scripts/invoke_code_review.py +463 -0
- package/scripts/resolve_worker_spawn.py +619 -0
- package/scripts/spawn_grok_batch.py +672 -0
- package/scripts/test_grok_headless_runner.py +626 -0
- package/scripts/test_grok_worker_preflight.py +1054 -0
- package/scripts/test_invoke_code_review.py +672 -0
- package/scripts/test_resolve_worker_spawn.py +1014 -0
- package/scripts/test_spawn_grok_batch.py +1017 -0
- package/skills/CLAUDE.md +5 -3
- package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +72 -13
- package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +121 -14
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +78 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +121 -0
- package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +196 -6
- package/skills/autoconverge/CLAUDE.md +3 -3
- package/skills/autoconverge/SKILL.md +9 -3
- package/skills/autoconverge/reference/CLAUDE.md +2 -2
- package/skills/autoconverge/reference/convergence.md +33 -11
- package/skills/autoconverge/reference/stop-conditions.md +16 -5
- package/skills/autoconverge/workflow/CLAUDE.md +2 -1
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +7 -2
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +300 -0
- package/skills/autoconverge/workflow/converge.contract.test.mjs +5 -5
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +29 -29
- package/skills/autoconverge/workflow/converge.fix-progress.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.mjs +200 -16
- package/skills/bugteam/CLAUDE.md +2 -2
- package/skills/bugteam/CONSTRAINTS.md +3 -2
- package/skills/bugteam/PROMPTS.md +7 -6
- package/skills/bugteam/SKILL.md +18 -13
- package/skills/bugteam/reference/audit-and-teammates.md +215 -35
- package/skills/bugteam/reference/design-rationale.md +1 -1
- package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -1
- package/skills/bugteam/reference/team-setup.md +8 -2
- package/skills/codex-review/CLAUDE.md +46 -0
- package/skills/codex-review/SKILL.md +181 -0
- package/skills/codex-review/reference/CLAUDE.md +15 -0
- package/skills/codex-review/reference/cli-contract.md +253 -0
- package/skills/codex-review/reference/loop-integration.md +118 -0
- package/skills/codex-review/scripts/codex_down_classifier.py +98 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +18 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/__init__.py +1 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/classifier_constants.py +35 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +86 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/findings_constants.py +18 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +45 -0
- package/skills/codex-review/scripts/codex_usage_probe.py +573 -0
- package/skills/codex-review/scripts/fixtures/auth_failure_synthetic.txt +1 -0
- package/skills/codex-review/scripts/fixtures/config_load_failure_v0.125.0.txt +1 -0
- package/skills/codex-review/scripts/fixtures/freeform_findings_v0.144.3.txt +6 -0
- package/skills/codex-review/scripts/fixtures/model_rejection_v0.125.0.jsonl +5 -0
- package/skills/codex-review/scripts/fixtures/structured_findings.txt +13 -0
- package/skills/codex-review/scripts/fixtures/success_stream_v0.144.3.jsonl +6 -0
- package/skills/codex-review/scripts/fixtures/unknown_failure_synthetic.txt +1 -0
- package/skills/codex-review/scripts/fixtures/usage_limit_synthetic.txt +1 -0
- package/skills/codex-review/scripts/parse_codex_findings.py +207 -0
- package/skills/codex-review/scripts/run_codex_review.py +415 -0
- package/skills/codex-review/scripts/test_codex_down_classifier.py +143 -0
- package/skills/codex-review/scripts/test_codex_usage_probe.py +678 -0
- package/skills/codex-review/scripts/test_parse_codex_findings.py +130 -0
- package/skills/codex-review/scripts/test_run_codex_review.py +812 -0
- package/skills/codex-review/test_skill_scaffold.py +192 -0
- package/skills/grok-spawn/CLAUDE.md +28 -0
- package/skills/grok-spawn/SKILL.md +226 -0
- package/skills/grok-spawn/reference/flag-profiles.md +132 -0
- package/skills/grok-spawn/reference/worker-briefs.md +152 -0
- package/skills/grokify/SKILL.md +9 -1
- package/skills/grokify/capability-claims.test.mjs +28 -0
- package/skills/grokify/evals/README.md +72 -0
- package/skills/grokify/evals/parse-payload.test.mjs +171 -0
- package/skills/grokify/evals/run-capability-evals.mjs +545 -0
- package/skills/orchestrator/SKILL.md +42 -29
- package/skills/orchestrator-refresh/SKILL.md +17 -9
- package/skills/pr-converge/SKILL.md +34 -13
- package/skills/pr-converge/reference/convergence-gates.md +42 -15
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +1 -1
- package/skills/pr-converge/reference/per-tick.md +130 -42
- package/skills/pr-converge/reference/state-schema.md +10 -0
- package/skills/pr-converge/scripts/CLAUDE.md +2 -0
- package/skills/pr-converge/scripts/_pr_converge_path_setup.py +5 -1
- package/skills/pr-converge/scripts/check_convergence.py +605 -29
- package/skills/pr-converge/scripts/check_convergence_availability.py +232 -0
- package/skills/pr-converge/scripts/check_convergence_gates.py +279 -235
- package/skills/pr-converge/scripts/check_convergence_thread_gates.py +1 -1
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/convergence_gate_constants.py +36 -2
- package/skills/pr-converge/scripts/test__pr_converge_path_setup.py +4 -0
- package/skills/pr-converge/scripts/test_check_convergence.py +71 -3
- package/skills/pr-converge/scripts/test_check_convergence_availability.py +326 -0
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -0
- package/skills/pr-converge/scripts/test_check_convergence_contract.py +89 -17
- package/skills/pr-converge/scripts/test_check_convergence_fixture.py +179 -0
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -68
- package/skills/pr-converge/scripts/test_check_convergence_thread_gates.py +24 -0
- package/skills/pr-converge/test_step5_host_branch.py +106 -0
- package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
- package/skills/reviewer-gates/SKILL.md +7 -5
- 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
|
|
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"
|