claude-dev-env 2.11.0 → 2.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/_shared/advisor/reference/sol-rung.md +5 -2
- package/_shared/advisor/reference/warm-up.md +1 -0
- package/_shared/advisor/scripts/codex_sol_advisor.py +96 -31
- package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +5 -0
- package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +173 -11
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
- package/_shared/pr-loop/scripts/test_code_rules_gate.py +0 -26
- package/agents/CLAUDE.md +1 -2
- package/agents/pr-description-writer.md +11 -81
- package/bin/install.mjs +48 -9
- package/bin/install.prune.test.mjs +26 -0
- package/bin/install.settings-defaults.test.mjs +60 -33
- package/bin/install.test.mjs +2 -4
- package/commands/CLAUDE.md +1 -0
- package/commands/sr-loop.md +52 -0
- package/docs/CLAUDE.md +0 -1
- package/docs/references/CLAUDE.md +0 -1
- package/hooks/CLAUDE.md +1 -1
- package/hooks/blocking/CLAUDE.md +2 -27
- package/hooks/blocking/config/CLAUDE.md +1 -13
- package/hooks/blocking/conventional_pr_title_gate.py +1 -2
- package/hooks/blocking/send_user_file_open_locally_blocker.py +1 -1
- package/hooks/blocking/stop_dispatcher.py +4 -4
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -3
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +6 -7
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +4 -3
- package/hooks/blocking/test_shared_stdin_adoption.py +0 -2
- package/hooks/diagnostic/CLAUDE.md +3 -3
- package/hooks/diagnostic/hook_log_extractor.py +2 -36
- package/hooks/diagnostic/hook_log_stop_wrapper.py +6 -155
- package/hooks/diagnostic/test_hook_log_extractor.py +8 -21
- package/hooks/diagnostic/test_hook_log_stop_wrapper.py +3 -331
- package/hooks/git-hooks/git_hooks_constants/__init__.py +0 -6
- package/hooks/git-hooks/pre_push.py +3 -85
- package/hooks/git-hooks/test_pre_push.py +0 -130
- package/hooks/hooks.json +0 -35
- package/hooks/hooks_constants/CLAUDE.md +0 -9
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -11
- package/hooks/hooks_constants/convergence_branch_constants.py +0 -1
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +3 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -8
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +1 -6
- package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -26
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +0 -5
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -1
- package/hooks/lifecycle/CLAUDE.md +1 -3
- package/package.json +1 -1
- package/rules/CLAUDE.md +0 -2
- package/rules/gh-cli-conventions.md +0 -1
- package/rules/git-workflow.md +1 -3
- package/scripts/CLAUDE.md +0 -1
- package/scripts/_code_review_test_support.py +0 -95
- package/scripts/dev_env_scripts_constants/code_review_constants.py +9 -98
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +1 -44
- package/scripts/invoke_code_review.py +8 -451
- package/scripts/test_invoke_code_review.py +5 -61
- package/scripts/test_invoke_code_review_cli.py +1 -45
- package/scripts/test_invoke_code_review_contract.py +1 -118
- package/settings.json +1 -10
- package/skills/CLAUDE.md +8 -0
- package/skills/autoconverge/reference/CLAUDE.md +1 -1
- package/skills/autoconverge/reference/convergence.md +7 -7
- package/skills/autoconverge/reference/gotchas.md +3 -10
- package/skills/autoconverge/workflow/converge.contract.test.mjs +53 -1388
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +4 -3
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +33 -493
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +15 -97
- package/skills/autoconverge/workflow/converge.mjs +201 -238
- package/skills/autoconverge/workflow/converge.precatch.test.mjs +1 -1
- package/skills/bugteam/CONSTRAINTS.md +1 -1
- package/skills/bugteam/SKILL.md +9 -0
- package/skills/bugteam/reference/audit-and-teammates.md +10 -6
- package/skills/bugteam/test_skill_additions.py +9 -0
- package/skills/comments/SKILL.md +65 -0
- package/skills/descriptions/SKILL.md +51 -0
- package/skills/emergencies/SKILL.md +42 -0
- package/skills/imagegen/SKILL.md +21 -0
- package/skills/imagegen/scripts/config/__init__.py +1 -0
- package/skills/imagegen/scripts/config/constants.py +28 -0
- package/skills/imagegen/scripts/imagegen.py +45 -0
- package/skills/imagegen/scripts/imagegen_core.py +528 -0
- package/skills/imagegen/scripts/test_imagegen.py +204 -0
- package/skills/orchestrator/SKILL.md +2 -4
- package/skills/plan-to-pr/SKILL.md +6 -7
- package/skills/plan-to-pr/reference/final-validation-tasks.md +1 -1
- package/skills/plan-to-pr/reference/process-inventory.md +2 -2
- package/skills/plan-to-pr/reference/review-loop.md +2 -2
- package/skills/plan-to-pr/reference/run-record.schema.json +1 -1
- package/skills/plan-to-pr/reference/task-seeds.md +1 -1
- package/skills/plan-to-pr/reference/task-ticket.md +3 -4
- package/skills/plan-to-pr/scripts/config/constants.py +1 -2
- package/skills/plan-to-pr/scripts/test_validate_protocol.py +1 -1
- package/skills/plan-to-pr/test_skill_contract.py +2 -2
- package/skills/pr-converge/SKILL.md +13 -16
- package/skills/pr-converge/reference/progress-checklist.md +6 -5
- package/skills/pr-converge/reference/state-schema.md +1 -27
- package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
- package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +3 -3
- package/skills/pr-loop-lifecycle/SKILL.md +1 -1
- package/skills/pr-loop-lifecycle/reference/teardown-publish-permissions.md +1 -1
- package/skills/prototype/SKILL.md +4 -4
- package/skills/prototype/reference/honest-limitations.md +4 -4
- package/skills/prototype/reference/promotion-tasks.md +2 -2
- package/skills/prototype/workflows/promotion.md +4 -6
- package/skills/reviews/SKILL.md +85 -0
- package/skills/small-cl/SKILL.md +52 -0
- package/skills/usage-pause/SKILL.md +1 -1
- package/skills/usage-pause/scripts/resolve_usage_window.py +32 -4
- package/skills/usage-pause/scripts/test_resolve_usage_window.py +26 -0
- package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +3 -1
- package/agents/code-verifier.md +0 -74
- package/docs/PR_DESCRIPTION_GUIDE.md +0 -157
- package/docs/references/code-review-enforcement.md +0 -132
- package/hooks/blocking/code_review_enforcement_config_bootstrap.py +0 -53
- package/hooks/blocking/code_review_gate_deny.py +0 -74
- package/hooks/blocking/code_review_pr_create_gate.py +0 -198
- package/hooks/blocking/code_review_push_gate.py +0 -145
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +0 -348
- package/hooks/blocking/code_review_stamp_store.py +0 -233
- package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +0 -7
- package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +0 -15
- package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +0 -212
- package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +0 -138
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +0 -49
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +0 -38
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +0 -483
- package/hooks/blocking/config/code_review_enforcement_constants.py +0 -143
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +0 -166
- package/hooks/blocking/config/verified_commit_constants.py +0 -160
- package/hooks/blocking/config/verified_commit_context_constants.py +0 -21
- package/hooks/blocking/config/verified_commit_gate_output_constants.py +0 -14
- package/hooks/blocking/conftest.py +0 -123
- package/hooks/blocking/convergence_gate_blocker.py +0 -310
- package/hooks/blocking/pr_converge_bugteam_enforcer.py +0 -170
- package/hooks/blocking/pr_description_body_audit.py +0 -148
- package/hooks/blocking/pr_description_command_parser.py +0 -237
- package/hooks/blocking/pr_description_enforcer.py +0 -227
- package/hooks/blocking/pr_description_pr_number.py +0 -155
- package/hooks/blocking/pr_description_proof_of_work.py +0 -385
- package/hooks/blocking/pr_description_readability.py +0 -364
- package/hooks/blocking/reviewer_spawn_gate.py +0 -182
- package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +0 -62
- package/hooks/blocking/test_code_review_gate_deny.py +0 -54
- package/hooks/blocking/test_code_review_pr_create_gate.py +0 -199
- package/hooks/blocking/test_code_review_push_gate.py +0 -205
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +0 -199
- package/hooks/blocking/test_code_review_stamp_store.py +0 -205
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +0 -710
- package/hooks/blocking/test_code_verifier_tools_contract.py +0 -28
- package/hooks/blocking/test_convergence_gate_blocker.py +0 -428
- package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +0 -311
- package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +0 -184
- package/hooks/blocking/test_pr_description_enforcer.py +0 -221
- package/hooks/blocking/test_pr_description_enforcer_body_audit.py +0 -247
- package/hooks/blocking/test_pr_description_enforcer_body_rules.py +0 -493
- package/hooks/blocking/test_pr_description_enforcer_command_parser.py +0 -366
- package/hooks/blocking/test_pr_description_enforcer_pr_number.py +0 -159
- package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +0 -175
- package/hooks/blocking/test_pr_description_enforcer_readability.py +0 -443
- package/hooks/blocking/test_pr_description_proof_of_work.py +0 -313
- package/hooks/blocking/test_reviewer_spawn_gate.py +0 -230
- package/hooks/blocking/test_verdict_directory_write_blocker.py +0 -804
- package/hooks/blocking/test_verification_verdict_store.py +0 -974
- package/hooks/blocking/test_verified_commit_config_bootstrap.py +0 -67
- package/hooks/blocking/test_verified_commit_docs_delta.py +0 -176
- package/hooks/blocking/test_verified_commit_gate.py +0 -581
- package/hooks/blocking/test_verified_commit_gate_additional_context.py +0 -134
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +0 -131
- package/hooks/blocking/test_verifier_verdict_minter.py +0 -299
- package/hooks/blocking/tests/test_verified_commit_gate.py +0 -41
- package/hooks/blocking/verdict_directory_write_blocker.py +0 -687
- package/hooks/blocking/verification_verdict_store.py +0 -1039
- package/hooks/blocking/verified_commit_config_bootstrap.py +0 -63
- package/hooks/blocking/verified_commit_gate.py +0 -113
- package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +0 -28
- package/hooks/blocking/verified_commit_gate_parts/__init__.py +0 -1
- package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +0 -174
- package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +0 -53
- package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +0 -80
- package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +0 -170
- package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +0 -217
- package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +0 -10
- package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +0 -94
- package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +0 -17
- package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +0 -38
- package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +0 -71
- package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +0 -96
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +0 -167
- package/hooks/blocking/verifier_verdict_minter.py +0 -280
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +0 -64
- package/hooks/hooks_constants/convergence_gate_blocker_constants.py +0 -53
- package/hooks/hooks_constants/pr_converge_bugteam_enforcer_constants.py +0 -55
- package/hooks/hooks_constants/pr_converge_bugteam_enforcer_state.py +0 -67
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +0 -167
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -107
- package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +0 -41
- package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +0 -292
- package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +0 -198
- package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +0 -283
- package/rules/proof-of-work-pr-comments.md +0 -22
- package/rules/verified-commit-gate-skip.md +0 -4
- package/scripts/Show-Asset.ps1 +0 -106
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
# Code-review enforcement
|
|
2
|
-
|
|
3
|
-
This feature ties two git actions to a clean run of the built-in
|
|
4
|
-
`/code-review --fix`:
|
|
5
|
-
|
|
6
|
-
- **`git push`** needs a clean review at effort **low** or higher.
|
|
7
|
-
- **Pull-request creation** (`gh pr create` and the MCP `create_pull_request`
|
|
8
|
-
tool) needs a clean review at effort **xhigh** or higher.
|
|
9
|
-
|
|
10
|
-
The gates follow the same shape as the `verified_commit` gate family.
|
|
11
|
-
|
|
12
|
-
## Opt-in (default off)
|
|
13
|
-
|
|
14
|
-
Enforcement is **off by default**. Turn it on by setting the environment
|
|
15
|
-
variable `CLAUDE_CODE_REVIEW_ENFORCEMENT` to `1`, `true`, `yes`, or `on`
|
|
16
|
-
(case and surrounding spaces are ignored). Any other value, and an unset
|
|
17
|
-
variable, leave enforcement off.
|
|
18
|
-
|
|
19
|
-
Set the variable in the machine's own user environment so every gate reads it,
|
|
20
|
-
including the native git pre-push backstop, which git runs in the shell's
|
|
21
|
-
environment. An `env` block in `settings.json` reaches the three Claude Code
|
|
22
|
-
hook gates alone. Each gate process reads the variable as it starts, so a
|
|
23
|
-
Claude Code session already running keeps its current setting until it
|
|
24
|
-
restarts, while the git pre-push backstop picks up the current shell
|
|
25
|
-
environment on each push.
|
|
26
|
-
|
|
27
|
-
A user-environment setting arms the gates machine-wide. The backstop installs
|
|
28
|
-
through the shared `core.hooksPath`, so it runs for every repository on the
|
|
29
|
-
machine, and the gates carry no per-repository allowlist. Each repository then
|
|
30
|
-
needs its own clean stamp before a push lands there.
|
|
31
|
-
|
|
32
|
-
The variable feeds the master flag `CODE_REVIEW_ENFORCEMENT_ENABLED` in
|
|
33
|
-
`hooks/blocking/config/code_review_enforcement_constants.py`, which every gate
|
|
34
|
-
reads at start-up. When it is on, the push gate, the PR-create gate, the native
|
|
35
|
-
pre-push backstop (via the shared deny decision), and the stamp-directory write
|
|
36
|
-
blocker all enforce. When it is off, every gate allows the action and the
|
|
37
|
-
write-blocker allows stamp-directory access.
|
|
38
|
-
|
|
39
|
-
`npx claude-dev-env` copies the shipped `hooks/` tree over `~/.claude/` on each
|
|
40
|
-
install, so the environment setting survives an install and an edit to the
|
|
41
|
-
constant does not.
|
|
42
|
-
|
|
43
|
-
## How a stamp works
|
|
44
|
-
|
|
45
|
-
A stamp is a small JSON file that records one fact: a clean `/code-review` pass
|
|
46
|
-
ran against an exact branch surface at a given effort. Each work tree keeps one
|
|
47
|
-
file under `~/.claude/code-review-stamps/`, named by a hash of the resolved
|
|
48
|
-
work-tree path.
|
|
49
|
-
|
|
50
|
-
The stamp binds to a **branch-surface hash** — the hash of every changed path
|
|
51
|
-
and untracked file, each bound by its content digest, measured against the
|
|
52
|
-
merge base. When any byte of the change surface moves, the live hash stops
|
|
53
|
-
matching the stored hash, so the stamp stops covering the surface and the gate
|
|
54
|
-
asks for a fresh review.
|
|
55
|
-
|
|
56
|
-
A gate allows the action only when a stored stamp matches the live hash exactly
|
|
57
|
-
and its effort ranks at or above the effort the action needs. A missing,
|
|
58
|
-
unreadable, or malformed stamp reads as no coverage, so the gate fails closed.
|
|
59
|
-
|
|
60
|
-
## The single sanctioned minter
|
|
61
|
-
|
|
62
|
-
Only `invoke_code_review.py --record-stamp` writes a stamp. It forces a headless
|
|
63
|
-
`/code-review <effort> --fix` run, then mints a stamp only when the review
|
|
64
|
-
returns a clean exit code and leaves the branch surface unchanged in the same
|
|
65
|
-
pass. A pass that applies fixes mints nothing; the run loops on the new surface
|
|
66
|
-
up to a capped number of passes and mints only on a stable clean pass.
|
|
67
|
-
|
|
68
|
-
## Two layers guard the stamp directory
|
|
69
|
-
|
|
70
|
-
The gates trust one rule: only the sanctioned minter writes stamp files. Two
|
|
71
|
-
layers hold that rule.
|
|
72
|
-
|
|
73
|
-
1. **File-tool deny in `settings.json`.** `Write`, `Edit`, and `MultiEdit`
|
|
74
|
-
under `~/.claude/code-review-stamps/` are denied. This layer covers work
|
|
75
|
-
inside the repository. The installer merges hook groups into a user's
|
|
76
|
-
`settings.json` and does not ship this package's `permissions.deny`, so on a
|
|
77
|
-
user's machine this layer protects contributor work, at parity with the
|
|
78
|
-
`verified_commit` gate's own file-tool deny.
|
|
79
|
-
2. **`code_review_stamp_directory_write_blocker` hook.** This hook ships through
|
|
80
|
-
`hooks.json`, so it reaches every install. It has two arms:
|
|
81
|
-
- a shell arm that denies any Bash or PowerShell command naming the stamp
|
|
82
|
-
directory, or importing the stamp store module, or calling its mint
|
|
83
|
-
function — while it lets the sanctioned minter command through;
|
|
84
|
-
- a file-tool arm that denies any `Write`, `Edit`, or `MultiEdit` whose path
|
|
85
|
-
resolves under the stamp directory. This arm closes the plain file-tool
|
|
86
|
-
forge on every shipped install, which the package `settings.json` deny
|
|
87
|
-
cannot reach on its own.
|
|
88
|
-
|
|
89
|
-
## What the gates block
|
|
90
|
-
|
|
91
|
-
- **Casual and accidental forges.** A plain file-tool write to the stamp
|
|
92
|
-
directory, and a casual shell write to it, are both denied.
|
|
93
|
-
- **Hidden-path and split-step shell forges.** A shell command that assembles
|
|
94
|
-
the stamp path from hex, base64, or character math is decoded and denied. A
|
|
95
|
-
command that splits the directory change across steps to walk into the stamp
|
|
96
|
-
directory is traced and denied.
|
|
97
|
-
- **Lazy skips.** A push or a pull-request creation cannot go ahead without a
|
|
98
|
-
stamp that matches the live surface at the needed effort.
|
|
99
|
-
|
|
100
|
-
## What the gates do not block
|
|
101
|
-
|
|
102
|
-
The chain-mode `/code-review` runs as a subprocess spawn of the `claude`
|
|
103
|
-
binary, not a harness-recorded subagent, so there is no signed sidecar to
|
|
104
|
-
anchor a forgery-proof mint. The stamp reaches the same posture the
|
|
105
|
-
`verified_commit` gate holds, and no further. These bypass surfaces stay open:
|
|
106
|
-
|
|
107
|
-
- **Pull requests that skip the tool paths.** A PR opened through
|
|
108
|
-
`gh api -X POST .../pulls` or the GitHub web page never triggers the
|
|
109
|
-
create-PR gate.
|
|
110
|
-
- **`git push --no-verify`.** This flag tells git to skip the native pre-push
|
|
111
|
-
hook, so the native backstop does not run.
|
|
112
|
-
- **A push that clears the enable variable.** The backstop reads
|
|
113
|
-
`CLAUDE_CODE_REVIEW_ENFORCEMENT` from the shell that runs the push, so
|
|
114
|
-
`CLAUDE_CODE_REVIEW_ENFORCEMENT=0 git push` reads as enforcement off and the
|
|
115
|
-
backstop allows the push with no flag and no bypass marker.
|
|
116
|
-
- **A rebuilt store.** A script that re-implements the stamp store in memory
|
|
117
|
-
and writes a matching file can mint a stamp the gates accept.
|
|
118
|
-
|
|
119
|
-
In short: these gates stop casual forges and lazy skips. They do not stop a
|
|
120
|
-
determined attacker who sets out to defeat them.
|
|
121
|
-
|
|
122
|
-
## Where the pieces live
|
|
123
|
-
|
|
124
|
-
- Gates: `hooks/blocking/code_review_push_gate.py`,
|
|
125
|
-
`hooks/blocking/code_review_pr_create_gate.py`.
|
|
126
|
-
- Stamp store: `hooks/blocking/code_review_stamp_store.py`.
|
|
127
|
-
- Directory guard: `hooks/blocking/code_review_stamp_directory_write_blocker.py`.
|
|
128
|
-
- Shared constants: `hooks/blocking/config/code_review_enforcement_constants.py`.
|
|
129
|
-
- Native backstop: `hooks/git-hooks/pre_push.py` reuses the push gate's
|
|
130
|
-
`deny_reason_for_directory` so the native hook and the Claude gate share one
|
|
131
|
-
decision source.
|
|
132
|
-
- Minter: `scripts/invoke_code_review.py --record-stamp`.
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
"""Deterministic loader for the code-review enforcement constants module.
|
|
2
|
-
|
|
3
|
-
The stamp store and every code-review gate import their shared constants as
|
|
4
|
-
``from config.code_review_enforcement_constants import ...``. In the installed
|
|
5
|
-
hook tree a second, unrelated ``config`` package can sit ahead of this package
|
|
6
|
-
on ``sys.path`` and win that dotted name by path order, so the import binds to
|
|
7
|
-
the wrong file and raises ImportError on any constant the stale copy lacks.
|
|
8
|
-
This module binds the dotted name to its sibling ``config/`` file by explicit
|
|
9
|
-
location, so resolution never depends on ``sys.path`` order.
|
|
10
|
-
"""
|
|
11
|
-
|
|
12
|
-
from __future__ import annotations
|
|
13
|
-
|
|
14
|
-
import importlib.util
|
|
15
|
-
import sys
|
|
16
|
-
from pathlib import Path
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def register_code_review_enforcement_constants() -> None:
|
|
20
|
-
"""Bind the code-review enforcement constants dotted name to its config file.
|
|
21
|
-
|
|
22
|
-
::
|
|
23
|
-
|
|
24
|
-
sys.path = ["<installed hooks>", "<blocking>"] # foreign config first
|
|
25
|
-
register_code_review_enforcement_constants()
|
|
26
|
-
from config.code_review_enforcement_constants import STAMP_DIRECTORY_NAME
|
|
27
|
-
ok: bound to <blocking>/config/code_review_enforcement_constants.py
|
|
28
|
-
|
|
29
|
-
A ``from config.code_review_enforcement_constants import`` that follows this
|
|
30
|
-
call reads the entry straight from ``sys.modules``, so it resolves to this
|
|
31
|
-
file whatever else owns the ``config`` name. An entry already cached is left
|
|
32
|
-
in place, so the call is idempotent and never displaces a module a caller
|
|
33
|
-
loaded on purpose.
|
|
34
|
-
|
|
35
|
-
Returns:
|
|
36
|
-
None. The effect is the ``sys.modules`` registration.
|
|
37
|
-
"""
|
|
38
|
-
_bind_config_module_by_location(
|
|
39
|
-
"config.code_review_enforcement_constants",
|
|
40
|
-
Path(__file__).resolve().parent / "config" / "code_review_enforcement_constants.py",
|
|
41
|
-
)
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def _bind_config_module_by_location(dotted_name: str, constants_file_path: Path) -> None:
|
|
45
|
-
"""Register a dotted config name against an explicit file path in sys.modules."""
|
|
46
|
-
if dotted_name in sys.modules:
|
|
47
|
-
return
|
|
48
|
-
module_spec = importlib.util.spec_from_file_location(dotted_name, constants_file_path)
|
|
49
|
-
if module_spec is None or module_spec.loader is None:
|
|
50
|
-
return
|
|
51
|
-
constants_module = importlib.util.module_from_spec(module_spec)
|
|
52
|
-
sys.modules[dotted_name] = constants_module
|
|
53
|
-
module_spec.loader.exec_module(constants_module)
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
"""Shared deny scaffold for the code-review PreToolUse gates.
|
|
2
|
-
|
|
3
|
-
The push gate and the PR-create gate deny a blocked tool call the same way: a
|
|
4
|
-
``hookSpecificOutput`` payload carrying the deny decision and a corrective
|
|
5
|
-
reason, logged through ``log_hook_block`` before it reaches stdout. This module
|
|
6
|
-
holds that one payload builder and the log-and-emit helper, so the two gates
|
|
7
|
-
share a single deny shape and stay in step.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
from __future__ import annotations
|
|
11
|
-
|
|
12
|
-
import json
|
|
13
|
-
import sys
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
|
|
16
|
-
try:
|
|
17
|
-
_blocking_directory = str(Path(__file__).resolve().parent)
|
|
18
|
-
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
19
|
-
for each_bootstrap_directory in (_blocking_directory, _hooks_directory):
|
|
20
|
-
if each_bootstrap_directory not in sys.path:
|
|
21
|
-
sys.path.insert(0, each_bootstrap_directory)
|
|
22
|
-
|
|
23
|
-
from code_review_enforcement_config_bootstrap import (
|
|
24
|
-
register_code_review_enforcement_constants,
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
register_code_review_enforcement_constants()
|
|
28
|
-
|
|
29
|
-
from config.code_review_enforcement_constants import (
|
|
30
|
-
DENY_PERMISSION_DECISION,
|
|
31
|
-
PRE_TOOL_USE_HOOK_EVENT_NAME,
|
|
32
|
-
)
|
|
33
|
-
|
|
34
|
-
from hooks_constants.hook_block_logger import log_hook_block
|
|
35
|
-
except ImportError as import_error:
|
|
36
|
-
raise ImportError(
|
|
37
|
-
"the code_review_gate_deny dependencies did not import; "
|
|
38
|
-
"ensure the hooks directory is importable."
|
|
39
|
-
) from import_error
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def build_code_review_deny_payload(deny_reason: str) -> dict[str, dict[str, str]]:
|
|
43
|
-
"""Build the PreToolUse deny payload for a blocked code-review gate action.
|
|
44
|
-
|
|
45
|
-
Args:
|
|
46
|
-
deny_reason: The corrective message naming why the action is denied.
|
|
47
|
-
|
|
48
|
-
Returns:
|
|
49
|
-
The ``hookSpecificOutput`` deny payload.
|
|
50
|
-
"""
|
|
51
|
-
return {
|
|
52
|
-
"hookSpecificOutput": {
|
|
53
|
-
"hookEventName": PRE_TOOL_USE_HOOK_EVENT_NAME,
|
|
54
|
-
"permissionDecision": DENY_PERMISSION_DECISION,
|
|
55
|
-
"permissionDecisionReason": deny_reason,
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def log_and_emit_code_review_deny(deny_reason: str, tool_name: str, hook_module_name: str) -> None:
|
|
61
|
-
"""Log a code-review gate block and write its deny payload to stdout.
|
|
62
|
-
|
|
63
|
-
Args:
|
|
64
|
-
deny_reason: The corrective message naming why the action is denied.
|
|
65
|
-
tool_name: The name of the gated tool, recorded in the block log.
|
|
66
|
-
hook_module_name: The calling gate's module name, recorded in the log.
|
|
67
|
-
"""
|
|
68
|
-
log_hook_block(
|
|
69
|
-
calling_hook_name=hook_module_name,
|
|
70
|
-
hook_event=PRE_TOOL_USE_HOOK_EVENT_NAME,
|
|
71
|
-
block_reason=deny_reason,
|
|
72
|
-
tool_name=tool_name if isinstance(tool_name, str) else None,
|
|
73
|
-
)
|
|
74
|
-
sys.stdout.write(json.dumps(build_code_review_deny_payload(deny_reason)) + "\n")
|
|
@@ -1,198 +0,0 @@
|
|
|
1
|
-
"""PreToolUse gate: PR create lands only behind a clean xhigh code-review stamp.
|
|
2
|
-
|
|
3
|
-
Fires when ``CODE_REVIEW_ENFORCEMENT_ENABLED`` is on (default off). Runs on
|
|
4
|
-
Bash/PowerShell ``gh pr create`` and on the MCP ``create_pull_request``
|
|
5
|
-
tool. Resolves the repository from the session
|
|
6
|
-
working directory (or the payload cwd), computes the live change-surface
|
|
7
|
-
hash, and allows the create only when a clean stamp at effort ``xhigh`` or
|
|
8
|
-
higher covers that exact hash under ``~/.claude/code-review-stamps/``.
|
|
9
|
-
|
|
10
|
-
Empty surfaces and repos with no resolvable upstream base are out of scope
|
|
11
|
-
and allowed. Push lands behind ``code_review_push_gate`` and commit behind
|
|
12
|
-
``verified_commit_gate``; this gate covers pull-request creation only.
|
|
13
|
-
"""
|
|
14
|
-
|
|
15
|
-
from __future__ import annotations
|
|
16
|
-
|
|
17
|
-
import json
|
|
18
|
-
import re
|
|
19
|
-
import sys
|
|
20
|
-
from pathlib import Path
|
|
21
|
-
|
|
22
|
-
try:
|
|
23
|
-
_blocking_directory = str(Path(__file__).resolve().parent)
|
|
24
|
-
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
25
|
-
for each_bootstrap_directory in (_blocking_directory, _hooks_directory):
|
|
26
|
-
if each_bootstrap_directory not in sys.path:
|
|
27
|
-
sys.path.insert(0, each_bootstrap_directory)
|
|
28
|
-
|
|
29
|
-
from code_review_enforcement_config_bootstrap import (
|
|
30
|
-
register_code_review_enforcement_constants,
|
|
31
|
-
)
|
|
32
|
-
from verified_commit_config_bootstrap import register_verified_commit_constants
|
|
33
|
-
|
|
34
|
-
register_code_review_enforcement_constants()
|
|
35
|
-
register_verified_commit_constants()
|
|
36
|
-
|
|
37
|
-
from code_review_gate_deny import (
|
|
38
|
-
build_code_review_deny_payload,
|
|
39
|
-
log_and_emit_code_review_deny,
|
|
40
|
-
)
|
|
41
|
-
from code_review_stamp_store import live_surface_hash, stamp_covers_surface
|
|
42
|
-
from config.code_review_enforcement_constants import (
|
|
43
|
-
ALL_GATED_SHELL_TOOL_NAMES,
|
|
44
|
-
CODE_REVIEW_ENFORCEMENT_ENABLED,
|
|
45
|
-
GH_PR_CREATE_INVOCATION_PATTERN,
|
|
46
|
-
HASH_PREVIEW_LENGTH,
|
|
47
|
-
MCP_CREATE_PULL_REQUEST_TOOL_NAME,
|
|
48
|
-
PR_CREATE_GATE_CORRECTIVE_MESSAGE,
|
|
49
|
-
PR_CREATE_GATE_HOOK_MODULE_NAME,
|
|
50
|
-
PR_CREATE_REQUIRED_EFFORT,
|
|
51
|
-
)
|
|
52
|
-
from verification_verdict_store import resolve_repo_root
|
|
53
|
-
except ImportError as import_error:
|
|
54
|
-
raise ImportError(
|
|
55
|
-
"the code_review_pr_create_gate dependencies did not import; "
|
|
56
|
-
"ensure the hooks directory is importable."
|
|
57
|
-
) from import_error
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
def _strip_quoted_regions(command_text: str) -> str:
|
|
61
|
-
"""Blank out single- and double-quoted spans so prose is not gated."""
|
|
62
|
-
quote_pattern = re.compile(r"\"[^\"]*\"|'[^']*'")
|
|
63
|
-
return quote_pattern.sub(lambda each_match: " " * len(each_match.group(0)), command_text)
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
def command_invokes_gh_pr_create(command_text: str) -> bool:
|
|
67
|
-
"""Decide whether a shell command invokes ``gh pr create``.
|
|
68
|
-
|
|
69
|
-
::
|
|
70
|
-
|
|
71
|
-
gh pr create --title T --body-file b.md -> True
|
|
72
|
-
echo "gh pr create" -> False (quoted prose)
|
|
73
|
-
gh pr edit 1 --title T -> False
|
|
74
|
-
|
|
75
|
-
Args:
|
|
76
|
-
command_text: The raw command string from the tool payload.
|
|
77
|
-
|
|
78
|
-
Returns:
|
|
79
|
-
True when the command contains a real ``gh pr create`` invocation.
|
|
80
|
-
"""
|
|
81
|
-
quote_stripped_command = _strip_quoted_regions(command_text)
|
|
82
|
-
invocation_pattern = re.compile(GH_PR_CREATE_INVOCATION_PATTERN, re.IGNORECASE)
|
|
83
|
-
return invocation_pattern.search(quote_stripped_command) is not None
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def is_mcp_create_pull_request_tool(tool_name: str) -> bool:
|
|
87
|
-
"""Decide whether a tool name is the MCP create-pull-request tool.
|
|
88
|
-
|
|
89
|
-
Args:
|
|
90
|
-
tool_name: The PreToolUse ``tool_name`` field.
|
|
91
|
-
|
|
92
|
-
Returns:
|
|
93
|
-
True when the name is the configured MCP create-PR tool.
|
|
94
|
-
"""
|
|
95
|
-
return tool_name == MCP_CREATE_PULL_REQUEST_TOOL_NAME
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
def deny_reason_for_directory(target_directory: str) -> str | None:
|
|
99
|
-
"""Decide whether a PR create from a directory must be blocked.
|
|
100
|
-
|
|
101
|
-
::
|
|
102
|
-
|
|
103
|
-
enforcement off / no repo / empty surface / covering xhigh stamp -> None
|
|
104
|
-
production surface without xhigh-or-higher stamp -> corrective (deny)
|
|
105
|
-
|
|
106
|
-
Args:
|
|
107
|
-
target_directory: The session or repo directory the create targets.
|
|
108
|
-
|
|
109
|
-
Returns:
|
|
110
|
-
The deny reason when the surface needs a clean xhigh stamp and none
|
|
111
|
-
covers it; None when the create may proceed.
|
|
112
|
-
"""
|
|
113
|
-
if not CODE_REVIEW_ENFORCEMENT_ENABLED:
|
|
114
|
-
return None
|
|
115
|
-
repo_root = resolve_repo_root(target_directory)
|
|
116
|
-
if repo_root is None:
|
|
117
|
-
return None
|
|
118
|
-
surface_hash = live_surface_hash(repo_root)
|
|
119
|
-
if surface_hash is None:
|
|
120
|
-
return None
|
|
121
|
-
if stamp_covers_surface(repo_root, surface_hash, PR_CREATE_REQUIRED_EFFORT):
|
|
122
|
-
return None
|
|
123
|
-
hash_preview = surface_hash[:HASH_PREVIEW_LENGTH]
|
|
124
|
-
return (
|
|
125
|
-
f"{PR_CREATE_GATE_CORRECTIVE_MESSAGE} (repo: {repo_root}, surface sha256 {hash_preview}...)"
|
|
126
|
-
)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def _deny_for_directory_or_none(
|
|
130
|
-
session_directory: str,
|
|
131
|
-
) -> dict[str, dict[str, str]] | None:
|
|
132
|
-
"""Return the deny payload for a directory that lacks a covering stamp."""
|
|
133
|
-
deny_reason = deny_reason_for_directory(session_directory)
|
|
134
|
-
if deny_reason is None:
|
|
135
|
-
return None
|
|
136
|
-
return build_code_review_deny_payload(deny_reason)
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def _shell_command_targets_pr_create(all_pretooluse_payload: dict[str, object]) -> bool:
|
|
140
|
-
"""Decide whether a shell payload carries a real ``gh pr create``."""
|
|
141
|
-
tool_input = all_pretooluse_payload.get("tool_input", {})
|
|
142
|
-
if not isinstance(tool_input, dict):
|
|
143
|
-
return False
|
|
144
|
-
command_text = tool_input.get("command", "")
|
|
145
|
-
if not isinstance(command_text, str) or not command_text:
|
|
146
|
-
return False
|
|
147
|
-
return command_invokes_gh_pr_create(command_text)
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
def decision_for_payload(
|
|
151
|
-
all_pretooluse_payload: dict[str, object],
|
|
152
|
-
) -> dict[str, dict[str, str]] | None:
|
|
153
|
-
"""Build the deny decision for a gated PR-create attempt.
|
|
154
|
-
|
|
155
|
-
Args:
|
|
156
|
-
all_pretooluse_payload: The PreToolUse hook payload.
|
|
157
|
-
|
|
158
|
-
Returns:
|
|
159
|
-
The deny decision mapping when the create lacks a covering xhigh
|
|
160
|
-
stamp; None when the create may proceed.
|
|
161
|
-
"""
|
|
162
|
-
tool_name = all_pretooluse_payload.get("tool_name", "")
|
|
163
|
-
if not isinstance(tool_name, str):
|
|
164
|
-
return None
|
|
165
|
-
session_directory = all_pretooluse_payload.get("cwd", ".")
|
|
166
|
-
if not isinstance(session_directory, str):
|
|
167
|
-
session_directory = "."
|
|
168
|
-
if is_mcp_create_pull_request_tool(tool_name):
|
|
169
|
-
return _deny_for_directory_or_none(session_directory)
|
|
170
|
-
if tool_name not in ALL_GATED_SHELL_TOOL_NAMES:
|
|
171
|
-
return None
|
|
172
|
-
if not _shell_command_targets_pr_create(all_pretooluse_payload):
|
|
173
|
-
return None
|
|
174
|
-
return _deny_for_directory_or_none(session_directory)
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
def main() -> None:
|
|
178
|
-
"""Read the PreToolUse payload and decide whether to allow the PR create."""
|
|
179
|
-
try:
|
|
180
|
-
pretooluse_payload = json.load(sys.stdin)
|
|
181
|
-
except json.JSONDecodeError:
|
|
182
|
-
return
|
|
183
|
-
deny_decision = decision_for_payload(pretooluse_payload)
|
|
184
|
-
if deny_decision is None:
|
|
185
|
-
return
|
|
186
|
-
tool_name = pretooluse_payload.get("tool_name", "")
|
|
187
|
-
deny_reason = deny_decision["hookSpecificOutput"]["permissionDecisionReason"]
|
|
188
|
-
if not isinstance(deny_reason, str):
|
|
189
|
-
return
|
|
190
|
-
log_and_emit_code_review_deny(
|
|
191
|
-
deny_reason,
|
|
192
|
-
tool_name if isinstance(tool_name, str) else "",
|
|
193
|
-
PR_CREATE_GATE_HOOK_MODULE_NAME,
|
|
194
|
-
)
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
if __name__ == "__main__":
|
|
198
|
-
main()
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
"""PreToolUse gate: git push lands only behind a clean low code-review stamp.
|
|
2
|
-
|
|
3
|
-
Fires on Bash/PowerShell tool calls when ``CODE_REVIEW_ENFORCEMENT_ENABLED``
|
|
4
|
-
is on (default off). When the command carries a ``git push``, the gate
|
|
5
|
-
resolves each repository the push targets, computes the live
|
|
6
|
-
change-surface hash against the merge base, and allows the push only when a
|
|
7
|
-
clean stamp at effort ``low`` or higher covers that exact hash under
|
|
8
|
-
``~/.claude/code-review-stamps/``.
|
|
9
|
-
|
|
10
|
-
Allowed without a stamp:
|
|
11
|
-
|
|
12
|
-
- a command carrying the bypass marker (``# code-review-skip``) as a trailing
|
|
13
|
-
shell comment,
|
|
14
|
-
- a repository with no resolvable upstream base,
|
|
15
|
-
- a mechanically exempt surface (docs and image files, pytest test files, and
|
|
16
|
-
Python files whose docstring-stripped AST is unchanged), and
|
|
17
|
-
- a surface a clean ``low``-or-higher stamp already covers.
|
|
18
|
-
|
|
19
|
-
Commit lands behind ``verified_commit_gate``; only push is gated here.
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
from __future__ import annotations
|
|
23
|
-
|
|
24
|
-
import json
|
|
25
|
-
import sys
|
|
26
|
-
from pathlib import Path
|
|
27
|
-
|
|
28
|
-
try:
|
|
29
|
-
_blocking_directory = str(Path(__file__).resolve().parent)
|
|
30
|
-
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
31
|
-
for each_bootstrap_directory in (_blocking_directory, _hooks_directory):
|
|
32
|
-
if each_bootstrap_directory not in sys.path:
|
|
33
|
-
sys.path.insert(0, each_bootstrap_directory)
|
|
34
|
-
|
|
35
|
-
from code_review_enforcement_config_bootstrap import (
|
|
36
|
-
register_code_review_enforcement_constants,
|
|
37
|
-
)
|
|
38
|
-
from verified_commit_config_bootstrap import register_verified_commit_constants
|
|
39
|
-
|
|
40
|
-
register_code_review_enforcement_constants()
|
|
41
|
-
register_verified_commit_constants()
|
|
42
|
-
|
|
43
|
-
from code_review_gate_deny import log_and_emit_code_review_deny
|
|
44
|
-
from code_review_stamp_store import live_surface_hash, stamp_covers_surface
|
|
45
|
-
from config.code_review_enforcement_constants import (
|
|
46
|
-
ALL_GATED_SHELL_TOOL_NAMES,
|
|
47
|
-
CODE_REVIEW_BYPASS_MARKER,
|
|
48
|
-
CODE_REVIEW_ENFORCEMENT_ENABLED,
|
|
49
|
-
GATED_PUSH_SUBCOMMANDS,
|
|
50
|
-
HASH_PREVIEW_LENGTH,
|
|
51
|
-
PUSH_GATE_CORRECTIVE_MESSAGE,
|
|
52
|
-
PUSH_GATE_HOOK_MODULE_NAME,
|
|
53
|
-
PUSH_REQUIRED_EFFORT,
|
|
54
|
-
)
|
|
55
|
-
from verification_verdict_store import (
|
|
56
|
-
is_verification_exempt_diff,
|
|
57
|
-
resolve_merge_base,
|
|
58
|
-
resolve_repo_root,
|
|
59
|
-
)
|
|
60
|
-
from verified_commit_gate_parts.command_tokenization import (
|
|
61
|
-
command_carries_trailing_marker,
|
|
62
|
-
)
|
|
63
|
-
from verified_commit_gate_parts.gated_invocations import gated_repo_directories
|
|
64
|
-
except ImportError as import_error:
|
|
65
|
-
raise ImportError(
|
|
66
|
-
"the code_review_push_gate dependencies did not import; "
|
|
67
|
-
"ensure the hooks directory is importable."
|
|
68
|
-
) from import_error
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def deny_reason_for_directory(target_directory: str) -> str | None:
|
|
72
|
-
"""Decide whether a push from a directory must be blocked.
|
|
73
|
-
|
|
74
|
-
Allowed when enforcement is off, the directory is no repo, has no upstream
|
|
75
|
-
base, is a mechanically exempt surface, or a clean low-or-higher stamp
|
|
76
|
-
covers it.
|
|
77
|
-
|
|
78
|
-
Args:
|
|
79
|
-
target_directory: The directory the push targets.
|
|
80
|
-
|
|
81
|
-
Returns:
|
|
82
|
-
The deny reason when the surface lacks a covering low stamp, else None.
|
|
83
|
-
"""
|
|
84
|
-
if not CODE_REVIEW_ENFORCEMENT_ENABLED:
|
|
85
|
-
return None
|
|
86
|
-
repo_root = resolve_repo_root(target_directory)
|
|
87
|
-
if repo_root is None:
|
|
88
|
-
return None
|
|
89
|
-
merge_base_sha = resolve_merge_base(repo_root)
|
|
90
|
-
if merge_base_sha is None:
|
|
91
|
-
return None
|
|
92
|
-
if is_verification_exempt_diff(repo_root, merge_base_sha):
|
|
93
|
-
return None
|
|
94
|
-
surface_hash = live_surface_hash(repo_root)
|
|
95
|
-
if surface_hash is None:
|
|
96
|
-
return None
|
|
97
|
-
if stamp_covers_surface(repo_root, surface_hash, PUSH_REQUIRED_EFFORT):
|
|
98
|
-
return None
|
|
99
|
-
hash_preview = surface_hash[:HASH_PREVIEW_LENGTH]
|
|
100
|
-
return f"{PUSH_GATE_CORRECTIVE_MESSAGE} (repo: {repo_root}, surface sha256 {hash_preview}...)"
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
def _emit_first_denied_directory(command_text: str, session_directory: str, tool_name: str) -> None:
|
|
104
|
-
"""Deny the push against the first target directory that lacks a stamp."""
|
|
105
|
-
all_target_directories = gated_repo_directories(
|
|
106
|
-
command_text, session_directory, all_gated_subcommands=GATED_PUSH_SUBCOMMANDS
|
|
107
|
-
)
|
|
108
|
-
for each_target_directory in all_target_directories:
|
|
109
|
-
deny_reason = deny_reason_for_directory(each_target_directory)
|
|
110
|
-
if deny_reason is None:
|
|
111
|
-
continue
|
|
112
|
-
log_and_emit_code_review_deny(deny_reason, tool_name, PUSH_GATE_HOOK_MODULE_NAME)
|
|
113
|
-
return
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def main() -> None:
|
|
117
|
-
"""Read the PreToolUse payload and decide whether to allow the push.
|
|
118
|
-
|
|
119
|
-
Allows the command without a stamp when it carries the bypass marker
|
|
120
|
-
(``CODE_REVIEW_BYPASS_MARKER``) as a genuine trailing shell comment;
|
|
121
|
-
otherwise denies a push whose surface lacks a covering low stamp.
|
|
122
|
-
"""
|
|
123
|
-
try:
|
|
124
|
-
pretooluse_payload = json.load(sys.stdin)
|
|
125
|
-
except json.JSONDecodeError:
|
|
126
|
-
return
|
|
127
|
-
tool_name = pretooluse_payload.get("tool_name", "")
|
|
128
|
-
if tool_name not in ALL_GATED_SHELL_TOOL_NAMES:
|
|
129
|
-
return
|
|
130
|
-
tool_input = pretooluse_payload.get("tool_input", {})
|
|
131
|
-
if not isinstance(tool_input, dict):
|
|
132
|
-
return
|
|
133
|
-
command_text = tool_input.get("command", "")
|
|
134
|
-
if not isinstance(command_text, str) or not command_text:
|
|
135
|
-
return
|
|
136
|
-
if command_carries_trailing_marker(command_text, CODE_REVIEW_BYPASS_MARKER):
|
|
137
|
-
return
|
|
138
|
-
session_directory = pretooluse_payload.get("cwd", ".")
|
|
139
|
-
if not isinstance(session_directory, str):
|
|
140
|
-
session_directory = "."
|
|
141
|
-
_emit_first_denied_directory(command_text, session_directory, tool_name)
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
if __name__ == "__main__":
|
|
145
|
-
main()
|