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
|
@@ -1,155 +1,160 @@
|
|
|
1
|
-
"""Constants for the verified-commit gate hook family.
|
|
2
|
-
|
|
3
|
-
Shared by ``verification_verdict_store.py``, ``verified_commit_gate.py``,
|
|
4
|
-
and ``verifier_verdict_minter.py`` so every tunable lives in one place.
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
from __future__ import annotations
|
|
8
|
-
|
|
9
|
-
GIT_TIMEOUT_SECONDS = 30
|
|
10
|
-
ROOT_KEY_HEX_LENGTH = 16
|
|
11
|
-
VERDICT_JSON_INDENT = 2
|
|
12
|
-
CLAUDE_HOME_DIRECTORY_NAME = ".claude"
|
|
13
|
-
VERDICT_DIRECTORY_NAME = "verification"
|
|
14
|
-
VERDICT_DIRECTORY_NAME_SEPARATOR_PATTERN = r"['\"\\/,\s]+"
|
|
15
|
-
VERDICT_DIRECTORY_PATH_BOUNDARY_PATTERN = r"(?=['\"]*[\\/,])"
|
|
16
|
-
RELATIVE_VERDICT_DIRECTORY_PATTERN = r"(?:^|(?<=[\s;&|(='\"]))verification[\\/]"
|
|
17
|
-
VERDICT_PATH_GLUE_PATTERN = r"['\"+\\/\s]*[\\/]['\"+\\/\s]*"
|
|
18
|
-
VERDICT_DIRECTORY_CHANGE_TARGET_PATTERN = r"[ \t]+['\"]?verification[\\/]?['\"]?(?=[\s;&|]|$)"
|
|
19
|
-
VERDICT_PATH_JOINED_VARIABLE_PATTERN = r"\$\{?(\w+)\}?[\\/]|[\\/]\$\{?(\w+)\}?"
|
|
20
|
-
VERDICT_PATH_VARIABLE_ASSIGNMENT_PATTERN = r"(?:^|(?<=[\s;&|(]))%s=(\S+)"
|
|
21
|
-
VERDICT_FILE_RELATIVE_REFERENCE_PATTERN = (
|
|
22
|
-
rf"(?:^|(?<=[\s;&|(='\"\\/]))verification[\\/][0-9a-f]{{{ROOT_KEY_HEX_LENGTH}}}\.json"
|
|
23
|
-
)
|
|
24
|
-
PATH_OBFUSCATION_PRIMITIVE_PATTERN = (
|
|
25
|
-
r"chr\s*\(|bytes\.fromhex\s*\(|b64decode\s*\(|codecs\.decode\s*\("
|
|
26
|
-
r"|(?:bytes|bytearray)\s*\(\s*\[|\[char\[?\]?\]"
|
|
27
|
-
)
|
|
28
|
-
ALL_VERDICT_PATH_SEGMENT_NAMES = (".claude", "verification")
|
|
29
|
-
ALL_VERDICT_PATH_SEGMENT_BODIES = ("claude", "verification")
|
|
30
|
-
HEX_TOKEN_PATTERN = r"(?<![0-9a-fx])([0-9a-f]{6,})(?![0-9a-f])"
|
|
31
|
-
BASE64_TOKEN_PATTERN = r"[A-Za-z0-9+/]{8,}={0,2}"
|
|
32
|
-
CHARACTER_CODE_SEQUENCE_PATTERN = r"\d{1,3}(?:\s*,\s*\d{1,3})+"
|
|
33
|
-
CHR_CALL_CHAIN_PATTERN = r"chr\(\s*\d{1,3}\s*\)(?:\s*\+\s*chr\(\s*\d{1,3}\s*\))+"
|
|
34
|
-
CHR_CALL_CODE_PATTERN = r"chr\(\s*(\d{1,3})\s*\)"
|
|
35
|
-
HEX_DIGITS_PER_BYTE = 2
|
|
36
|
-
FILE_WRITE_PRIMITIVE_PATTERN = (
|
|
37
|
-
r"\bopen\s*\(|\.write_text\s*\(|\.write_bytes\s*\("
|
|
38
|
-
r"|Out-File|Set-Content|Add-Content|\btee\b|>"
|
|
39
|
-
)
|
|
40
|
-
NON_REDIRECT_FILE_WRITE_PRIMITIVE_PATTERN = (
|
|
41
|
-
r"\bopen\s*\(|\.write_text\s*\(|\.write_bytes\s*\("
|
|
42
|
-
r"|Out-File|Set-Content|Add-Content|\btee\b"
|
|
43
|
-
)
|
|
44
|
-
WRITE_CALL_REGION_PATTERN = (
|
|
45
|
-
r"(?:\bopen\s*\(|\.write_text\s*\(|\.write_bytes\s*\("
|
|
46
|
-
r"|Out-File|Set-Content|Add-Content|\btee\b)[^;&|\n]*"
|
|
47
|
-
)
|
|
48
|
-
VERDICT_KEY_ALL_PASS = "all_pass"
|
|
49
|
-
VERDICT_KEY_MANIFEST_SHA256 = "manifest_sha256"
|
|
50
|
-
VERDICT_KEY_FINDINGS = "findings"
|
|
51
|
-
VERDICT_FINDING_CHECK_KEY = "check"
|
|
52
|
-
LOGS_DIRECTORY_NAME = "logs"
|
|
53
|
-
VERIFIER_VERDICTS_JSONL_FILENAME = "verifier-verdicts.jsonl"
|
|
54
|
-
LEDGER_KEY_TIMESTAMP = "timestamp"
|
|
55
|
-
LEDGER_KEY_REPO_ROOT = "repo_root"
|
|
56
|
-
LEDGER_KEY_BRANCH = "branch"
|
|
57
|
-
LEDGER_KEY_MANIFEST_HASH = "manifest_hash"
|
|
58
|
-
LEDGER_KEY_ALL_PASS = "all_pass"
|
|
59
|
-
LEDGER_KEY_FINDING_COUNT = "finding_count"
|
|
60
|
-
LEDGER_KEY_FINDING_CHECK_NAMES = "finding_check_names"
|
|
61
|
-
LEDGER_KEY_MINTED_FROM_AGENT_ID = "minted_from_agent_id"
|
|
62
|
-
VERDICT_FILE_GLOB = "*.json"
|
|
63
|
-
SUBAGENTS_DIRECTORY_NAME = "subagents"
|
|
64
|
-
AGENT_TRANSCRIPT_GLOB = "agent-*.jsonl"
|
|
65
|
-
AGENT_META_SIDECAR_SUFFIX = ".meta.json"
|
|
66
|
-
AGENT_META_TYPE_KEY = "agentType"
|
|
67
|
-
TRANSCRIPT_ENTRY_TYPE_KEY = "type"
|
|
68
|
-
TRANSCRIPT_ASSISTANT_ENTRY_TYPE = "assistant"
|
|
69
|
-
TRANSCRIPT_MESSAGE_KEY = "message"
|
|
70
|
-
TRANSCRIPT_CONTENT_KEY = "content"
|
|
71
|
-
TRANSCRIPT_CONTENT_TYPE_KEY = "type"
|
|
72
|
-
TRANSCRIPT_TEXT_CONTENT_TYPE = "text"
|
|
73
|
-
TRANSCRIPT_TEXT_KEY = "text"
|
|
74
|
-
VERDICT_FENCE_PATTERN = r"```verdict\s*\n(.*?)```"
|
|
75
|
-
MANIFEST_HASH_CLI_FLAG = "--manifest-hash"
|
|
76
|
-
MANIFEST_HASH_FOR_BRANCH_CLI_FLAG = "--manifest-hash-for-branch"
|
|
77
|
-
WORKTREE_LIST_PATH_PREFIX = "worktree "
|
|
78
|
-
WORKTREE_LIST_BRANCH_PREFIX = "branch "
|
|
79
|
-
BRANCH_REFERENCE_PREFIX = "refs/heads/"
|
|
80
|
-
EMPTY_SURFACE_GUARD_MESSAGE = (
|
|
81
|
-
"ERROR: The work tree at {repo_root} has no changed or untracked files "
|
|
82
|
-
"versus origin/main (empty change surface). This work tree holds nothing "
|
|
83
|
-
"to verify — you are pointed at the wrong work tree. Run `git worktree list` "
|
|
84
|
-
"to see all checked-out work trees and target the one on the branch under review."
|
|
85
|
-
)
|
|
86
|
-
BRANCH_WORKTREE_ABSENT_MESSAGE = (
|
|
87
|
-
"ERROR: No work tree has branch '{branch}' checked out. "
|
|
88
|
-
"Check it out in a work tree first: "
|
|
89
|
-
"`git worktree add <path> <branch>` or `git checkout <branch>`."
|
|
90
|
-
)
|
|
91
|
-
DOCS_ONLY_EXTENSIONS = frozenset(
|
|
92
|
-
{".md", ".txt", ".rst", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".ico"}
|
|
93
|
-
)
|
|
94
|
-
PYTHON_EXTENSION = ".py"
|
|
95
|
-
TEST_FILE_PREFIX = "test_"
|
|
96
|
-
TEST_FILE_SUFFIX = "_test.py"
|
|
97
|
-
CONFTEST_FILE_NAME = "conftest.py"
|
|
98
|
-
MINIMUM_STATUS_FIELD_COUNT = 2
|
|
99
|
-
ALL_FALLBACK_BASE_REFERENCES = ("origin/main", "origin/master")
|
|
100
|
-
DETACHED_HEAD_LABEL = "HEAD"
|
|
101
|
-
BRANCH_REMOTE_CONFIG_KEY_TEMPLATE = "branch.{branch_name}.remote"
|
|
102
|
-
ALL_TOOLING_STATE_PREFIXES = (
|
|
103
|
-
".claude/verification/",
|
|
104
|
-
".claude/worktrees/",
|
|
105
|
-
".claude/daemon/",
|
|
106
|
-
".claude/teams/",
|
|
107
|
-
".claude/sessions/",
|
|
108
|
-
".cursor/worktrees/",
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
"
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
"
|
|
141
|
-
"
|
|
142
|
-
"
|
|
143
|
-
"
|
|
144
|
-
"
|
|
145
|
-
"
|
|
146
|
-
"
|
|
147
|
-
"
|
|
148
|
-
"
|
|
149
|
-
"
|
|
150
|
-
"
|
|
151
|
-
"surface
|
|
152
|
-
"
|
|
153
|
-
"
|
|
154
|
-
"
|
|
155
|
-
|
|
1
|
+
"""Constants for the verified-commit gate hook family.
|
|
2
|
+
|
|
3
|
+
Shared by ``verification_verdict_store.py``, ``verified_commit_gate.py``,
|
|
4
|
+
and ``verifier_verdict_minter.py`` so every tunable lives in one place.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
GIT_TIMEOUT_SECONDS = 30
|
|
10
|
+
ROOT_KEY_HEX_LENGTH = 16
|
|
11
|
+
VERDICT_JSON_INDENT = 2
|
|
12
|
+
CLAUDE_HOME_DIRECTORY_NAME = ".claude"
|
|
13
|
+
VERDICT_DIRECTORY_NAME = "verification"
|
|
14
|
+
VERDICT_DIRECTORY_NAME_SEPARATOR_PATTERN = r"['\"\\/,\s]+"
|
|
15
|
+
VERDICT_DIRECTORY_PATH_BOUNDARY_PATTERN = r"(?=['\"]*[\\/,])"
|
|
16
|
+
RELATIVE_VERDICT_DIRECTORY_PATTERN = r"(?:^|(?<=[\s;&|(='\"]))verification[\\/]"
|
|
17
|
+
VERDICT_PATH_GLUE_PATTERN = r"['\"+\\/\s]*[\\/]['\"+\\/\s]*"
|
|
18
|
+
VERDICT_DIRECTORY_CHANGE_TARGET_PATTERN = r"[ \t]+['\"]?verification[\\/]?['\"]?(?=[\s;&|]|$)"
|
|
19
|
+
VERDICT_PATH_JOINED_VARIABLE_PATTERN = r"\$\{?(\w+)\}?[\\/]|[\\/]\$\{?(\w+)\}?"
|
|
20
|
+
VERDICT_PATH_VARIABLE_ASSIGNMENT_PATTERN = r"(?:^|(?<=[\s;&|(]))%s=(\S+)"
|
|
21
|
+
VERDICT_FILE_RELATIVE_REFERENCE_PATTERN = (
|
|
22
|
+
rf"(?:^|(?<=[\s;&|(='\"\\/]))verification[\\/][0-9a-f]{{{ROOT_KEY_HEX_LENGTH}}}\.json"
|
|
23
|
+
)
|
|
24
|
+
PATH_OBFUSCATION_PRIMITIVE_PATTERN = (
|
|
25
|
+
r"chr\s*\(|bytes\.fromhex\s*\(|b64decode\s*\(|codecs\.decode\s*\("
|
|
26
|
+
r"|(?:bytes|bytearray)\s*\(\s*\[|\[char\[?\]?\]"
|
|
27
|
+
)
|
|
28
|
+
ALL_VERDICT_PATH_SEGMENT_NAMES = (".claude", "verification")
|
|
29
|
+
ALL_VERDICT_PATH_SEGMENT_BODIES = ("claude", "verification")
|
|
30
|
+
HEX_TOKEN_PATTERN = r"(?<![0-9a-fx])([0-9a-f]{6,})(?![0-9a-f])"
|
|
31
|
+
BASE64_TOKEN_PATTERN = r"[A-Za-z0-9+/]{8,}={0,2}"
|
|
32
|
+
CHARACTER_CODE_SEQUENCE_PATTERN = r"\d{1,3}(?:\s*,\s*\d{1,3})+"
|
|
33
|
+
CHR_CALL_CHAIN_PATTERN = r"chr\(\s*\d{1,3}\s*\)(?:\s*\+\s*chr\(\s*\d{1,3}\s*\))+"
|
|
34
|
+
CHR_CALL_CODE_PATTERN = r"chr\(\s*(\d{1,3})\s*\)"
|
|
35
|
+
HEX_DIGITS_PER_BYTE = 2
|
|
36
|
+
FILE_WRITE_PRIMITIVE_PATTERN = (
|
|
37
|
+
r"\bopen\s*\(|\.write_text\s*\(|\.write_bytes\s*\("
|
|
38
|
+
r"|Out-File|Set-Content|Add-Content|\btee\b|>"
|
|
39
|
+
)
|
|
40
|
+
NON_REDIRECT_FILE_WRITE_PRIMITIVE_PATTERN = (
|
|
41
|
+
r"\bopen\s*\(|\.write_text\s*\(|\.write_bytes\s*\("
|
|
42
|
+
r"|Out-File|Set-Content|Add-Content|\btee\b"
|
|
43
|
+
)
|
|
44
|
+
WRITE_CALL_REGION_PATTERN = (
|
|
45
|
+
r"(?:\bopen\s*\(|\.write_text\s*\(|\.write_bytes\s*\("
|
|
46
|
+
r"|Out-File|Set-Content|Add-Content|\btee\b)[^;&|\n]*"
|
|
47
|
+
)
|
|
48
|
+
VERDICT_KEY_ALL_PASS = "all_pass"
|
|
49
|
+
VERDICT_KEY_MANIFEST_SHA256 = "manifest_sha256"
|
|
50
|
+
VERDICT_KEY_FINDINGS = "findings"
|
|
51
|
+
VERDICT_FINDING_CHECK_KEY = "check"
|
|
52
|
+
LOGS_DIRECTORY_NAME = "logs"
|
|
53
|
+
VERIFIER_VERDICTS_JSONL_FILENAME = "verifier-verdicts.jsonl"
|
|
54
|
+
LEDGER_KEY_TIMESTAMP = "timestamp"
|
|
55
|
+
LEDGER_KEY_REPO_ROOT = "repo_root"
|
|
56
|
+
LEDGER_KEY_BRANCH = "branch"
|
|
57
|
+
LEDGER_KEY_MANIFEST_HASH = "manifest_hash"
|
|
58
|
+
LEDGER_KEY_ALL_PASS = "all_pass"
|
|
59
|
+
LEDGER_KEY_FINDING_COUNT = "finding_count"
|
|
60
|
+
LEDGER_KEY_FINDING_CHECK_NAMES = "finding_check_names"
|
|
61
|
+
LEDGER_KEY_MINTED_FROM_AGENT_ID = "minted_from_agent_id"
|
|
62
|
+
VERDICT_FILE_GLOB = "*.json"
|
|
63
|
+
SUBAGENTS_DIRECTORY_NAME = "subagents"
|
|
64
|
+
AGENT_TRANSCRIPT_GLOB = "agent-*.jsonl"
|
|
65
|
+
AGENT_META_SIDECAR_SUFFIX = ".meta.json"
|
|
66
|
+
AGENT_META_TYPE_KEY = "agentType"
|
|
67
|
+
TRANSCRIPT_ENTRY_TYPE_KEY = "type"
|
|
68
|
+
TRANSCRIPT_ASSISTANT_ENTRY_TYPE = "assistant"
|
|
69
|
+
TRANSCRIPT_MESSAGE_KEY = "message"
|
|
70
|
+
TRANSCRIPT_CONTENT_KEY = "content"
|
|
71
|
+
TRANSCRIPT_CONTENT_TYPE_KEY = "type"
|
|
72
|
+
TRANSCRIPT_TEXT_CONTENT_TYPE = "text"
|
|
73
|
+
TRANSCRIPT_TEXT_KEY = "text"
|
|
74
|
+
VERDICT_FENCE_PATTERN = r"```verdict\s*\n(.*?)```"
|
|
75
|
+
MANIFEST_HASH_CLI_FLAG = "--manifest-hash"
|
|
76
|
+
MANIFEST_HASH_FOR_BRANCH_CLI_FLAG = "--manifest-hash-for-branch"
|
|
77
|
+
WORKTREE_LIST_PATH_PREFIX = "worktree "
|
|
78
|
+
WORKTREE_LIST_BRANCH_PREFIX = "branch "
|
|
79
|
+
BRANCH_REFERENCE_PREFIX = "refs/heads/"
|
|
80
|
+
EMPTY_SURFACE_GUARD_MESSAGE = (
|
|
81
|
+
"ERROR: The work tree at {repo_root} has no changed or untracked files "
|
|
82
|
+
"versus origin/main (empty change surface). This work tree holds nothing "
|
|
83
|
+
"to verify — you are pointed at the wrong work tree. Run `git worktree list` "
|
|
84
|
+
"to see all checked-out work trees and target the one on the branch under review."
|
|
85
|
+
)
|
|
86
|
+
BRANCH_WORKTREE_ABSENT_MESSAGE = (
|
|
87
|
+
"ERROR: No work tree has branch '{branch}' checked out. "
|
|
88
|
+
"Check it out in a work tree first: "
|
|
89
|
+
"`git worktree add <path> <branch>` or `git checkout <branch>`."
|
|
90
|
+
)
|
|
91
|
+
DOCS_ONLY_EXTENSIONS = frozenset(
|
|
92
|
+
{".md", ".txt", ".rst", ".png", ".jpg", ".jpeg", ".gif", ".svg", ".webp", ".ico"}
|
|
93
|
+
)
|
|
94
|
+
PYTHON_EXTENSION = ".py"
|
|
95
|
+
TEST_FILE_PREFIX = "test_"
|
|
96
|
+
TEST_FILE_SUFFIX = "_test.py"
|
|
97
|
+
CONFTEST_FILE_NAME = "conftest.py"
|
|
98
|
+
MINIMUM_STATUS_FIELD_COUNT = 2
|
|
99
|
+
ALL_FALLBACK_BASE_REFERENCES = ("origin/main", "origin/master")
|
|
100
|
+
DETACHED_HEAD_LABEL = "HEAD"
|
|
101
|
+
BRANCH_REMOTE_CONFIG_KEY_TEMPLATE = "branch.{branch_name}.remote"
|
|
102
|
+
ALL_TOOLING_STATE_PREFIXES = (
|
|
103
|
+
".claude/verification/",
|
|
104
|
+
".claude/worktrees/",
|
|
105
|
+
".claude/daemon/",
|
|
106
|
+
".claude/teams/",
|
|
107
|
+
".claude/sessions/",
|
|
108
|
+
".cursor/worktrees/",
|
|
109
|
+
".claude-plugin-data/",
|
|
110
|
+
)
|
|
111
|
+
GATED_GIT_SUBCOMMANDS = frozenset({"commit", "push"})
|
|
112
|
+
ALL_GIT_BINARY_NAMES = frozenset({"git", "git.exe"})
|
|
113
|
+
VALUE_TAKING_GIT_OPTIONS = frozenset({"-C", "-c", "--git-dir", "--work-tree", "--namespace"})
|
|
114
|
+
REPO_DIRECTORY_OPTION = "-C"
|
|
115
|
+
WORK_TREE_OPTION = "--work-tree"
|
|
116
|
+
DIRECTORY_CHANGE_VERBS = frozenset({"cd", "pushd", "set-location", "sl"})
|
|
117
|
+
DIRECTORY_CHANGE_PATH_OPTIONS = frozenset({"-path", "-literalpath"})
|
|
118
|
+
DIRECTORY_CHANGE_OPTION_TERMINATOR = "--"
|
|
119
|
+
DIRECTORY_CHANGE_PATTERN_PREFIX = r"(?:^|(?<=[\s;&|(]))(?:"
|
|
120
|
+
DIRECTORY_CHANGE_PATTERN_SUFFIX = r")(?=\s|$)"
|
|
121
|
+
DIRECTORY_CHANGE_OPTION_PREFIX_PATTERN = r"(?:[ \t]+(?:%s)(?=\s|$))*"
|
|
122
|
+
DIRECTORY_CHANGE_TARGET_PATTERN = r"[ \t]+['\"]?\S*"
|
|
123
|
+
CLAUDE_HOME_TARGET_BOUNDARY_PATTERN = r"[\\/]?['\"]?(?=[\s;&|]|$)"
|
|
124
|
+
VERDICT_DIRECTORY_TARGET_BOUNDARY_PATTERN = r"[\\/]?['\"]?(?=[\s;&|]|$)"
|
|
125
|
+
COMMAND_AFTER_DIRECTORY_CHANGE_PATTERN = r"[;&|\n][\s]*\S"
|
|
126
|
+
OPTION_WITH_VALUE_STEP = 2
|
|
127
|
+
ALL_GATED_TOOL_NAMES = ("Bash", "PowerShell")
|
|
128
|
+
HASH_PREVIEW_LENGTH = 16
|
|
129
|
+
VERIFICATION_BYPASS_MARKER = "# verify-skip"
|
|
130
|
+
MINTING_AGENT_TYPE = "code-verifier"
|
|
131
|
+
VERDICT_DIRECTORY_GUARD_MESSAGE = (
|
|
132
|
+
"BLOCKED: [VERDICT_DIRECTORY_GUARD] Shell access to the verification "
|
|
133
|
+
"verdict directory (~/.claude/verification/) is denied. Only the "
|
|
134
|
+
"verifier_verdict_minter.py SubagentStop hook writes verdict files; a "
|
|
135
|
+
"shell write here would forge a passing verdict and defeat the "
|
|
136
|
+
"verified-commit gate. Spawn the code-verifier agent to earn a verdict "
|
|
137
|
+
"instead of writing one."
|
|
138
|
+
)
|
|
139
|
+
CORRECTIVE_MESSAGE = (
|
|
140
|
+
"BLOCKED: [VERIFIED_COMMIT_GATE] This branch surface has no passing "
|
|
141
|
+
"verification verdict. Clear commit-committability gates first "
|
|
142
|
+
"(CODE_RULES on changed lines and merge conflicts vs base — the "
|
|
143
|
+
"code_verifier_spawn_preflight_gate enforces these on the spawn). If a "
|
|
144
|
+
"prior commit failed on CODE_RULES or staging, fix those before "
|
|
145
|
+
"re-verifying. Then spawn the code-verifier agent (Agent or Task tool, "
|
|
146
|
+
"subagent_type 'code-verifier') with the task texts, the diff scope, "
|
|
147
|
+
"and recorded baselines; when it finishes with a clean verdict the "
|
|
148
|
+
"SubagentStop hook mints the verdict and this command will pass. Any "
|
|
149
|
+
"file change after verification invalidates the verdict, so verify "
|
|
150
|
+
"last only after commit gates are green. The verdict binds to the exact "
|
|
151
|
+
"surface content (every changed and untracked file's bytes), not to a "
|
|
152
|
+
"work-tree path, so a commit from a different work tree passes only "
|
|
153
|
+
"when its surface is byte-identical; a work tree carrying any extra or "
|
|
154
|
+
"later edit has a surface no verdict covers. To land this change, run "
|
|
155
|
+
"this command from the work tree that holds the verified surface, or "
|
|
156
|
+
"verify this surface here. Exempt automatically: docs/image files, "
|
|
157
|
+
"pytest test files, and "
|
|
158
|
+
"Python files whose docstring- and comment-stripped AST is unchanged "
|
|
159
|
+
"(comment-only edits in non-Python files are not exempt)."
|
|
160
|
+
)
|
|
@@ -26,6 +26,8 @@ _MIRRORED_BLOCKING_FILE_NAMES = (
|
|
|
26
26
|
"verifier_verdict_minter.py",
|
|
27
27
|
"verdict_directory_write_blocker.py",
|
|
28
28
|
"verified_commit_config_bootstrap.py",
|
|
29
|
+
"code_review_enforcement_config_bootstrap.py",
|
|
30
|
+
"code_review_stamp_store.py",
|
|
29
31
|
)
|
|
30
32
|
_DECOY_CONFIG_INIT_SOURCE = '"""Foreign config package that must never win resolution."""\n'
|
|
31
33
|
_DECOY_CONFIG_CONSTANTS_SOURCE = (
|
|
@@ -18,11 +18,19 @@ _hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
|
18
18
|
if _hooks_dir not in sys.path:
|
|
19
19
|
sys.path.insert(0, _hooks_dir)
|
|
20
20
|
|
|
21
|
+
from blocking.pr_description_pr_number import ( # noqa: E402
|
|
22
|
+
_extract_pr_number_from_command,
|
|
23
|
+
)
|
|
21
24
|
from hooks_constants.convergence_gate_blocker_constants import ( # noqa: E402
|
|
25
|
+
ALL_GH_PR_VIEW_NUMBER_COMMAND,
|
|
26
|
+
BASH_LINE_CONTINUATION_PATTERN,
|
|
22
27
|
COMMAND_SEPARATOR_PATTERN,
|
|
23
28
|
GH_PR_READY_ANCHOR_PATTERN,
|
|
29
|
+
GH_REPO_FLAG,
|
|
30
|
+
GIT_URL_SUFFIX,
|
|
24
31
|
PR_URL_OWNER_REPO_NUMBER_PATTERN,
|
|
25
32
|
REPO_OVERRIDE_FLAG_PATTERN,
|
|
33
|
+
REPO_SLUG_TEMPLATE,
|
|
26
34
|
)
|
|
27
35
|
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
28
36
|
|
|
@@ -36,12 +44,15 @@ def _ready_command_segment(command: str) -> str:
|
|
|
36
44
|
^^^^^^^^^^^^^^^^ clipped -> returned segment
|
|
37
45
|
|
|
38
46
|
Scanning only this segment keeps a ``--repo`` flag or PR URL that belongs to
|
|
39
|
-
a chained command from binding the gate to the wrong PR.
|
|
47
|
+
a chained command from binding the gate to the wrong PR. A backslash-newline
|
|
48
|
+
continuation is folded to a space before the separator search, so a
|
|
49
|
+
``--repo`` flag written on a continued line stays inside the segment.
|
|
40
50
|
"""
|
|
41
51
|
ready_match = re.search(GH_PR_READY_ANCHOR_PATTERN, command)
|
|
42
52
|
if ready_match is None:
|
|
43
53
|
return command
|
|
44
54
|
ready_tail = command[ready_match.start() :]
|
|
55
|
+
ready_tail = re.sub(BASH_LINE_CONTINUATION_PATTERN, " ", ready_tail)
|
|
45
56
|
separator_match = re.search(COMMAND_SEPARATOR_PATTERN, ready_tail)
|
|
46
57
|
if separator_match is None:
|
|
47
58
|
return ready_tail
|
|
@@ -61,30 +72,59 @@ def _parse_repo_flag(command: str) -> tuple[str, str] | None:
|
|
|
61
72
|
flag_match = re.search(REPO_OVERRIDE_FLAG_PATTERN, command)
|
|
62
73
|
if flag_match is None:
|
|
63
74
|
return None
|
|
64
|
-
return flag_match.group("owner"), flag_match.group("repo")
|
|
75
|
+
return flag_match.group("owner"), flag_match.group("repo").removesuffix(GIT_URL_SUFFIX)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _resolve_named_identity(
|
|
79
|
+
command: str, cwd: str | None
|
|
80
|
+
) -> tuple[tuple[str, str] | None, int] | None:
|
|
81
|
+
"""Resolve the repository the command names and its PR number.
|
|
82
|
+
|
|
83
|
+
A full PR URL yields both the (owner, repo) pair and the number. A
|
|
84
|
+
``--repo``/``-R`` flag yields the pair while the number resolves from the
|
|
85
|
+
command's positional argument or the named repository's current-branch PR.
|
|
86
|
+
With neither present, the pair is None — leaving gh bound to the current
|
|
87
|
+
directory's repository — and the number resolves the same way. Every parse
|
|
88
|
+
runs over the ``gh pr ready`` segment alone, clipped at the next command
|
|
89
|
+
separator, so a flag or URL belonging to a chained command cannot bind the
|
|
90
|
+
gate to the wrong PR.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
command: The raw shell command captured by the hook.
|
|
94
|
+
cwd: The working directory gh runs in when the number resolves from the
|
|
95
|
+
current branch, or None for the process default.
|
|
96
|
+
|
|
97
|
+
Returns:
|
|
98
|
+
The (owner, repo) pair the command names — None when it names none —
|
|
99
|
+
paired with the resolved PR number, or None when no number resolves.
|
|
100
|
+
"""
|
|
101
|
+
ready_segment = _ready_command_segment(command)
|
|
102
|
+
pr_url_identity = _parse_pr_url(ready_segment)
|
|
103
|
+
if pr_url_identity is not None:
|
|
104
|
+
url_owner, url_repo, url_number = pr_url_identity
|
|
105
|
+
return (url_owner, url_repo), url_number
|
|
106
|
+
all_target_repo = _parse_repo_flag(ready_segment)
|
|
107
|
+
from_owner, from_repo = all_target_repo if all_target_repo is not None else (None, None)
|
|
108
|
+
resolved_pr_number = _resolve_pr_number(ready_segment, cwd, from_owner, from_repo)
|
|
109
|
+
if resolved_pr_number is None:
|
|
110
|
+
return None
|
|
111
|
+
return all_target_repo, resolved_pr_number
|
|
65
112
|
|
|
66
113
|
|
|
67
114
|
def _resolve_target_identity(command: str, cwd: str | None) -> tuple[str, str, int] | None:
|
|
68
115
|
"""Resolve the (owner, repo, pr_number) the gate keys its evidence to.
|
|
69
116
|
|
|
70
|
-
|
|
71
|
-
the repository
|
|
72
|
-
|
|
73
|
-
resolve from the cwd.
|
|
117
|
+
Delegates the repository-and-number resolution to the shared core, then
|
|
118
|
+
falls back to the cwd repository for the number when the command names no
|
|
119
|
+
repository of its own.
|
|
74
120
|
"""
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
if pr_url_identity is not None:
|
|
78
|
-
return pr_url_identity
|
|
79
|
-
|
|
80
|
-
pr_number = _resolve_pr_number(ready_segment, cwd)
|
|
81
|
-
if pr_number is None:
|
|
121
|
+
named_identity = _resolve_named_identity(command, cwd)
|
|
122
|
+
if named_identity is None:
|
|
82
123
|
return None
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return flag_owner, flag_repo, pr_number
|
|
124
|
+
all_target_repo, pr_number = named_identity
|
|
125
|
+
if all_target_repo is not None:
|
|
126
|
+
owner, repo = all_target_repo
|
|
127
|
+
return owner, repo, pr_number
|
|
88
128
|
|
|
89
129
|
cwd_identity = _resolve_owner_repo(cwd)
|
|
90
130
|
if cwd_identity is None:
|
|
@@ -93,13 +133,46 @@ def _resolve_target_identity(command: str, cwd: str | None) -> tuple[str, str, i
|
|
|
93
133
|
return cwd_owner, cwd_repo, pr_number
|
|
94
134
|
|
|
95
135
|
|
|
96
|
-
def
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
136
|
+
def _repo_flag_arguments(all_target_repo: tuple[str, str] | None) -> list[str]:
|
|
137
|
+
"""Build the ``--repo owner/repo`` arguments for a gh subcommand.
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
all_target_repo: The (owner, repo) pair the command names, or None when
|
|
141
|
+
the command names no repository.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
A ``[--repo, owner/repo]`` argument pair when a repository is named, or
|
|
145
|
+
an empty list when it is None — leaving gh bound to the current
|
|
146
|
+
directory's repository.
|
|
147
|
+
"""
|
|
148
|
+
if all_target_repo is None:
|
|
149
|
+
return []
|
|
150
|
+
owner, repo = all_target_repo
|
|
151
|
+
return [GH_REPO_FLAG, REPO_SLUG_TEMPLATE.format(owner=owner, repo=repo)]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _resolve_current_branch_pr_number(
|
|
155
|
+
all_target_repo: tuple[str, str] | None, cwd: str | None
|
|
156
|
+
) -> int | None:
|
|
157
|
+
"""Resolve the current-branch PR number through ``gh pr view``.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
all_target_repo: The (owner, repo) pair the command names, or None to
|
|
161
|
+
read the current directory's repository.
|
|
162
|
+
cwd: The working directory gh runs in, or None for the process default.
|
|
163
|
+
|
|
164
|
+
Returns:
|
|
165
|
+
The PR number gh reports, or None when gh is missing, exits non-zero,
|
|
166
|
+
or prints no integer — every failure path returns None so the caller
|
|
167
|
+
fails open.
|
|
168
|
+
"""
|
|
169
|
+
all_view_arguments = [
|
|
170
|
+
*ALL_GH_PR_VIEW_NUMBER_COMMAND,
|
|
171
|
+
*_repo_flag_arguments(all_target_repo),
|
|
172
|
+
]
|
|
100
173
|
try:
|
|
101
174
|
completed_process = subprocess.run(
|
|
102
|
-
|
|
175
|
+
all_view_arguments,
|
|
103
176
|
capture_output=True,
|
|
104
177
|
text=True,
|
|
105
178
|
cwd=cwd or None,
|
|
@@ -115,6 +188,22 @@ def _resolve_pr_number(command: str, cwd: str | None) -> int | None:
|
|
|
115
188
|
return None
|
|
116
189
|
|
|
117
190
|
|
|
191
|
+
def _resolve_pr_number(
|
|
192
|
+
command: str,
|
|
193
|
+
cwd: str | None,
|
|
194
|
+
from_owner: str | None,
|
|
195
|
+
from_repo: str | None,
|
|
196
|
+
) -> int | None:
|
|
197
|
+
direct_number = _extract_pr_number_from_command(command)
|
|
198
|
+
if direct_number is not None:
|
|
199
|
+
return direct_number
|
|
200
|
+
if from_owner is not None and from_repo is not None:
|
|
201
|
+
all_target_repo: tuple[str, str] | None = (from_owner, from_repo)
|
|
202
|
+
else:
|
|
203
|
+
all_target_repo = None
|
|
204
|
+
return _resolve_current_branch_pr_number(all_target_repo, cwd)
|
|
205
|
+
|
|
206
|
+
|
|
118
207
|
def _resolve_owner_repo(cwd: str | None) -> tuple[str, str] | None:
|
|
119
208
|
try:
|
|
120
209
|
completed_process = subprocess.run(
|
|
@@ -24,6 +24,8 @@ from hooks_constants.destructive_command_segment_constants import ( # noqa: E40
|
|
|
24
24
|
ALL_BENIGN_COMPOUND_SEGMENT_COMMANDS,
|
|
25
25
|
ALL_COMMAND_LAUNCHER_WRAPPER_COMMANDS,
|
|
26
26
|
ALL_KNOWN_TEMPORARY_ENVIRONMENT_VARIABLE_NAMES,
|
|
27
|
+
ALL_TRUTHY_ENV_VALUES,
|
|
28
|
+
DESTRUCTIVE_DENY_MODE_ENV_VAR,
|
|
27
29
|
ALL_FILE_WRITING_OUTPUT_FLAGS_BY_BENIGN_PROGRAM,
|
|
28
30
|
ALL_FIND_EXEC_ACTION_FLAGS,
|
|
29
31
|
ALL_FIND_EXEC_ACTION_TERMINATORS,
|
|
@@ -56,12 +58,17 @@ from hooks_constants.destructive_command_segment_constants import ( # noqa: E40
|
|
|
56
58
|
|
|
57
59
|
CLAUDE_DIRECTORY_PATH = os.path.normpath(os.path.expanduser("~/.claude"))
|
|
58
60
|
GH_REDIRECT_ACTIVE_ENV_VAR = "CLAUDE_GH_REDIRECT_ACTIVE"
|
|
59
|
-
GH_REDIRECT_ACTIVE_TRUTHY_VALUES = frozenset({"1", "true", "yes", "on"})
|
|
60
61
|
|
|
61
62
|
|
|
62
63
|
def gh_redirect_is_active() -> bool:
|
|
63
64
|
env_var_value = os.environ.get(GH_REDIRECT_ACTIVE_ENV_VAR, "").strip().lower()
|
|
64
|
-
return env_var_value in
|
|
65
|
+
return env_var_value in ALL_TRUTHY_ENV_VALUES
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def deny_mode_is_active() -> bool:
|
|
69
|
+
env_var_value = os.environ.get(DESTRUCTIVE_DENY_MODE_ENV_VAR, "").strip().lower()
|
|
70
|
+
return env_var_value in ALL_TRUTHY_ENV_VALUES
|
|
71
|
+
|
|
65
72
|
|
|
66
73
|
def directory_is_ephemeral(directory_path: str) -> bool:
|
|
67
74
|
"""Return True when a directory belongs to the ephemeral auto-allow namespace.
|
|
@@ -2043,14 +2050,20 @@ def main() -> None:
|
|
|
2043
2050
|
matched_description = co_resident_hazard_description
|
|
2044
2051
|
|
|
2045
2052
|
if matched_description is not None:
|
|
2046
|
-
|
|
2053
|
+
if deny_mode_is_active():
|
|
2054
|
+
terminal_decision = "deny"
|
|
2055
|
+
terminal_reason = f"DESTRUCTIVE: {matched_description}. Blocked in deny mode."
|
|
2056
|
+
else:
|
|
2057
|
+
terminal_decision = "ask"
|
|
2058
|
+
terminal_reason = f"DESTRUCTIVE: {matched_description}. Requires explicit user approval."
|
|
2059
|
+
terminal_response = {
|
|
2047
2060
|
"hookSpecificOutput": {
|
|
2048
2061
|
"hookEventName": "PreToolUse",
|
|
2049
|
-
"permissionDecision":
|
|
2050
|
-
"permissionDecisionReason":
|
|
2062
|
+
"permissionDecision": terminal_decision,
|
|
2063
|
+
"permissionDecisionReason": terminal_reason
|
|
2051
2064
|
}
|
|
2052
2065
|
}
|
|
2053
|
-
print(json.dumps(
|
|
2066
|
+
print(json.dumps(terminal_response))
|
|
2054
2067
|
|
|
2055
2068
|
sys.exit(0)
|
|
2056
2069
|
|