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
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Behavioral tests for the split directory-change-into-stamp matcher.
|
|
2
|
+
|
|
3
|
+
The matcher blocks a command that changes into the Claude home, then into a
|
|
4
|
+
relative ``code-review-stamps`` directory, then runs any further command, while
|
|
5
|
+
an unrelated second change or a benign command passes.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from code_review_stamp_write_blocker_parts.split_directory_change_into_stamp import (
|
|
11
|
+
changes_through_split_directory_into_stamp,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
SPLIT_CHANGE_FORGE = "cd ~/.claude && cd code-review-stamps && echo x > f.json"
|
|
15
|
+
UNRELATED_SECOND_CHANGE = "cd ~/.claude && cd hooks && echo x > f.json"
|
|
16
|
+
PYTEST_RUN = "python -m pytest test_code_review_stamp_store.py"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def test_split_change_into_stamp_then_command_is_blocked() -> None:
|
|
20
|
+
assert changes_through_split_directory_into_stamp(SPLIT_CHANGE_FORGE)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_unrelated_second_change_is_not_blocked() -> None:
|
|
24
|
+
assert not changes_through_split_directory_into_stamp(UNRELATED_SECOND_CHANGE)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def test_pytest_run_is_not_blocked() -> None:
|
|
28
|
+
assert not changes_through_split_directory_into_stamp(PYTEST_RUN)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def test_change_into_stamp_without_following_command_is_not_blocked() -> None:
|
|
32
|
+
assert not changes_through_split_directory_into_stamp("cd ~/.claude && cd code-review-stamps")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_pushd_split_change_into_stamp_then_command_is_blocked() -> None:
|
|
36
|
+
assert changes_through_split_directory_into_stamp(
|
|
37
|
+
"pushd ~/.claude && pushd code-review-stamps && cp a b"
|
|
38
|
+
)
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""PreToolUse hook: pre-flight gate for the code-verifier subagent spawn.
|
|
3
3
|
|
|
4
|
-
The hook fires
|
|
4
|
+
The hook fires on an ``Agent`` or ``Task`` tool call whose ``subagent_type`` is
|
|
5
5
|
``code-verifier``. Before that verification spawn runs, the hook checks the
|
|
6
6
|
branch for two committability problems against the resolved base ref: a real
|
|
7
7
|
merge conflict (a non-mutating trial-merge of HEAD against the base ref) and a
|
|
8
8
|
CODE_RULES violation on a line added in the working tree since the merge base
|
|
9
|
-
(committed on the branch or uncommitted). When
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
9
|
+
(committed on the branch or uncommitted). When either fires, the hook denies
|
|
10
|
+
the spawn with a reason addressed to the spawning agent that names the
|
|
11
|
+
conflicting files and the violating file:line, so that agent fixes them and
|
|
12
|
+
re-spawns. A CODE_RULES engine import/load failure is fail-closed (deny with a
|
|
13
|
+
named load-failure section). Environmental problems — a non-repo cwd, an
|
|
14
|
+
absent base ref, a git failure, an unreadable file alone, or a timeout — fail
|
|
15
|
+
OPEN because the authoritative fail-closed CODE_RULES enforcement already runs
|
|
16
|
+
at Write time and at commit time. The hook never network-fetches and never
|
|
16
17
|
mutates the index or working tree.
|
|
17
18
|
"""
|
|
18
19
|
|
|
@@ -21,6 +22,7 @@ from __future__ import annotations
|
|
|
21
22
|
import contextlib
|
|
22
23
|
import io
|
|
23
24
|
import json
|
|
25
|
+
import re
|
|
24
26
|
import subprocess
|
|
25
27
|
import sys
|
|
26
28
|
from pathlib import Path
|
|
@@ -43,6 +45,7 @@ from verification_verdict_store import ( # noqa: E402
|
|
|
43
45
|
)
|
|
44
46
|
|
|
45
47
|
from hooks_constants.code_verifier_spawn_preflight_gate_constants import ( # noqa: E402
|
|
48
|
+
ALL_CODE_VERIFIER_SPAWN_TOOL_NAMES,
|
|
46
49
|
ALL_MERGE_HEAD_PROBE_FLAGS,
|
|
47
50
|
ALL_MERGE_TREE_COMMAND_FLAGS,
|
|
48
51
|
ALL_NAME_ONLY_WORKTREE_DIFF_FLAGS,
|
|
@@ -51,6 +54,7 @@ from hooks_constants.code_verifier_spawn_preflight_gate_constants import ( # no
|
|
|
51
54
|
CODE_RULES_SECTION_HEADER,
|
|
52
55
|
CODE_VERIFIER_SUBAGENT_TYPE,
|
|
53
56
|
DENY_REASON_LEAD,
|
|
57
|
+
ENGINE_LOAD_FAILURE_SECTION,
|
|
54
58
|
GATE_SCRIPTS_RELATIVE_PATH,
|
|
55
59
|
MERGE_CONFLICT_SECTION_HEADER,
|
|
56
60
|
MERGE_TREE_CLEAN_EXIT_CODE,
|
|
@@ -58,9 +62,6 @@ from hooks_constants.code_verifier_spawn_preflight_gate_constants import ( # no
|
|
|
58
62
|
MERGE_TREE_TIMEOUT_SECONDS,
|
|
59
63
|
)
|
|
60
64
|
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
61
|
-
from hooks_constants.pr_converge_bugteam_enforcer_constants import ( # noqa: E402
|
|
62
|
-
AGENT_TOOL_NAME,
|
|
63
|
-
)
|
|
64
65
|
|
|
65
66
|
_scripts_dir = str(Path(__file__).resolve().parents[2] / GATE_SCRIPTS_RELATIVE_PATH)
|
|
66
67
|
if _scripts_dir not in sys.path:
|
|
@@ -78,17 +79,17 @@ from code_rules_gate import ( # noqa: E402
|
|
|
78
79
|
|
|
79
80
|
|
|
80
81
|
def _should_run(payload_by_field: dict[str, object]) -> bool:
|
|
81
|
-
"""Return True only for a code-verifier Agent spawn.
|
|
82
|
+
"""Return True only for a code-verifier Agent or Task spawn.
|
|
82
83
|
|
|
83
84
|
Args:
|
|
84
85
|
payload_by_field: The full PreToolUse hook payload (already
|
|
85
86
|
JSON-parsed), keyed by top-level field name.
|
|
86
87
|
|
|
87
88
|
Returns:
|
|
88
|
-
True when the tool is Agent and ``tool_input.subagent_type``
|
|
89
|
-
``code-verifier``; False for every other shape.
|
|
89
|
+
True when the tool is Agent or Task and ``tool_input.subagent_type``
|
|
90
|
+
is ``code-verifier``; False for every other shape.
|
|
90
91
|
"""
|
|
91
|
-
if payload_by_field.get("tool_name", "")
|
|
92
|
+
if payload_by_field.get("tool_name", "") not in ALL_CODE_VERIFIER_SPAWN_TOOL_NAMES:
|
|
92
93
|
return False
|
|
93
94
|
tool_input = payload_by_field.get("tool_input", {})
|
|
94
95
|
if not isinstance(tool_input, dict):
|
|
@@ -297,7 +298,7 @@ def _tracked_file_added_lines(repo_root: str, merge_base_sha: str, relative_path
|
|
|
297
298
|
def _code_rules_report(
|
|
298
299
|
repo_root: str, all_file_paths: list[Path], all_added_lines_by_path: dict[Path, set[int]]
|
|
299
300
|
) -> str | None:
|
|
300
|
-
"""Run the CODE_RULES engine and return
|
|
301
|
+
"""Run the CODE_RULES engine and return a full deny section, or None.
|
|
301
302
|
|
|
302
303
|
Args:
|
|
303
304
|
repo_root: The repository top-level directory.
|
|
@@ -306,19 +307,30 @@ def _code_rules_report(
|
|
|
306
307
|
by resolved absolute path.
|
|
307
308
|
|
|
308
309
|
Returns:
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
changed file caused a non-zero gate
|
|
312
|
-
|
|
313
|
-
harness hook timeout in hooks.json is the wall-clock
|
|
314
|
-
engine.
|
|
310
|
+
A full deny section when a blocking violation lands on an added line or
|
|
311
|
+
the CODE_RULES engine fails to import/load (fail-closed). None when the
|
|
312
|
+
surface is clean, only an unreadable changed file caused a non-zero gate
|
|
313
|
+
exit, or an environmental engine error arises — those non-block outcomes
|
|
314
|
+
fail OPEN. The harness hook timeout in hooks.json is the wall-clock
|
|
315
|
+
bound on a runaway engine.
|
|
315
316
|
"""
|
|
316
317
|
if not all_file_paths:
|
|
317
318
|
return None
|
|
318
319
|
try:
|
|
319
320
|
validate_content = load_validate_content()
|
|
320
321
|
except SystemExit:
|
|
321
|
-
return
|
|
322
|
+
return ENGINE_LOAD_FAILURE_SECTION
|
|
323
|
+
except (
|
|
324
|
+
ImportError,
|
|
325
|
+
SyntaxError,
|
|
326
|
+
AttributeError,
|
|
327
|
+
RuntimeError,
|
|
328
|
+
TypeError,
|
|
329
|
+
NameError,
|
|
330
|
+
OSError,
|
|
331
|
+
re.error,
|
|
332
|
+
):
|
|
333
|
+
return ENGINE_LOAD_FAILURE_SECTION
|
|
322
334
|
try:
|
|
323
335
|
blocking_present, captured_report = _run_gate_capturing_stderr(
|
|
324
336
|
validate_content, all_file_paths, Path(repo_root), all_added_lines_by_path
|
|
@@ -327,7 +339,7 @@ def _code_rules_report(
|
|
|
327
339
|
return None
|
|
328
340
|
if not blocking_present:
|
|
329
341
|
return None
|
|
330
|
-
return captured_report
|
|
342
|
+
return f"{CODE_RULES_SECTION_HEADER}\n{captured_report.strip()}"
|
|
331
343
|
|
|
332
344
|
|
|
333
345
|
def _run_gate_capturing_stderr(
|
|
@@ -382,8 +394,8 @@ def _build_deny_reason(
|
|
|
382
394
|
all_conflicting_files: The conflicting file paths from the conflict
|
|
383
395
|
check, an empty list when clean, or None when that check failed open.
|
|
384
396
|
base_ref: The base ref named in the conflict section header.
|
|
385
|
-
code_rules_report:
|
|
386
|
-
when that check found nothing or failed open.
|
|
397
|
+
code_rules_report: A full CODE_RULES deny section (violations or engine
|
|
398
|
+
load failure), or None when that check found nothing or failed open.
|
|
387
399
|
|
|
388
400
|
Returns:
|
|
389
401
|
The full deny reason when either check fired, or None when neither
|
|
@@ -395,7 +407,7 @@ def _build_deny_reason(
|
|
|
395
407
|
conflict_header = MERGE_CONFLICT_SECTION_HEADER.format(base_ref=base_ref)
|
|
396
408
|
reason_sections.append(f"{conflict_header}\n{conflict_lines}")
|
|
397
409
|
if code_rules_report:
|
|
398
|
-
reason_sections.append(
|
|
410
|
+
reason_sections.append(code_rules_report.strip())
|
|
399
411
|
if not reason_sections:
|
|
400
412
|
return None
|
|
401
413
|
return DENY_REASON_LEAD + "\n\n" + "\n\n".join(reason_sections)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"""Configuration package for the blocking hooks.
|
|
2
|
-
|
|
3
|
-
A regular package (not a namespace package) so it resolves ahead of any
|
|
4
|
-
same-named package later on ``sys.path``.
|
|
5
|
-
"""
|
|
1
|
+
"""Configuration package for the blocking hooks.
|
|
2
|
+
|
|
3
|
+
A regular package (not a namespace package) so it resolves ahead of any
|
|
4
|
+
same-named package later on ``sys.path``.
|
|
5
|
+
"""
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Single source of truth for the code-review enforcement gate family.
|
|
2
|
+
|
|
3
|
+
Holds the stamp directory name, the ordered effort tokens (``low`` under
|
|
4
|
+
``medium`` under ``high`` under ``xhigh`` under ``max``, with ``ultra`` kept
|
|
5
|
+
out because it needs an interactive terminal), the effort a push and a
|
|
6
|
+
pull-request creation each require, the stamp record keys, the gate and
|
|
7
|
+
write-blocker messages, the store-forge shell patterns, the MCP create-PR
|
|
8
|
+
tool name, and the effort comparison every gate and the stamp store share so
|
|
9
|
+
the thresholds never drift between them, and the master enable flag
|
|
10
|
+
(``CODE_REVIEW_ENFORCEMENT_ENABLED``, default off) that every gate and
|
|
11
|
+
the stamp-directory write-blocker read before they enforce anything.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
CODE_REVIEW_ENFORCEMENT_ENABLED = False
|
|
17
|
+
STAMP_DIRECTORY_NAME = "code-review-stamps"
|
|
18
|
+
ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER = ("low", "medium", "high", "xhigh", "max")
|
|
19
|
+
PUSH_REQUIRED_EFFORT = "low"
|
|
20
|
+
PR_CREATE_REQUIRED_EFFORT = "xhigh"
|
|
21
|
+
STAMP_KEY_EFFORT = "effort"
|
|
22
|
+
STAMP_KEY_MANIFEST_SHA256 = "manifest_sha256"
|
|
23
|
+
STAMP_KEY_RECORDED_AT_EPOCH = "recorded_at_epoch"
|
|
24
|
+
CODE_REVIEW_BYPASS_MARKER = "# code-review-skip"
|
|
25
|
+
GATED_PUSH_SUBCOMMANDS = frozenset({"push"})
|
|
26
|
+
ALL_GATED_SHELL_TOOL_NAMES = ("Bash", "PowerShell")
|
|
27
|
+
ALL_WRITE_EDIT_TOOL_NAMES = ("Write", "Edit", "MultiEdit")
|
|
28
|
+
MCP_CREATE_PULL_REQUEST_TOOL_NAME = "mcp__plugin_github_github__create_pull_request"
|
|
29
|
+
HASH_PREVIEW_LENGTH = 16
|
|
30
|
+
PRE_TOOL_USE_HOOK_EVENT_NAME = "PreToolUse"
|
|
31
|
+
DENY_PERMISSION_DECISION = "deny"
|
|
32
|
+
PUSH_GATE_HOOK_MODULE_NAME = "code_review_push_gate.py"
|
|
33
|
+
PR_CREATE_GATE_HOOK_MODULE_NAME = "code_review_pr_create_gate.py"
|
|
34
|
+
STAMP_WRITE_BLOCKER_HOOK_MODULE_NAME = "code_review_stamp_directory_write_blocker.py"
|
|
35
|
+
SANCTIONED_STAMP_MINTER_FLAG = "--record-stamp"
|
|
36
|
+
PUSH_GATE_CORRECTIVE_MESSAGE = (
|
|
37
|
+
"BLOCKED: [CODE_REVIEW_PUSH_GATE] This branch surface has no clean "
|
|
38
|
+
"code-review stamp at effort 'low' or higher. Run "
|
|
39
|
+
"`python invoke_code_review.py --record-stamp --cwd <dir> "
|
|
40
|
+
"--session-model <alias> low` (or a higher effort) so a clean review "
|
|
41
|
+
"mints the stamp before `git push`; any file change after minting "
|
|
42
|
+
"invalidates it. Append `# code-review-skip` as a trailing shell comment "
|
|
43
|
+
"only when the verified-commit gate's verify-skip rule for a same-surface "
|
|
44
|
+
"re-land applies. Exempt automatically: docs/image files, pytest test "
|
|
45
|
+
"files, and Python files whose docstring- and comment-stripped AST is "
|
|
46
|
+
"unchanged."
|
|
47
|
+
)
|
|
48
|
+
PR_CREATE_GATE_CORRECTIVE_MESSAGE = (
|
|
49
|
+
"BLOCKED: [CODE_REVIEW_PR_CREATE_GATE] This branch surface has no clean "
|
|
50
|
+
"code-review stamp at effort 'xhigh' or higher. Run "
|
|
51
|
+
"`python invoke_code_review.py --record-stamp --cwd <dir> "
|
|
52
|
+
"--session-model <alias> xhigh` (or max) so a clean review mints the "
|
|
53
|
+
"stamp before `gh pr create` or the MCP create_pull_request tool. Any "
|
|
54
|
+
"file change after minting invalidates the stamp."
|
|
55
|
+
)
|
|
56
|
+
STAMP_DIRECTORY_GUARD_MESSAGE = (
|
|
57
|
+
"BLOCKED: [CODE_REVIEW_STAMP_DIRECTORY_GUARD] Access to the code-review "
|
|
58
|
+
"stamp directory (~/.claude/code-review-stamps/) is denied. Only "
|
|
59
|
+
"invoke_code_review.py --record-stamp mints stamp files; a shell or "
|
|
60
|
+
"Write/Edit forge here would defeat the push and PR-create gates. "
|
|
61
|
+
"Earn a stamp by running a clean /code-review through the invoker."
|
|
62
|
+
)
|
|
63
|
+
RELATIVE_STAMP_DIRECTORY_PATTERN = r"(?:^|(?<=[\s;&|(='\"]))code-review-stamps[\\/]"
|
|
64
|
+
STAMP_DIRECTORY_CHANGE_TARGET_PATTERN = r"[ \t]+['\"]?code-review-stamps[\\/]?['\"]?(?=[\s;&|]|$)"
|
|
65
|
+
STAMP_FILE_REFERENCE_BOUNDARY_PREFIX_PATTERN = r"(?:^|(?<=[\s;&|(='\"\\/]))"
|
|
66
|
+
STAMP_FILE_ROOT_KEY_JSON_SUFFIX_PATTERN = r"[\\/][0-9a-f]{%d}\.json"
|
|
67
|
+
GH_PR_CREATE_INVOCATION_PATTERN = (
|
|
68
|
+
r"(?:^|[;&|\n`({]|\$\()[ \t]*"
|
|
69
|
+
r"(?:(?:if|then|else|elif|while|until|do|!)[ \t]+)*"
|
|
70
|
+
r"(?:[A-Za-z_][A-Za-z0-9_]*=\S+[ \t]+)*"
|
|
71
|
+
r"gh(?:[ \t]+(?:--[A-Za-z][\w-]*(?:=\S+)?|-[A-Za-z])(?:[ \t]+(?!-)\S+)?)*"
|
|
72
|
+
r"[ \t]+pr[ \t]+create\b"
|
|
73
|
+
)
|
|
74
|
+
ALL_STAMP_PATH_SEGMENT_NAMES = (".claude", "code-review-stamps")
|
|
75
|
+
ALL_STAMP_PATH_SEGMENT_BODIES = ("claude", "code-review-stamps")
|
|
76
|
+
STAMP_STORE_MODULE_NAME = "code_review_stamp_store"
|
|
77
|
+
STAMP_MINT_FUNCTION_NAME = "record_clean_stamp"
|
|
78
|
+
ALL_STAMP_STORE_FORGE_PATTERNS = (
|
|
79
|
+
rf"\bimport\s+{STAMP_STORE_MODULE_NAME}\b",
|
|
80
|
+
rf"\bfrom\s+{STAMP_STORE_MODULE_NAME}\s+import\b",
|
|
81
|
+
rf"\b{STAMP_MINT_FUNCTION_NAME}\s*\(",
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def effort_meets_threshold(candidate_effort: str, required_effort: str) -> bool:
|
|
86
|
+
"""Decide whether a recorded effort satisfies a required threshold.
|
|
87
|
+
|
|
88
|
+
::
|
|
89
|
+
|
|
90
|
+
effort_meets_threshold("xhigh", "low") -> True xhigh outranks low
|
|
91
|
+
effort_meets_threshold("low", "xhigh") -> False low sits below xhigh
|
|
92
|
+
effort_meets_threshold("ultra", "low") -> False ultra is unranked
|
|
93
|
+
|
|
94
|
+
A candidate satisfies the threshold when it sits at or above the required
|
|
95
|
+
token in the ascending order. A token outside the known order — ``ultra``
|
|
96
|
+
among them — satisfies nothing, so an unrecognized stored effort clears no
|
|
97
|
+
gate.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
candidate_effort: The effort a recorded stamp was earned at.
|
|
101
|
+
required_effort: The effort the gate demands for the action.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
True when both tokens are known and the candidate ranks at or above the
|
|
105
|
+
required token; False otherwise.
|
|
106
|
+
"""
|
|
107
|
+
if candidate_effort not in ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER:
|
|
108
|
+
return False
|
|
109
|
+
if required_effort not in ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER:
|
|
110
|
+
return False
|
|
111
|
+
candidate_rank = ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER.index(candidate_effort)
|
|
112
|
+
required_rank = ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER.index(required_effort)
|
|
113
|
+
return candidate_rank >= required_rank
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Behavioral tests for the code-review enforcement constants and comparator.
|
|
2
|
+
|
|
3
|
+
These exercise ``effort_meets_threshold`` against the ordered effort tokens and
|
|
4
|
+
assert the push and pull-request thresholds carry the push-based names the gate
|
|
5
|
+
family reads, so a rename that reintroduces commit terminology fails loudly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import importlib.util
|
|
9
|
+
import pathlib
|
|
10
|
+
import re
|
|
11
|
+
|
|
12
|
+
_CONFIG_DIR = pathlib.Path(__file__).parent
|
|
13
|
+
|
|
14
|
+
SAMPLE_ROOT_KEY_HEX_LENGTH = 16
|
|
15
|
+
|
|
16
|
+
_constants_spec = importlib.util.spec_from_file_location(
|
|
17
|
+
"code_review_enforcement_constants",
|
|
18
|
+
_CONFIG_DIR / "code_review_enforcement_constants.py",
|
|
19
|
+
)
|
|
20
|
+
assert _constants_spec is not None
|
|
21
|
+
assert _constants_spec.loader is not None
|
|
22
|
+
_constants_module = importlib.util.module_from_spec(_constants_spec)
|
|
23
|
+
_constants_spec.loader.exec_module(_constants_module)
|
|
24
|
+
|
|
25
|
+
effort_meets_threshold = _constants_module.effort_meets_threshold
|
|
26
|
+
CODE_REVIEW_ENFORCEMENT_ENABLED = _constants_module.CODE_REVIEW_ENFORCEMENT_ENABLED
|
|
27
|
+
PUSH_REQUIRED_EFFORT = _constants_module.PUSH_REQUIRED_EFFORT
|
|
28
|
+
PR_CREATE_REQUIRED_EFFORT = _constants_module.PR_CREATE_REQUIRED_EFFORT
|
|
29
|
+
GATED_PUSH_SUBCOMMANDS = _constants_module.GATED_PUSH_SUBCOMMANDS
|
|
30
|
+
ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER = _constants_module.ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER
|
|
31
|
+
PUSH_GATE_CORRECTIVE_MESSAGE = _constants_module.PUSH_GATE_CORRECTIVE_MESSAGE
|
|
32
|
+
SANCTIONED_STAMP_MINTER_FLAG = _constants_module.SANCTIONED_STAMP_MINTER_FLAG
|
|
33
|
+
STAMP_DIRECTORY_GUARD_MESSAGE = _constants_module.STAMP_DIRECTORY_GUARD_MESSAGE
|
|
34
|
+
STAMP_DIRECTORY_NAME = _constants_module.STAMP_DIRECTORY_NAME
|
|
35
|
+
GH_PR_CREATE_INVOCATION_PATTERN = _constants_module.GH_PR_CREATE_INVOCATION_PATTERN
|
|
36
|
+
STAMP_FILE_REFERENCE_BOUNDARY_PREFIX_PATTERN = (
|
|
37
|
+
_constants_module.STAMP_FILE_REFERENCE_BOUNDARY_PREFIX_PATTERN
|
|
38
|
+
)
|
|
39
|
+
STAMP_FILE_ROOT_KEY_JSON_SUFFIX_PATTERN = _constants_module.STAMP_FILE_ROOT_KEY_JSON_SUFFIX_PATTERN
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_gh_pr_create_pattern_matches_create_but_not_edit() -> None:
|
|
43
|
+
invocation_pattern = re.compile(GH_PR_CREATE_INVOCATION_PATTERN, re.IGNORECASE)
|
|
44
|
+
assert invocation_pattern.search("gh pr create --title T --body-file b.md") is not None
|
|
45
|
+
assert invocation_pattern.search("gh pr edit 1 --title T") is None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_stamp_file_pattern_fragments_match_a_stamp_file_reference() -> None:
|
|
49
|
+
stamp_file_pattern = re.compile(
|
|
50
|
+
STAMP_FILE_REFERENCE_BOUNDARY_PREFIX_PATTERN
|
|
51
|
+
+ re.escape(STAMP_DIRECTORY_NAME)
|
|
52
|
+
+ STAMP_FILE_ROOT_KEY_JSON_SUFFIX_PATTERN % SAMPLE_ROOT_KEY_HEX_LENGTH,
|
|
53
|
+
re.IGNORECASE,
|
|
54
|
+
)
|
|
55
|
+
stamp_file_reference = f"cat {STAMP_DIRECTORY_NAME}/{'a' * SAMPLE_ROOT_KEY_HEX_LENGTH}.json"
|
|
56
|
+
assert stamp_file_pattern.search(stamp_file_reference) is not None
|
|
57
|
+
assert stamp_file_pattern.search("cat notes.json") is None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_higher_effort_meets_a_lower_threshold() -> None:
|
|
61
|
+
assert effort_meets_threshold("xhigh", "low") is True
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_lower_effort_fails_a_higher_threshold() -> None:
|
|
65
|
+
assert effort_meets_threshold("low", "xhigh") is False
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def test_equal_effort_meets_the_threshold() -> None:
|
|
69
|
+
assert effort_meets_threshold("low", "low") is True
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_ultra_is_unranked_and_meets_nothing() -> None:
|
|
73
|
+
assert effort_meets_threshold("ultra", "low") is False
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def test_unknown_required_effort_meets_nothing() -> None:
|
|
77
|
+
assert effort_meets_threshold("low", "ultra") is False
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_push_requires_the_lowest_effort_token() -> None:
|
|
81
|
+
every_token_meets_push = all(
|
|
82
|
+
effort_meets_threshold(each_token, PUSH_REQUIRED_EFFORT)
|
|
83
|
+
for each_token in ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER
|
|
84
|
+
)
|
|
85
|
+
assert every_token_meets_push
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_pr_create_threshold_admits_xhigh_but_rejects_high() -> None:
|
|
89
|
+
assert effort_meets_threshold("xhigh", PR_CREATE_REQUIRED_EFFORT) is True
|
|
90
|
+
assert effort_meets_threshold("high", PR_CREATE_REQUIRED_EFFORT) is False
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def test_gated_push_subcommands_gates_push_not_commit() -> None:
|
|
94
|
+
assert "push" in GATED_PUSH_SUBCOMMANDS
|
|
95
|
+
assert "commit" not in GATED_PUSH_SUBCOMMANDS
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_ultra_absent_from_the_ordered_tokens() -> None:
|
|
99
|
+
assert "ultra" not in ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def test_push_gate_message_names_the_push_action_and_low_effort() -> None:
|
|
103
|
+
assert "git push" in PUSH_GATE_CORRECTIVE_MESSAGE
|
|
104
|
+
assert "CODE_REVIEW_PUSH_GATE" in PUSH_GATE_CORRECTIVE_MESSAGE
|
|
105
|
+
assert "'low'" in PUSH_GATE_CORRECTIVE_MESSAGE
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_guard_message_directs_users_to_the_sanctioned_minter_flag() -> None:
|
|
109
|
+
assert SANCTIONED_STAMP_MINTER_FLAG in STAMP_DIRECTORY_GUARD_MESSAGE
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_enforcement_defaults_to_off() -> None:
|
|
113
|
+
assert CODE_REVIEW_ENFORCEMENT_ENABLED is False
|