claude-dev-env 1.94.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_shared/advisor/CLAUDE.md +2 -2
- package/_shared/advisor/advisor-protocol.md +41 -33
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +17 -13
- package/_shared/advisor/scripts/model_tier_run_validator.py +27 -18
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +95 -25
- 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/CLAUDE.md +10 -1
- package/bin/ever-shipped-skills.mjs +73 -0
- package/bin/expand_home_directory_tokens.mjs +1 -1
- package/bin/install.mjs +140 -7
- package/bin/install.prune.test.mjs +457 -0
- package/docs/CODE_RULES.md +1 -1
- 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_enforcer.py +4 -0
- 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 +83 -1
- package/hooks/blocking/code_rules_test_assertions.py +1 -1
- package/hooks/blocking/code_rules_test_layout.py +9 -3
- 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/plain_language_blocker.py +138 -4
- package/hooks/blocking/pr_description_command_parser.py +8 -4
- package/hooks/blocking/precommit_code_rules_gate.py +3 -3
- package/hooks/blocking/sensitive_file_protector.py +114 -48
- package/hooks/blocking/tdd_enforcer.py +97 -601
- 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_code_rules_enforcer_scratchpad.py +105 -0
- package/hooks/blocking/test_code_rules_shared.py +181 -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_plain_language_blocker_allowlist.py +184 -0
- package/hooks/blocking/test_sensitive_file_protector.py +185 -0
- package/hooks/blocking/test_tdd_enforcer_restore.py +108 -0
- package/hooks/blocking/test_tdd_enforcer_scratchpad.py +105 -0
- 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 +4 -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/harness_scratchpad_constants.py +17 -0
- package/hooks/hooks_constants/local_identity.py +59 -8
- package/hooks/hooks_constants/pii_prevention_constants.py +0 -6
- package/hooks/hooks_constants/plain_language_blocker_constants.py +5 -0
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +42 -0
- package/hooks/hooks_constants/test_local_identity.py +105 -3
- package/hooks/pyproject.toml +52 -4
- package/hooks/session/plugin_data_dir_cleanup.py +0 -1
- package/hooks/validation/mypy_validator.py +2 -2
- package/hooks/validators/CLAUDE.md +1 -1
- package/hooks/validators/README.md +2 -0
- package/hooks/validators/health_check.py +1 -0
- package/hooks/validators/mypy_integration.py +2 -0
- package/hooks/validators/python_style_checks.py +114 -136
- package/hooks/validators/python_style_helpers.py +95 -0
- package/hooks/validators/ruff_integration.py +3 -0
- package/hooks/validators/test_python_style_checks.py +0 -164
- package/hooks/validators/test_python_style_checks_decorator_gap.py +119 -0
- package/hooks/validators/test_python_style_fixes.py +251 -0
- package/hooks/validators/test_python_style_helpers.py +125 -0
- package/hooks/workflow/auto_formatter.py +5 -4
- package/package.json +1 -1
- package/rules/CLAUDE.md +1 -0
- package/rules/anti-corollary-tests.md +69 -0
- package/rules/bdd.md +1 -3
- package/rules/code-reviews.md +1 -1
- package/rules/gh-paginate.md +1 -1
- package/rules/plain-language.md +2 -0
- 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 +6 -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 +202 -18
- 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/README.md +2 -3
- 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/closeout/SKILL.md +153 -0
- package/skills/closeout/reference/handoff-prompt-template.md +72 -0
- package/skills/closeout/reference/issue-body-templates.md +108 -0
- package/skills/closeout/reference/pii-redaction-checklist.md +36 -0
- 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 +32 -22
- package/skills/orchestrator-refresh/SKILL.md +12 -8
- package/skills/pr-converge/CLAUDE.md +1 -1
- 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-fix-protocol/SKILL.md +65 -0
- package/skills/pr-loop-cloud-transport/SKILL.md +2 -0
- package/skills/reviewer-gates/SKILL.md +7 -5
- package/skills/skill-builder/references/skill-modularity.md +1 -1
- package/skills/team-advisor/SKILL.md +16 -12
- package/system-prompts/software-engineer.xml +7 -6
- package/hooks/validators/test_verify_paths.py +0 -32
- package/hooks/validators/verify_paths.py +0 -57
|
@@ -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):
|
|
@@ -158,6 +158,7 @@ from code_rules_shared import ( # noqa: E402
|
|
|
158
158
|
is_ephemeral_script_path,
|
|
159
159
|
is_hook_infrastructure,
|
|
160
160
|
is_test_file,
|
|
161
|
+
is_under_session_scratchpad,
|
|
161
162
|
)
|
|
162
163
|
from code_rules_string_magic import ( # noqa: E402
|
|
163
164
|
check_inline_literal_collections,
|
|
@@ -1104,6 +1105,9 @@ def main(all_arguments: list[str]) -> None:
|
|
|
1104
1105
|
tool_input = pretooluse_payload.get("tool_input", {})
|
|
1105
1106
|
file_path = tool_input.get("file_path", "")
|
|
1106
1107
|
|
|
1108
|
+
if is_under_session_scratchpad(file_path, pretooluse_payload):
|
|
1109
|
+
sys.exit(0)
|
|
1110
|
+
|
|
1107
1111
|
runs_full_verdict = _is_validated_target(file_path)
|
|
1108
1112
|
if not runs_full_verdict and not _is_hook_infrastructure_python_target(file_path):
|
|
1109
1113
|
sys.exit(0)
|
|
@@ -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
|
|
@@ -4,6 +4,7 @@ import ast
|
|
|
4
4
|
import difflib
|
|
5
5
|
import os
|
|
6
6
|
import sys
|
|
7
|
+
import tempfile
|
|
7
8
|
from collections.abc import Collection, Iterator
|
|
8
9
|
from pathlib import Path
|
|
9
10
|
|
|
@@ -29,6 +30,13 @@ from hooks_constants.code_rules_enforcer_constants import ( # noqa: E402
|
|
|
29
30
|
LEADING_DRIVE_LETTER_PATTERN,
|
|
30
31
|
STRICT_TEST_FILE_BASENAME_PATTERN,
|
|
31
32
|
)
|
|
33
|
+
from hooks_constants.harness_scratchpad_constants import ( # noqa: E402
|
|
34
|
+
HARNESS_SCRATCHPAD_LEAF_DIRECTORY_NAME,
|
|
35
|
+
HARNESS_SCRATCHPAD_PATH_SEPARATOR_REPLACEMENT,
|
|
36
|
+
HARNESS_SCRATCHPAD_USER_DIRECTORY_PREFIX,
|
|
37
|
+
HOOK_PAYLOAD_SESSION_ID_KEY,
|
|
38
|
+
HOOK_PAYLOAD_WORKING_DIRECTORY_KEY,
|
|
39
|
+
)
|
|
32
40
|
from hooks_constants.unused_module_import_constants import ( # noqa: E402
|
|
33
41
|
TYPE_CHECKING_IDENTIFIER,
|
|
34
42
|
)
|
|
@@ -175,7 +183,7 @@ def _collect_fixture_injection_arguments(
|
|
|
175
183
|
undefaulted_keyword_only_arguments = [
|
|
176
184
|
each_keyword_argument
|
|
177
185
|
for each_keyword_argument, each_default in zip(
|
|
178
|
-
arguments.kwonlyargs, arguments.kw_defaults
|
|
186
|
+
arguments.kwonlyargs, arguments.kw_defaults, strict=False
|
|
179
187
|
)
|
|
180
188
|
if each_default is None
|
|
181
189
|
]
|
|
@@ -267,6 +275,80 @@ def is_ephemeral_script_path(file_path: str) -> bool:
|
|
|
267
275
|
return False
|
|
268
276
|
|
|
269
277
|
|
|
278
|
+
def _resolve_session_scratchpad_root(hook_payload: dict) -> str | None:
|
|
279
|
+
"""Rebuild the harness session scratchpad directory from a PreToolUse payload.
|
|
280
|
+
|
|
281
|
+
::
|
|
282
|
+
|
|
283
|
+
payload.cwd = /home/user/project payload.session_id = 5f2c...
|
|
284
|
+
| |
|
|
285
|
+
<tempdir>/claude-<uid>/-home-user-project/5f2c.../scratchpad
|
|
286
|
+
| |
|
|
287
|
+
os.getuid() cwd with each "/" turned to "-"
|
|
288
|
+
|
|
289
|
+
No environment variable carries this path, so it is rebuilt from the three
|
|
290
|
+
signals a hook can read: the POSIX user id and the ``cwd`` and ``session_id``
|
|
291
|
+
fields the harness puts in every PreToolUse payload. The rebuilt directory
|
|
292
|
+
is returned only when it exists on disk, so a wrong guess or a non-POSIX
|
|
293
|
+
platform yields None and the gates keep full enforcement.
|
|
294
|
+
|
|
295
|
+
Args:
|
|
296
|
+
hook_payload: The PreToolUse payload carrying ``cwd`` and ``session_id``.
|
|
297
|
+
|
|
298
|
+
Returns:
|
|
299
|
+
The scratchpad directory path when it exists on disk, else None.
|
|
300
|
+
"""
|
|
301
|
+
get_user_id = getattr(os, "getuid", None)
|
|
302
|
+
if get_user_id is None:
|
|
303
|
+
return None
|
|
304
|
+
session_id = hook_payload.get(HOOK_PAYLOAD_SESSION_ID_KEY, "")
|
|
305
|
+
working_directory = hook_payload.get(HOOK_PAYLOAD_WORKING_DIRECTORY_KEY, "")
|
|
306
|
+
if not isinstance(session_id, str) or not session_id:
|
|
307
|
+
return None
|
|
308
|
+
if not isinstance(working_directory, str) or not working_directory:
|
|
309
|
+
return None
|
|
310
|
+
mangled_working_directory = working_directory.replace("\\", "/").replace(
|
|
311
|
+
"/", HARNESS_SCRATCHPAD_PATH_SEPARATOR_REPLACEMENT
|
|
312
|
+
)
|
|
313
|
+
user_directory_name = f"{HARNESS_SCRATCHPAD_USER_DIRECTORY_PREFIX}{get_user_id()}"
|
|
314
|
+
scratchpad_root = os.path.join(
|
|
315
|
+
tempfile.gettempdir(),
|
|
316
|
+
user_directory_name,
|
|
317
|
+
mangled_working_directory,
|
|
318
|
+
session_id,
|
|
319
|
+
HARNESS_SCRATCHPAD_LEAF_DIRECTORY_NAME,
|
|
320
|
+
)
|
|
321
|
+
if not os.path.isdir(scratchpad_root):
|
|
322
|
+
return None
|
|
323
|
+
return scratchpad_root
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def is_under_session_scratchpad(file_path: str, hook_payload: dict) -> bool:
|
|
327
|
+
"""Return True when file_path resolves under the harness session scratchpad.
|
|
328
|
+
|
|
329
|
+
One-off scripts written to the session scratchpad are throwaway tooling
|
|
330
|
+
outside every repo, so the TDD and CODE_RULES gates skip them. Both
|
|
331
|
+
file_path and the rebuilt scratchpad root are resolved through the real
|
|
332
|
+
filesystem (symlinks followed) before the containment test, so a symlink
|
|
333
|
+
into the scratchpad exempts and a symlink out of it does not.
|
|
334
|
+
|
|
335
|
+
Args:
|
|
336
|
+
file_path: The path the write targets.
|
|
337
|
+
hook_payload: The PreToolUse payload carrying ``cwd`` and ``session_id``.
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
True when file_path's real path sits at or under the session scratchpad.
|
|
341
|
+
"""
|
|
342
|
+
if not file_path:
|
|
343
|
+
return False
|
|
344
|
+
scratchpad_root = _resolve_session_scratchpad_root(hook_payload)
|
|
345
|
+
if scratchpad_root is None:
|
|
346
|
+
return False
|
|
347
|
+
real_target = os.path.realpath(file_path)
|
|
348
|
+
real_root = os.path.realpath(scratchpad_root)
|
|
349
|
+
return real_target == real_root or real_target.startswith(real_root + os.sep)
|
|
350
|
+
|
|
351
|
+
|
|
270
352
|
def is_migration_file(file_path: str) -> bool:
|
|
271
353
|
"""Check if file is a Django migration (must be self-contained)."""
|
|
272
354
|
path_lower = file_path.lower().replace("\\", "/")
|
|
@@ -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
|
|
@@ -133,7 +133,11 @@ def _referenced_names(tree: ast.Module) -> set[str]:
|
|
|
133
133
|
}
|
|
134
134
|
return load_names | literal_values
|
|
135
135
|
|
|
136
|
-
def _dead_constant_messages(
|
|
136
|
+
def _dead_constant_messages(
|
|
137
|
+
tree: ast.Module,
|
|
138
|
+
referenced_names: set[str],
|
|
139
|
+
maximum_issues: int,
|
|
140
|
+
) -> list[str]:
|
|
137
141
|
"""Return one message per module constant absent from the referenced set."""
|
|
138
142
|
issues: list[str] = []
|
|
139
143
|
for each_name, each_line in _module_constant_targets(tree):
|
|
@@ -142,7 +146,7 @@ def _dead_constant_messages(tree: ast.Module, referenced_names: set[str]) -> lis
|
|
|
142
146
|
issues.append(
|
|
143
147
|
f"Line {each_line}: constant {each_name!r} - {DEAD_TEST_CONSTANT_GUIDANCE}"
|
|
144
148
|
)
|
|
145
|
-
if len(issues) >=
|
|
149
|
+
if len(issues) >= maximum_issues:
|
|
146
150
|
break
|
|
147
151
|
return issues
|
|
148
152
|
|
|
@@ -174,7 +178,9 @@ def check_dead_test_module_constant(content: str, file_path: str) -> list[str]:
|
|
|
174
178
|
tree = _parse_module(content)
|
|
175
179
|
if tree is None:
|
|
176
180
|
return []
|
|
177
|
-
return _dead_constant_messages(
|
|
181
|
+
return _dead_constant_messages(
|
|
182
|
+
tree, _referenced_names(tree), MAX_TEST_LAYOUT_ISSUES
|
|
183
|
+
)
|
|
178
184
|
|
|
179
185
|
def _node_names_fixture(node: ast.AST) -> bool:
|
|
180
186
|
"""Return whether one decorator sub-node spells the pytest fixture marker."""
|
|
@@ -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"
|