claude-dev-env 2.3.0 → 2.5.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/CLAUDE.md +53 -48
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +4 -2
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
- package/_shared/pr-loop/worker-spawn.md +1 -1
- package/agents/CLAUDE.md +3 -1
- package/agents/caveman.md +0 -1
- package/agents/clasp-deployment-orchestrator.md +0 -1
- package/agents/clean-coder.md +0 -1
- package/agents/code-advisor.md +0 -1
- package/agents/code-quality-agent.md +1 -2
- package/agents/code-verifier.md +3 -4
- package/agents/deep-research.md +0 -1
- package/agents/docs-agent.md +0 -1
- package/agents/git-commit-crafter.md +0 -1
- package/agents/issue-tracker.md +42 -0
- package/agents/plan-packet-validator.md +0 -1
- package/agents/pr-description-writer.md +0 -1
- package/agents/skill-writer-agent.md +84 -0
- package/agents/test_agent_frontmatter.py +67 -18
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +105 -3
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
- package/bin/CLAUDE.md +68 -5
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install-constants.mjs +88 -0
- package/bin/install.mjs +1138 -114
- package/bin/install.prune.test.mjs +869 -19
- package/bin/install.test.mjs +906 -2
- package/commands/implement.md +1 -1
- package/commands/right-size.md +1 -1
- package/docs/CLAUDE.md +2 -0
- package/docs/host-pool-health-monitor.md +102 -0
- package/docs/references/CLAUDE.md +5 -2
- package/docs/references/advisor-tool.md +13 -0
- package/docs/references/code-review-enforcement.md +107 -0
- package/docs/references/team-advisor-skill.md +14 -0
- package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
- package/hooks/blocking/CLAUDE.md +9 -1
- package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
- package/hooks/blocking/code_review_gate_deny.py +74 -0
- package/hooks/blocking/code_review_pr_create_gate.py +198 -0
- package/hooks/blocking/code_review_push_gate.py +145 -0
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +348 -0
- package/hooks/blocking/code_review_stamp_store.py +233 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
- package/hooks/blocking/config/__init__.py +5 -5
- package/hooks/blocking/config/code_review_enforcement_constants.py +113 -0
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +113 -0
- package/hooks/blocking/config/verified_commit_constants.py +160 -155
- package/hooks/blocking/conftest.py +2 -0
- package/hooks/blocking/convergence_gate_blocker.py +112 -23
- package/hooks/blocking/destructive_command_blocker.py +19 -6
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
- package/hooks/blocking/pr_description_proof_of_work.py +52 -34
- package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
- package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
- package/hooks/blocking/test_code_review_gate_deny.py +54 -0
- package/hooks/blocking/test_code_review_pr_create_gate.py +199 -0
- package/hooks/blocking/test_code_review_push_gate.py +205 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +199 -0
- package/hooks/blocking/test_code_review_stamp_store.py +205 -0
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
- package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
- package/hooks/blocking/test_destructive_command_blocker.py +1 -1
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
- package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +17 -8
- package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
- package/hooks/blocking/test_verification_verdict_store.py +974 -903
- package/hooks/blocking/test_verified_commit_gate.py +581 -581
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
- package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
- package/hooks/blocking/verification_verdict_store.py +1039 -1014
- package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
- package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
- package/hooks/blocking/verifier_verdict_minter.py +280 -280
- package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
- package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
- package/hooks/git-hooks/pre_push.py +89 -2
- package/hooks/git-hooks/test_pre_push.py +128 -0
- package/hooks/hooks.json +26 -1
- package/hooks/hooks_constants/CLAUDE.md +3 -1
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
- package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
- package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
- package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
- package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
- package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
- package/hooks/pyproject.toml +1 -0
- package/hooks/validators/CLAUDE.md +2 -0
- package/hooks/validators/config/__init__.py +0 -0
- package/hooks/validators/config/directory_exemption_constants.py +183 -0
- package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
- package/hooks/validators/conftest.py +4 -0
- package/hooks/validators/mypy_integration.py +63 -50
- package/hooks/validators/pyproject_config_discovery.py +101 -0
- package/hooks/validators/ruff_integration.py +257 -25
- package/hooks/validators/run_all_validators.py +223 -19
- package/hooks/validators/test_directory_exemption_constants.py +185 -0
- package/hooks/validators/test_mypy_integration.py +32 -0
- package/hooks/validators/test_pyproject_config_discovery.py +94 -0
- package/hooks/validators/test_python_antipattern_checks.py +110 -5
- package/hooks/validators/test_ruff_integration.py +160 -2
- package/hooks/validators/test_run_all_validators.py +140 -68
- package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
- package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
- package/package.json +10 -2
- package/rules/CLAUDE.md +1 -0
- package/rules/docstring-prose-matches-implementation.md +45 -67
- package/rules/durable-post-artifacts.md +7 -0
- package/rules/state-what-is.md +25 -0
- package/rules/verified-commit-gate-skip.md +1 -1
- package/scripts/CLAUDE.md +1 -0
- package/scripts/Capture-PoolHealth.ps1 +410 -0
- package/scripts/_code_review_test_support.py +404 -0
- package/scripts/claude_chain_runner.py +141 -1
- package/scripts/codec_forwarding_test_support.py +83 -0
- package/scripts/conftest.py +23 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -2
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +53 -1
- package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
- package/scripts/invoke_code_review.py +550 -38
- package/scripts/resolve_worker_spawn.py +626 -619
- package/scripts/spawn_grok_batch.py +672 -672
- package/scripts/test_claude_chain_runner.py +131 -0
- package/scripts/test_invoke_code_review_chain.py +70 -0
- package/scripts/test_invoke_code_review_cli.py +192 -0
- package/scripts/test_invoke_code_review_codec.py +77 -0
- package/scripts/test_invoke_code_review_contract.py +256 -0
- package/scripts/test_invoke_code_review_git.py +123 -0
- package/scripts/test_invoke_code_review_mode.py +99 -0
- package/scripts/test_resolve_worker_spawn.py +1014 -1014
- package/scripts/test_resolve_worker_spawn_codec.py +101 -0
- package/skills/CLAUDE.md +2 -0
- package/skills/auditing-claude-config/SKILL.md +114 -114
- package/skills/autoconverge/SKILL.md +427 -421
- package/skills/autoconverge/reference/convergence.md +26 -4
- package/skills/autoconverge/reference/multi-pr.md +6 -1
- package/skills/autoconverge/reference/stop-conditions.md +16 -10
- package/skills/autoconverge/workflow/CLAUDE.md +1 -0
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1244
- package/skills/autoconverge/workflow/converge.mjs +191 -8
- package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
- package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
- package/skills/closeout/SKILL.md +33 -50
- package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
- package/skills/codex-review/scripts/run_codex_review.py +233 -1
- package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
- package/skills/condensing-instructions/SKILL.md +81 -0
- package/skills/copilot-review/SKILL.md +119 -119
- package/skills/e-code-review/SKILL.md +52 -0
- package/skills/e-code-review/reference/fix.md +54 -0
- package/skills/e-code-review/reference/loop.md +43 -0
- package/skills/e-code-review/reference/low.md +57 -0
- package/skills/e-code-review/reference/medium.md +153 -0
- package/skills/e-code-review/reference/xhigh.md +182 -0
- package/skills/e-simplify/SKILL.md +97 -0
- package/skills/fresh-branch/CLAUDE.md +2 -0
- package/skills/fresh-branch/SKILL.md +2 -0
- package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
- package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
- package/skills/fresh-branch/scripts/pytest.ini +4 -0
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
- package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
- package/skills/issue-tracker/SKILL.md +92 -0
- package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
- package/skills/issue-tracker/reference/handoff-schema.md +64 -0
- package/skills/issue-tracker/reference/operation-matrix.md +41 -0
- package/skills/orchestrator/SKILL.md +162 -21
- package/skills/orchestrator/scripts/status_gate.py +625 -0
- package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
- package/skills/orchestrator/scripts/test_status_gate.py +439 -0
- package/skills/orchestrator-refresh/SKILL.md +110 -35
- package/skills/plan-to-pr/SKILL.md +155 -0
- package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
- package/skills/plan-to-pr/reference/model-routing.md +36 -0
- package/skills/plan-to-pr/reference/packet-contract.md +43 -0
- package/skills/plan-to-pr/reference/packet-schema.json +57 -0
- package/skills/plan-to-pr/reference/process-inventory.md +22 -0
- package/skills/plan-to-pr/reference/review-loop.md +33 -0
- package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
- package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
- package/skills/plan-to-pr/reference/task-seeds.md +14 -0
- package/skills/plan-to-pr/reference/task-ticket.md +38 -0
- package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
- package/skills/plan-to-pr/scripts/config/constants.py +193 -0
- package/skills/plan-to-pr/scripts/create_packet.py +173 -0
- package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
- package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
- package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
- package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
- package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
- package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
- package/skills/plan-to-pr/scripts/validate_run.py +173 -0
- package/skills/plan-to-pr/test_skill_contract.py +207 -0
- package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
- package/skills/pr-converge/SKILL.md +472 -469
- package/skills/pr-converge/reference/examples.md +3 -3
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +7 -4
- package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
- package/skills/pr-converge/reference/per-tick.md +5 -5
- package/skills/pr-converge/reference/progress-checklist.md +1 -1
- package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
- package/skills/pr-converge/test_step5_host_branch.py +1 -1
- package/skills/pr-fix-protocol/SKILL.md +1 -1
- package/skills/privacy-hygiene/SKILL.md +68 -68
- package/skills/prototype/SKILL.md +86 -0
- package/skills/prototype/reference/honest-limitations.md +23 -0
- package/skills/prototype/reference/promotion-tasks.md +23 -0
- package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
- package/skills/prototype/scripts/conftest.py +15 -0
- package/skills/prototype/scripts/launch_sandbox.py +205 -0
- package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
- package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
- package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
- package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
- package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
- package/skills/prototype/workflows/promotion.md +27 -0
- package/skills/prototype/workflows/sandbox.md +35 -0
- package/skills/release-notes-html/SKILL.md +164 -0
- package/skills/skill-builder/CLAUDE.md +3 -3
- package/skills/skill-builder/SKILL.md +5 -5
- package/skills/skill-builder/references/CLAUDE.md +1 -1
- package/skills/skill-builder/references/delegation-map.md +3 -3
- package/skills/skill-builder/references/description-field.md +1 -1
- package/skills/skill-builder/references/skill-modularity.md +2 -3
- package/skills/skill-builder/workflows/CLAUDE.md +1 -1
- package/skills/skill-builder/workflows/improve-skill.md +1 -1
- package/skills/skill-builder/workflows/new-skill.md +2 -2
- package/skills/task-build/CLAUDE.md +8 -7
- package/skills/task-build/SKILL.md +16 -8
- package/skills/task-build/reference/tool-routing.md +19 -0
- package/skills/team-advisor/SKILL.md +2 -2
- package/scripts/test_invoke_code_review.py +0 -672
- package/skills/closeout/reference/issue-body-templates.md +0 -108
|
@@ -18,6 +18,15 @@ and scanned, since that content is what gets embedded in the post). The ``gh``
|
|
|
18
18
|
command is tokenized with ``shlex.split`` and the ``gh`` word must be the first
|
|
19
19
|
command token, so a ``gh pr comment`` that only appears as quoted data inside
|
|
20
20
|
another argument never classifies.
|
|
21
|
+
|
|
22
|
+
The marker scan runs in two tiers over the normalized body. Five bare markers
|
|
23
|
+
— the system temp locations and env tokens — match as a plain substring
|
|
24
|
+
anywhere. Two dot-relative directory markers, ``.claude-editor/jobs/`` and
|
|
25
|
+
``.claude/worktrees/``, match when either a slash sits immediately before the
|
|
26
|
+
marker or a path segment follows it. A machine-local absolute path
|
|
27
|
+
(``~/.claude/worktrees/wt/f.py``) and a relative path that names a child
|
|
28
|
+
(``see .claude/worktrees/wt-1/notes.md``) both count; a bare directory-name
|
|
29
|
+
mention with neither anchor posts cleanly.
|
|
21
30
|
"""
|
|
22
31
|
|
|
23
32
|
import json
|
|
@@ -42,30 +51,79 @@ from blocking._gh_body_arg_utils import ( # noqa: E402
|
|
|
42
51
|
)
|
|
43
52
|
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
44
53
|
from hooks_constants.volatile_path_in_post_blocker_constants import ( # noqa: E402
|
|
54
|
+
ALL_BARE_VOLATILE_PATH_MARKERS,
|
|
45
55
|
ALL_GH_POST_SUBCOMMANDS,
|
|
46
56
|
ALL_MCP_BODY_PARAM_NAMES,
|
|
47
|
-
|
|
57
|
+
ALL_PATH_ANCHORED_VOLATILE_PATH_MARKERS,
|
|
48
58
|
BASH_TOOL_NAME,
|
|
49
59
|
BODY_FILE_ENCODING,
|
|
50
60
|
CORRECTIVE_MESSAGE,
|
|
51
61
|
GH_COMMAND_NAME,
|
|
52
62
|
MCP_GITHUB_TOOL_PREFIX,
|
|
53
63
|
MINIMUM_POST_SUBCOMMAND_TOKEN_COUNT,
|
|
64
|
+
PATH_ANCHOR_CHARACTER,
|
|
65
|
+
PATH_SEGMENT_START_CHARACTERS,
|
|
54
66
|
TOKEN_JOIN_SEPARATOR,
|
|
55
67
|
)
|
|
56
68
|
|
|
57
69
|
|
|
70
|
+
def _character_starts_path_segment(character: str) -> bool:
|
|
71
|
+
"""Return whether a character can open a path segment after a marker."""
|
|
72
|
+
return bool(character) and (
|
|
73
|
+
character.isalnum() or character in PATH_SEGMENT_START_CHARACTERS
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _text_has_anchored_marker(normalized_text: str, marker: str) -> bool:
|
|
78
|
+
"""Return whether the marker appears as part of a path.
|
|
79
|
+
|
|
80
|
+
A match counts when either a slash sits immediately before the marker or a
|
|
81
|
+
path segment follows it. Every occurrence is scanned, so a bare directory
|
|
82
|
+
mention earlier in the text never masks a path occurrence later::
|
|
83
|
+
|
|
84
|
+
ok: ".claude/worktrees/ is the manifest key" -> False
|
|
85
|
+
flag: "path c:/users/me/.claude/worktrees/wt/f.py" -> True
|
|
86
|
+
flag: "see .claude/worktrees/wt-1/notes.md" -> True
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
normalized_text: The body text with backslashes folded to forward
|
|
90
|
+
slashes and lowercased.
|
|
91
|
+
marker: A dot-relative directory marker to search for.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
True when at least one occurrence is path-anchored.
|
|
95
|
+
"""
|
|
96
|
+
search_start = 0
|
|
97
|
+
while True:
|
|
98
|
+
found_index = normalized_text.find(marker, search_start)
|
|
99
|
+
if found_index < 0:
|
|
100
|
+
return False
|
|
101
|
+
preceding_character = normalized_text[found_index - 1 : found_index]
|
|
102
|
+
following_index = found_index + len(marker)
|
|
103
|
+
following_character = normalized_text[following_index : following_index + 1]
|
|
104
|
+
is_slash_before = preceding_character == PATH_ANCHOR_CHARACTER
|
|
105
|
+
is_segment_after = _character_starts_path_segment(following_character)
|
|
106
|
+
if is_slash_before or is_segment_after:
|
|
107
|
+
return True
|
|
108
|
+
search_start = found_index + 1
|
|
109
|
+
|
|
110
|
+
|
|
58
111
|
def scan_text_for_volatile_marker(text: str) -> str | None:
|
|
59
112
|
"""Return the first volatile-path marker found in text, or None.
|
|
60
113
|
|
|
61
114
|
Backslashes normalize to forward slashes and the text lowercases before the
|
|
62
|
-
scan
|
|
115
|
+
scan. The two dot-relative directory markers count when either a slash sits
|
|
116
|
+
immediately before them or a path segment follows them; the five bare
|
|
117
|
+
markers match anywhere::
|
|
63
118
|
|
|
64
|
-
ok: "
|
|
65
|
-
flag: r"C:\\Users\\me\\.claude-editor\\jobs\\x"
|
|
119
|
+
ok: "the `.claude/worktrees/` manifest key" -> None
|
|
120
|
+
flag: r"C:\\Users\\me\\.claude-editor\\jobs\\x" -> ".claude-editor/jobs/"
|
|
121
|
+
flag: "see .claude/worktrees/wt-1/notes.md" -> ".claude/worktrees/"
|
|
122
|
+
flag: "saved to %TEMP%\\out.txt" -> "%temp%"
|
|
66
123
|
|
|
67
|
-
|
|
68
|
-
|
|
124
|
+
A dot-relative marker with neither a slash before it nor a path segment
|
|
125
|
+
after it — start of text naming the directory, a quoted config constant,
|
|
126
|
+
or a placeholder form such as ``.claude/worktrees/<name>`` — posts cleanly.
|
|
69
127
|
|
|
70
128
|
Args:
|
|
71
129
|
text: The post body text to scan.
|
|
@@ -73,8 +131,11 @@ def scan_text_for_volatile_marker(text: str) -> str | None:
|
|
|
73
131
|
Returns:
|
|
74
132
|
The matched marker string, or None when the text names no volatile path.
|
|
75
133
|
"""
|
|
76
|
-
normalized_text = text.replace("\\",
|
|
77
|
-
for each_marker in
|
|
134
|
+
normalized_text = text.replace("\\", PATH_ANCHOR_CHARACTER).lower()
|
|
135
|
+
for each_marker in ALL_PATH_ANCHORED_VOLATILE_PATH_MARKERS:
|
|
136
|
+
if _text_has_anchored_marker(normalized_text, each_marker):
|
|
137
|
+
return each_marker
|
|
138
|
+
for each_marker in ALL_BARE_VOLATILE_PATH_MARKERS:
|
|
78
139
|
if each_marker in normalized_text:
|
|
79
140
|
return each_marker
|
|
80
141
|
return None
|
|
@@ -49,6 +49,12 @@ NO_PARSEABLE_STDIN_LINES_MESSAGE: str = (
|
|
|
49
49
|
)
|
|
50
50
|
NO_PARSEABLE_STDIN_LINES_SENTINEL: str = "__no_parseable_stdin_lines__"
|
|
51
51
|
LOCAL_BRANCH_REFERENCE_PREFIX: str = "refs/heads/"
|
|
52
|
+
CODE_REVIEW_PUSH_GATE_PATH_OVERRIDE_ENV_VAR: str = "CODE_REVIEW_PUSH_GATE_PATH"
|
|
53
|
+
CODE_REVIEW_PUSH_GATE_MODULE_NAME: str = "code_review_push_gate"
|
|
54
|
+
CODE_REVIEW_PUSH_GATE_SCRIPT_FILENAME: str = "code_review_push_gate.py"
|
|
55
|
+
CODE_REVIEW_DENY_REASON_FUNCTION_NAME: str = "deny_reason_for_directory"
|
|
56
|
+
BLOCKING_DIRECTORY_NAME: str = "blocking"
|
|
57
|
+
CODE_REVIEW_STAMP_BLOCK_EXIT_CODE: int = 1
|
|
52
58
|
ALL_PROTECTED_BRANCH_PUSH_NAMES: tuple[str, ...] = ("main", "master")
|
|
53
59
|
PROTECTED_BRANCH_PUSH_BLOCK_EXIT_CODE: int = 1
|
|
54
60
|
PROTECTED_BRANCH_PUSH_BLOCK_MESSAGE: str = (
|
|
@@ -30,6 +30,8 @@ Exit codes:
|
|
|
30
30
|
|
|
31
31
|
from __future__ import annotations
|
|
32
32
|
|
|
33
|
+
import importlib.util
|
|
34
|
+
import os
|
|
33
35
|
import subprocess
|
|
34
36
|
import sys
|
|
35
37
|
from pathlib import Path
|
|
@@ -38,6 +40,12 @@ from git_hooks_constants import (
|
|
|
38
40
|
ALL_PROTECTED_BRANCH_PUSH_NAMES,
|
|
39
41
|
ALL_ZEROS_OBJECT_NAME_CHARACTER,
|
|
40
42
|
BASE_REFERENCE_ARGUMENT,
|
|
43
|
+
BLOCKING_DIRECTORY_NAME,
|
|
44
|
+
CODE_REVIEW_DENY_REASON_FUNCTION_NAME,
|
|
45
|
+
CODE_REVIEW_PUSH_GATE_MODULE_NAME,
|
|
46
|
+
CODE_REVIEW_PUSH_GATE_PATH_OVERRIDE_ENV_VAR,
|
|
47
|
+
CODE_REVIEW_PUSH_GATE_SCRIPT_FILENAME,
|
|
48
|
+
CODE_REVIEW_STAMP_BLOCK_EXIT_CODE,
|
|
41
49
|
DEFAULT_REMOTE_BASE_REFERENCE,
|
|
42
50
|
GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE,
|
|
43
51
|
INVOKE_GATE_FAILURE_MESSAGE,
|
|
@@ -158,6 +166,82 @@ def invoke_gate(gate_script_path: Path, base_reference: str) -> int:
|
|
|
158
166
|
return completion.returncode
|
|
159
167
|
|
|
160
168
|
|
|
169
|
+
def resolve_code_review_gate_path() -> tuple[Path, Path | None]:
|
|
170
|
+
"""Return the code-review push gate script path and its exact-allow override.
|
|
171
|
+
|
|
172
|
+
The override environment variable, when set, names the only path the trust
|
|
173
|
+
check accepts; otherwise the gate script resolves to the ``blocking``
|
|
174
|
+
directory beside this hook's ``git-hooks`` directory, which holds in both
|
|
175
|
+
the repository layout and the installed ``~/.claude/hooks`` layout.
|
|
176
|
+
|
|
177
|
+
Returns:
|
|
178
|
+
A pair of the resolved gate script path and either the exact override
|
|
179
|
+
path (override set) or None (trust-root case).
|
|
180
|
+
"""
|
|
181
|
+
override_path_raw = os.environ.get(
|
|
182
|
+
CODE_REVIEW_PUSH_GATE_PATH_OVERRIDE_ENV_VAR, ""
|
|
183
|
+
).strip()
|
|
184
|
+
if override_path_raw:
|
|
185
|
+
exact_override = Path(override_path_raw).resolve()
|
|
186
|
+
return exact_override, exact_override
|
|
187
|
+
blocking_directory = Path(__file__).resolve().parent.parent / BLOCKING_DIRECTORY_NAME
|
|
188
|
+
return blocking_directory / CODE_REVIEW_PUSH_GATE_SCRIPT_FILENAME, None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def load_code_review_deny_reason(gate_script_path: Path, work_tree_directory: str) -> str | None:
|
|
192
|
+
"""Import the gate script and read its push deny reason for a work tree.
|
|
193
|
+
|
|
194
|
+
Any import or evaluation failure reads as no deny reason.
|
|
195
|
+
|
|
196
|
+
Args:
|
|
197
|
+
gate_script_path: The resolved code-review push gate script path.
|
|
198
|
+
work_tree_directory: The work tree the push targets.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
The gate's deny reason when the surface lacks a covering low stamp;
|
|
202
|
+
None when the gate allows the push or could not be evaluated.
|
|
203
|
+
"""
|
|
204
|
+
try:
|
|
205
|
+
gate_specification = importlib.util.spec_from_file_location(
|
|
206
|
+
CODE_REVIEW_PUSH_GATE_MODULE_NAME, str(gate_script_path)
|
|
207
|
+
)
|
|
208
|
+
if gate_specification is None or gate_specification.loader is None:
|
|
209
|
+
return None
|
|
210
|
+
gate_module = importlib.util.module_from_spec(gate_specification)
|
|
211
|
+
gate_specification.loader.exec_module(gate_module)
|
|
212
|
+
deny_reason_function = getattr(gate_module, CODE_REVIEW_DENY_REASON_FUNCTION_NAME, None)
|
|
213
|
+
if deny_reason_function is None:
|
|
214
|
+
return None
|
|
215
|
+
deny_reason = deny_reason_function(work_tree_directory)
|
|
216
|
+
except Exception:
|
|
217
|
+
return None
|
|
218
|
+
if isinstance(deny_reason, str) and deny_reason:
|
|
219
|
+
return deny_reason
|
|
220
|
+
return None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def code_review_stamp_block_exit_code() -> int:
|
|
224
|
+
"""Block the push when no clean low code-review stamp covers the surface.
|
|
225
|
+
|
|
226
|
+
Runs the gate decision only when the gate script sits at a trusted
|
|
227
|
+
installed location (or the exact override path). When the gate is absent
|
|
228
|
+
from a trusted location, the check is skipped and the push is allowed,
|
|
229
|
+
matching the CODE_RULES gate's fail-open posture.
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
The block exit code when a covering low stamp is missing; 0 when the
|
|
233
|
+
surface is covered or the gate is not installed at a trusted location.
|
|
234
|
+
"""
|
|
235
|
+
gate_script_path, exact_allowed_path = resolve_code_review_gate_path()
|
|
236
|
+
if not is_safe_regular_file(gate_script_path, exact_allowed_path):
|
|
237
|
+
return 0
|
|
238
|
+
deny_reason = load_code_review_deny_reason(gate_script_path, os.getcwd())
|
|
239
|
+
if deny_reason is None:
|
|
240
|
+
return 0
|
|
241
|
+
sys.stderr.write(deny_reason + "\n")
|
|
242
|
+
return CODE_REVIEW_STAMP_BLOCK_EXIT_CODE
|
|
243
|
+
|
|
244
|
+
|
|
161
245
|
def main() -> int:
|
|
162
246
|
stdin_read_failure_message = STDIN_READ_FAILURE_MESSAGE
|
|
163
247
|
gate_infrastructure_failure_exit_code = GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE
|
|
@@ -191,14 +275,17 @@ def main() -> int:
|
|
|
191
275
|
pre_push_gate_script_not_found_message.format(path=gate_script_path),
|
|
192
276
|
file=sys.stderr,
|
|
193
277
|
)
|
|
194
|
-
return
|
|
278
|
+
return code_review_stamp_block_exit_code()
|
|
195
279
|
base_reference = resolve_base_reference_from_stdin(stdin_text)
|
|
196
280
|
if base_reference is None:
|
|
197
281
|
return 0
|
|
198
282
|
if base_reference == no_parseable_stdin_lines_sentinel:
|
|
199
283
|
print(no_parseable_stdin_lines_message, file=sys.stderr)
|
|
200
284
|
return gate_infrastructure_failure_exit_code
|
|
201
|
-
|
|
285
|
+
code_rules_exit_code = invoke_gate(gate_script_path, base_reference)
|
|
286
|
+
if code_rules_exit_code != 0:
|
|
287
|
+
return code_rules_exit_code
|
|
288
|
+
return code_review_stamp_block_exit_code()
|
|
202
289
|
|
|
203
290
|
|
|
204
291
|
if __name__ == "__main__":
|
|
@@ -28,6 +28,12 @@ NON_ZERO_REMOTE_SHA_ONE: str = "1" * 40
|
|
|
28
28
|
NON_ZERO_REMOTE_SHA_TWO: str = "2" * 40
|
|
29
29
|
|
|
30
30
|
|
|
31
|
+
def _isolate_code_review_gate(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
32
|
+
monkeypatch.setenv(
|
|
33
|
+
"CODE_REVIEW_PUSH_GATE_PATH", str(tmp_path / "no_code_review_gate.py")
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
31
37
|
def test_resolve_base_reference_uses_remote_object_when_non_zero() -> None:
|
|
32
38
|
stdin_text = (
|
|
33
39
|
f"refs/heads/feature {NON_ZERO_LOCAL_SHA} refs/heads/feature {NON_ZERO_REMOTE_SHA_ONE}\n"
|
|
@@ -73,6 +79,7 @@ def test_main_exits_zero_when_gate_script_missing(
|
|
|
73
79
|
"CODE_RULES_GATE_PATH",
|
|
74
80
|
str(tmp_path / "does_not_exist.py"),
|
|
75
81
|
)
|
|
82
|
+
_isolate_code_review_gate(tmp_path, monkeypatch)
|
|
76
83
|
monkeypatch.setattr(sys, "stdin", io.StringIO(""))
|
|
77
84
|
|
|
78
85
|
exit_code = pre_push.main()
|
|
@@ -94,6 +101,7 @@ def test_main_invokes_gate_with_resolved_base_reference(
|
|
|
94
101
|
encoding="utf-8",
|
|
95
102
|
)
|
|
96
103
|
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(recording_gate_script_path))
|
|
104
|
+
_isolate_code_review_gate(tmp_path, monkeypatch)
|
|
97
105
|
remote_sha = "9" * 40
|
|
98
106
|
monkeypatch.setattr(
|
|
99
107
|
sys,
|
|
@@ -312,6 +320,7 @@ def test_invoke_gate_uses_resolved_path(
|
|
|
312
320
|
symlink_gate_path.symlink_to(real_gate_path)
|
|
313
321
|
resolved_path = symlink_gate_path.resolve()
|
|
314
322
|
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(symlink_gate_path))
|
|
323
|
+
_isolate_code_review_gate(tmp_path, monkeypatch)
|
|
315
324
|
monkeypatch.setattr(sys, "stdin", io.StringIO(
|
|
316
325
|
f"refs/heads/feature {NON_ZERO_LOCAL_SHA} refs/heads/feature {NON_ZERO_REMOTE_SHA_ONE}\n"
|
|
317
326
|
))
|
|
@@ -428,6 +437,7 @@ def test_main_allows_main_onto_main_push(
|
|
|
428
437
|
passing_gate_path = tmp_path / "gate.py"
|
|
429
438
|
passing_gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
|
|
430
439
|
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(passing_gate_path))
|
|
440
|
+
_isolate_code_review_gate(tmp_path, monkeypatch)
|
|
431
441
|
monkeypatch.setattr(
|
|
432
442
|
sys,
|
|
433
443
|
"stdin",
|
|
@@ -439,3 +449,121 @@ def test_main_allows_main_onto_main_push(
|
|
|
439
449
|
exit_code = pre_push.main()
|
|
440
450
|
|
|
441
451
|
assert exit_code == 0
|
|
452
|
+
|
|
453
|
+
|
|
454
|
+
CODE_REVIEW_STUB_BLOCK_REASON: str = "CODE_REVIEW_STUB_BLOCK_REASON"
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _write_passing_code_rules_gate(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
|
|
458
|
+
passing_gate_path = tmp_path / "code_rules_gate.py"
|
|
459
|
+
passing_gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
|
|
460
|
+
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(passing_gate_path))
|
|
461
|
+
monkeypatch.setattr(
|
|
462
|
+
sys,
|
|
463
|
+
"stdin",
|
|
464
|
+
io.StringIO(
|
|
465
|
+
f"refs/heads/feature {NON_ZERO_LOCAL_SHA} refs/heads/feature {NON_ZERO_REMOTE_SHA_ONE}\n"
|
|
466
|
+
),
|
|
467
|
+
)
|
|
468
|
+
|
|
469
|
+
|
|
470
|
+
def _write_code_review_gate_stub(tmp_path: Path, returned_reason_literal: str) -> Path:
|
|
471
|
+
stub_gate_path = tmp_path / "code_review_push_gate.py"
|
|
472
|
+
stub_gate_path.write_text(
|
|
473
|
+
"def deny_reason_for_directory(target_directory):\n"
|
|
474
|
+
f" return {returned_reason_literal}\n",
|
|
475
|
+
encoding="utf-8",
|
|
476
|
+
)
|
|
477
|
+
return stub_gate_path
|
|
478
|
+
|
|
479
|
+
|
|
480
|
+
def test_main_blocks_when_code_review_gate_returns_reason(
|
|
481
|
+
tmp_path: Path,
|
|
482
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
483
|
+
capsys: pytest.CaptureFixture[str],
|
|
484
|
+
) -> None:
|
|
485
|
+
_write_passing_code_rules_gate(tmp_path, monkeypatch)
|
|
486
|
+
stub_gate_path = _write_code_review_gate_stub(
|
|
487
|
+
tmp_path, repr(CODE_REVIEW_STUB_BLOCK_REASON)
|
|
488
|
+
)
|
|
489
|
+
monkeypatch.setenv("CODE_REVIEW_PUSH_GATE_PATH", str(stub_gate_path))
|
|
490
|
+
|
|
491
|
+
exit_code = pre_push.main()
|
|
492
|
+
|
|
493
|
+
assert exit_code == git_hooks_constants.CODE_REVIEW_STAMP_BLOCK_EXIT_CODE
|
|
494
|
+
captured = capsys.readouterr()
|
|
495
|
+
assert CODE_REVIEW_STUB_BLOCK_REASON in captured.err
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def test_main_allows_when_code_review_gate_returns_none(
|
|
499
|
+
tmp_path: Path,
|
|
500
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
501
|
+
) -> None:
|
|
502
|
+
_write_passing_code_rules_gate(tmp_path, monkeypatch)
|
|
503
|
+
stub_gate_path = _write_code_review_gate_stub(tmp_path, "None")
|
|
504
|
+
monkeypatch.setenv("CODE_REVIEW_PUSH_GATE_PATH", str(stub_gate_path))
|
|
505
|
+
|
|
506
|
+
exit_code = pre_push.main()
|
|
507
|
+
|
|
508
|
+
assert exit_code == 0
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def test_main_code_review_check_fails_open_when_gate_module_absent(
|
|
512
|
+
tmp_path: Path,
|
|
513
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
514
|
+
) -> None:
|
|
515
|
+
_write_passing_code_rules_gate(tmp_path, monkeypatch)
|
|
516
|
+
monkeypatch.setenv(
|
|
517
|
+
"CODE_REVIEW_PUSH_GATE_PATH", str(tmp_path / "does_not_exist.py")
|
|
518
|
+
)
|
|
519
|
+
|
|
520
|
+
exit_code = pre_push.main()
|
|
521
|
+
|
|
522
|
+
assert exit_code == 0
|
|
523
|
+
|
|
524
|
+
|
|
525
|
+
def test_main_allows_deletion_push_even_when_code_review_gate_would_block(
|
|
526
|
+
tmp_path: Path,
|
|
527
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
528
|
+
) -> None:
|
|
529
|
+
passing_gate_path = tmp_path / "code_rules_gate.py"
|
|
530
|
+
passing_gate_path.write_text("import sys\nsys.exit(0)\n", encoding="utf-8")
|
|
531
|
+
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(passing_gate_path))
|
|
532
|
+
stub_gate_path = _write_code_review_gate_stub(
|
|
533
|
+
tmp_path, repr(CODE_REVIEW_STUB_BLOCK_REASON)
|
|
534
|
+
)
|
|
535
|
+
monkeypatch.setenv("CODE_REVIEW_PUSH_GATE_PATH", str(stub_gate_path))
|
|
536
|
+
deletion_stdin = (
|
|
537
|
+
f"refs/heads/feature {ALL_ZEROS_OBJECT_NAME} "
|
|
538
|
+
f"refs/heads/feature {ALL_ZEROS_OBJECT_NAME}\n"
|
|
539
|
+
)
|
|
540
|
+
monkeypatch.setattr(sys, "stdin", io.StringIO(deletion_stdin))
|
|
541
|
+
|
|
542
|
+
exit_code = pre_push.main()
|
|
543
|
+
|
|
544
|
+
assert exit_code == 0
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def test_main_allows_when_code_review_enforcement_flag_is_off(
|
|
548
|
+
tmp_path: Path,
|
|
549
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
550
|
+
) -> None:
|
|
551
|
+
"""Native pre-push honors the real gate with enforcement off.
|
|
552
|
+
|
|
553
|
+
Points at the production push gate (not a reason stub). With the shipped
|
|
554
|
+
default `CODE_REVIEW_ENFORCEMENT_ENABLED = False`, the gate returns no
|
|
555
|
+
deny reason and the backstop allows the push — one path covering flag,
|
|
556
|
+
real gate load, and pre-push exit together.
|
|
557
|
+
"""
|
|
558
|
+
_write_passing_code_rules_gate(tmp_path, monkeypatch)
|
|
559
|
+
real_gate_path = (
|
|
560
|
+
Path(__file__).resolve().parent.parent
|
|
561
|
+
/ "blocking"
|
|
562
|
+
/ "code_review_push_gate.py"
|
|
563
|
+
)
|
|
564
|
+
assert real_gate_path.is_file()
|
|
565
|
+
monkeypatch.setenv("CODE_REVIEW_PUSH_GATE_PATH", str(real_gate_path))
|
|
566
|
+
|
|
567
|
+
exit_code = pre_push.main()
|
|
568
|
+
|
|
569
|
+
assert exit_code == 0
|
package/hooks/hooks.json
CHANGED
|
@@ -57,6 +57,16 @@
|
|
|
57
57
|
}
|
|
58
58
|
]
|
|
59
59
|
},
|
|
60
|
+
{
|
|
61
|
+
"matcher": "mcp__plugin_github_github__create_pull_request",
|
|
62
|
+
"hooks": [
|
|
63
|
+
{
|
|
64
|
+
"type": "command",
|
|
65
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/code_review_pr_create_gate.py",
|
|
66
|
+
"timeout": 20
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
},
|
|
60
70
|
{
|
|
61
71
|
"matcher": "mcp__plugin_github_github__.*",
|
|
62
72
|
"hooks": [
|
|
@@ -79,7 +89,12 @@
|
|
|
79
89
|
"type": "command",
|
|
80
90
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/pr_converge_bugteam_enforcer.py",
|
|
81
91
|
"timeout": 10
|
|
82
|
-
}
|
|
92
|
+
}
|
|
93
|
+
]
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
"matcher": "Agent|Task",
|
|
97
|
+
"hooks": [
|
|
83
98
|
{
|
|
84
99
|
"type": "command",
|
|
85
100
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/code_verifier_spawn_preflight_gate.py",
|
|
@@ -87,6 +102,16 @@
|
|
|
87
102
|
}
|
|
88
103
|
]
|
|
89
104
|
},
|
|
105
|
+
{
|
|
106
|
+
"matcher": "ScheduleWakeup|CronCreate",
|
|
107
|
+
"hooks": [
|
|
108
|
+
{
|
|
109
|
+
"type": "command",
|
|
110
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/orchestrator_refresh_reschedule_gate.py",
|
|
111
|
+
"timeout": 15
|
|
112
|
+
}
|
|
113
|
+
]
|
|
114
|
+
},
|
|
90
115
|
{
|
|
91
116
|
"matcher": "Skill",
|
|
92
117
|
"hooks": [
|
|
@@ -60,9 +60,11 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
60
60
|
| `pre_tool_use_stdin.py` | `read_hook_input_dictionary_from_stdin()` — shared stdin parser for PreToolUse hooks |
|
|
61
61
|
| `precommit_code_rules_gate_constants.py` | Scope argument and exit-code constants for the precommit gate |
|
|
62
62
|
| `project_paths_reader.py` | Loads `~/.claude/project-paths.json` — the per-user project-path registry |
|
|
63
|
+
| `pyproject_config_discovery_constants.py` | Table names (``tool`` key, ``mypy``, ``ruff``) for resolving a validator's config from a pyproject.toml `[tool.<name>]` table |
|
|
63
64
|
| `pytest_testpaths_orphan_blocker_constants.py` | Marker filename, section and key names, test-file pattern, search budget, and block-message text for the pytest unregistered-test-directory blocker |
|
|
64
65
|
| `python_style_checks_constants.py` | Command-line argument count and blank-line count between top-level functions for the style validator |
|
|
65
66
|
| `reviewer_spawn_gate_constants.py` | Bash tool name, the sentinel marker, the Copilot and Bugbot trigger markers, the availability-script relative path and override env-var name, and the deny-message template for the reviewer-spawn gate |
|
|
67
|
+
| `ruff_integration_constants.py` | ``NO_COLOR`` / ``FORCE_COLOR`` environment variable names for plain ruff diagnostics |
|
|
66
68
|
| `send_user_file_open_locally_blocker_constants.py` | Tool name, proactive status, and the block message for the open-locally attach blocker |
|
|
67
69
|
| `sensitive_file_protector_constants.py` | Sensitive filename patterns, the committed-template suffixes that earn an exemption, the write/edit tool names, and the deny decision and message template for `sensitive_file_protector` |
|
|
68
70
|
| `session_edit_stage_gate_constants.py` | Tracker filename prefix/suffix, JSON payload key, edit tool name set, session-id sanitize pattern, lock filename suffix and lock-acquire timing, git diff command, commit flag escapes, and deny-message template shared by the session edit stage gate trio |
|
|
@@ -89,6 +91,6 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
89
91
|
- Most files in this package are pure constants modules — no side effects, no I/O.
|
|
90
92
|
- Hooks import from this package with `from hooks_constants.<module> import <CONSTANT>`.
|
|
91
93
|
- Tests for these modules live beside them as `test_<module>.py`. Run with `python -m pytest hooks_constants/test_<name>.py`.
|
|
92
|
-
- `dynamic_stderr_handler.py`, `pre_tool_use_stdin.py`, `multi_edit_reconstruction.py`, `hosted_hook_runner.py`, and `text_stripping.py` are utility modules (not pure constants) but live here because they
|
|
94
|
+
- `dynamic_stderr_handler.py`, `pre_tool_use_stdin.py`, `multi_edit_reconstruction.py`, `hosted_hook_runner.py`, and `text_stripping.py` are utility modules (not pure constants) but live here because they hold logic shared across hook surfaces.
|
|
93
95
|
- `local_identity.py` is a loader: it reads the environment or `~/.claude/local-identity.json` (its path overridable via `CLAUDE_LOCAL_IDENTITY_PATH`) to resolve private NAS values (with committed placeholder defaults), the PII commit-scan exempt-repo slug set (`CLAUDE_PII_EXEMPT_REPOS` / `pii_exempt_repositories`), and the per-repository allowlisted-values mapping (`pii_allowlisted_values`).
|
|
94
96
|
|
|
@@ -75,4 +75,12 @@ ALL_BASH_HOSTED_HOOK_ENTRIES: tuple[BashHostedHookEntry, ...] = (
|
|
|
75
75
|
BashHostedHookEntry(
|
|
76
76
|
"blocking/verdict_directory_write_blocker.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES
|
|
77
77
|
),
|
|
78
|
+
BashHostedHookEntry("blocking/code_review_push_gate.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES),
|
|
79
|
+
BashHostedHookEntry(
|
|
80
|
+
"blocking/code_review_pr_create_gate.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES
|
|
81
|
+
),
|
|
82
|
+
BashHostedHookEntry(
|
|
83
|
+
"blocking/code_review_stamp_directory_write_blocker.py",
|
|
84
|
+
ALL_BASH_AND_POWERSHELL_TOOL_NAMES,
|
|
85
|
+
),
|
|
78
86
|
)
|
|
@@ -22,6 +22,7 @@ ALL_CONFIG_DIRECTORY_NAMES = frozenset(
|
|
|
22
22
|
"pr_converge_scripts_constants",
|
|
23
23
|
"bugteam_scripts_constants",
|
|
24
24
|
"usage_pause_constants",
|
|
25
|
+
"prototype_scripts_constants",
|
|
25
26
|
"codex_review_scripts_constants",
|
|
26
27
|
"dev_env_scripts_constants",
|
|
27
28
|
"fresh_branch_scripts_constants",
|
|
@@ -1,21 +1,29 @@
|
|
|
1
1
|
"""Configuration constants for the code_verifier_spawn_preflight_gate hook.
|
|
2
2
|
|
|
3
|
-
The gate denies an ``Agent`` spawn whose ``subagent_type`` is
|
|
4
|
-
when the branch carries a merge conflict against its base
|
|
5
|
-
violation on a line added in the working tree since the merge
|
|
6
|
-
the branch or uncommitted)
|
|
7
|
-
pre-flight checks before the expensive
|
|
8
|
-
deny reason to the spawning agent so that
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
3
|
+
The gate denies an ``Agent`` or ``Task`` spawn whose ``subagent_type`` is
|
|
4
|
+
``code-verifier`` when the branch carries a merge conflict against its base
|
|
5
|
+
ref, a CODE_RULES violation on a line added in the working tree since the merge
|
|
6
|
+
base (committed on the branch or uncommitted), or a CODE_RULES engine
|
|
7
|
+
import/load failure. It runs those pre-flight checks before the expensive
|
|
8
|
+
verification spawn and addresses its deny reason to the spawning agent so that
|
|
9
|
+
agent fixes the named issues and re-spawns. Environmental git failures stay
|
|
10
|
+
fail-open; engine import/load failure is fail-closed. Every literal the hook
|
|
11
|
+
body reads lives here; ``AGENT_TOOL_NAME`` comes from
|
|
12
|
+
``pr_converge_bugteam_enforcer_constants`` and joins ``TASK_TOOL_NAME`` in
|
|
13
|
+
``ALL_CODE_VERIFIER_SPAWN_TOOL_NAMES`` so both spawn tool names share one source.
|
|
12
14
|
"""
|
|
13
15
|
|
|
14
16
|
from __future__ import annotations
|
|
15
17
|
|
|
16
18
|
from pathlib import Path
|
|
17
19
|
|
|
20
|
+
from hooks_constants.pr_converge_bugteam_enforcer_constants import AGENT_TOOL_NAME
|
|
21
|
+
|
|
18
22
|
CODE_VERIFIER_SUBAGENT_TYPE: str = "code-verifier"
|
|
23
|
+
TASK_TOOL_NAME: str = "Task"
|
|
24
|
+
ALL_CODE_VERIFIER_SPAWN_TOOL_NAMES: frozenset[str] = frozenset(
|
|
25
|
+
{AGENT_TOOL_NAME, TASK_TOOL_NAME}
|
|
26
|
+
)
|
|
19
27
|
|
|
20
28
|
ALL_MERGE_TREE_COMMAND_FLAGS: tuple[str, ...] = (
|
|
21
29
|
"merge-tree",
|
|
@@ -39,11 +47,18 @@ ALL_NAME_ONLY_WORKTREE_DIFF_FLAGS: tuple[str, ...] = (
|
|
|
39
47
|
ALL_UNIFIED_ZERO_DIFF_FLAGS: tuple[str, ...] = ("diff", "--unified=0")
|
|
40
48
|
|
|
41
49
|
DENY_REASON_LEAD: str = (
|
|
42
|
-
"BLOCKED [code-verifier-spawn-preflight]:
|
|
43
|
-
"code-verifier is blocked because
|
|
50
|
+
"BLOCKED [code-verifier-spawn-preflight]: a code-verifier spawn "
|
|
51
|
+
"(Agent or Task tool, subagent_type code-verifier) is blocked because "
|
|
52
|
+
"the branch is not in a committable state. "
|
|
44
53
|
"Fix these, then re-spawn the code-verifier:"
|
|
45
54
|
)
|
|
46
55
|
MERGE_CONFLICT_SECTION_HEADER: str = "Merge conflicts vs {base_ref}:"
|
|
47
56
|
CODE_RULES_SECTION_HEADER: str = "CODE_RULES violations on changed lines:"
|
|
57
|
+
ENGINE_LOAD_FAILURE_SECTION: str = (
|
|
58
|
+
"CODE_RULES engine failed to load:\n"
|
|
59
|
+
" The CODE_RULES validate_content engine could not be imported or loaded "
|
|
60
|
+
"(package import/load failure). Repair the enforcer so load_validate_content "
|
|
61
|
+
"succeeds, then re-spawn code-verifier."
|
|
62
|
+
)
|
|
48
63
|
|
|
49
64
|
GATE_SCRIPTS_RELATIVE_PATH: Path = Path("_shared") / "pr-loop" / "scripts"
|
|
@@ -25,12 +25,29 @@ chained command does not bind the gate to the wrong PR::
|
|
|
25
25
|
|
|
26
26
|
GH_PR_READY_ANCHOR_PATTERN: str = r"\bgh\s+pr\s+ready\b(?![^&|;\n]*--undo)"
|
|
27
27
|
|
|
28
|
-
COMMAND_SEPARATOR_PATTERN: str = r"&&|\|\||;|\||\n"
|
|
28
|
+
COMMAND_SEPARATOR_PATTERN: str = r"&&|\|\||;|\||\n|(?<![>&])&(?![>&])"
|
|
29
|
+
|
|
30
|
+
BASH_LINE_CONTINUATION_PATTERN: str = r"\\\r?\n[ \t]*"
|
|
29
31
|
|
|
30
32
|
PR_URL_OWNER_REPO_NUMBER_PATTERN: str = (
|
|
31
|
-
r"https
|
|
33
|
+
r"https?://[^/]+/(?P<owner>[\w.-]+)/(?P<repo>[\w.-]+)/pull/(?P<number>\d+)"
|
|
32
34
|
)
|
|
33
35
|
|
|
34
36
|
REPO_OVERRIDE_FLAG_PATTERN: str = (
|
|
35
|
-
r"(?:^|\s)(?:--repo
|
|
37
|
+
r"(?:^|\s)(?:--repo(?:=|\s+)|-R(?:=|\s+)?)"
|
|
38
|
+
r"(?:https?://[^/\s]+/|git@[^:\s]+:|(?:[\w.-]+/)*)"
|
|
39
|
+
r"(?P<owner>[\w.-]+)/(?P<repo>[\w.-]+)"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
ALL_GH_PR_VIEW_NUMBER_COMMAND: tuple[str, ...] = (
|
|
43
|
+
"gh",
|
|
44
|
+
"pr",
|
|
45
|
+
"view",
|
|
46
|
+
"--json",
|
|
47
|
+
"number",
|
|
48
|
+
"--jq",
|
|
49
|
+
".number",
|
|
36
50
|
)
|
|
51
|
+
GH_REPO_FLAG: str = "--repo"
|
|
52
|
+
REPO_SLUG_TEMPLATE: str = "{owner}/{repo}"
|
|
53
|
+
GIT_URL_SUFFIX: str = ".git"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Segment-splitting
|
|
1
|
+
"""Segment-splitting, command-name, and deny-mode constants for the destructive command blocker."""
|
|
2
2
|
|
|
3
3
|
ALL_SHELL_CONTROL_OPERATOR_TOKENS: frozenset[str] = frozenset({"&&", "||", ";", "|&", "|", "&", "\n", "\r"})
|
|
4
4
|
ALL_COMMAND_LAUNCHER_WRAPPER_COMMANDS: frozenset[str] = frozenset(
|
|
@@ -191,3 +191,5 @@ ALL_KNOWN_TEMPORARY_ENVIRONMENT_VARIABLE_NAMES: frozenset[str] = frozenset(
|
|
|
191
191
|
"CLAUDE_JOB_DIR",
|
|
192
192
|
}
|
|
193
193
|
)
|
|
194
|
+
DESTRUCTIVE_DENY_MODE_ENV_VAR: str = "CLAUDE_DESTRUCTIVE_DENY_MODE"
|
|
195
|
+
ALL_TRUTHY_ENV_VALUES: frozenset[str] = frozenset({"1", "true", "yes", "on"})
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
"""Configuration constants for the ``enter_worktree_origin_prefetch`` hook.
|
|
2
|
-
|
|
3
|
-
The hook keeps a freshly created worktree from starting on a stale cached
|
|
4
|
-
``refs/remotes/origin/<default-branch>`` value: it fetches that ref right
|
|
5
|
-
before ``EnterWorktree`` resolves its base.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
ENTER_WORKTREE_TOOL_NAME: str = "EnterWorktree"
|
|
11
|
-
ENTER_WORKTREE_PATH_INPUT_KEY: str = "path"
|
|
12
|
-
|
|
13
|
-
ORIGIN_REMOTE_NAME: str = "origin"
|
|
14
|
-
ORIGIN_HEAD_SYMBOLIC_REF: str = "refs/remotes/origin/HEAD"
|
|
15
|
-
ORIGIN_REMOTE_REF_PREFIX: str = "refs/remotes/origin/"
|
|
16
|
-
|
|
17
|
-
GIT_SYMBOLIC_REF_TIMEOUT_SECONDS: int = 5
|
|
18
|
-
GIT_FETCH_TIMEOUT_SECONDS: int = 15
|
|
1
|
+
"""Configuration constants for the ``enter_worktree_origin_prefetch`` hook.
|
|
2
|
+
|
|
3
|
+
The hook keeps a freshly created worktree from starting on a stale cached
|
|
4
|
+
``refs/remotes/origin/<default-branch>`` value: it fetches that ref right
|
|
5
|
+
before ``EnterWorktree`` resolves its base.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
ENTER_WORKTREE_TOOL_NAME: str = "EnterWorktree"
|
|
11
|
+
ENTER_WORKTREE_PATH_INPUT_KEY: str = "path"
|
|
12
|
+
|
|
13
|
+
ORIGIN_REMOTE_NAME: str = "origin"
|
|
14
|
+
ORIGIN_HEAD_SYMBOLIC_REF: str = "refs/remotes/origin/HEAD"
|
|
15
|
+
ORIGIN_REMOTE_REF_PREFIX: str = "refs/remotes/origin/"
|
|
16
|
+
|
|
17
|
+
GIT_SYMBOLIC_REF_TIMEOUT_SECONDS: int = 5
|
|
18
|
+
GIT_FETCH_TIMEOUT_SECONDS: int = 15
|