claude-dev-env 2.2.1 → 2.4.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 +3 -2
- package/_shared/advisor/advisor-protocol.md +2 -2
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -284
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +3 -3
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -0
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +5 -0
- package/_shared/pr-loop/scripts/stale_worktree_rule_sweep.py +107 -0
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +2 -0
- package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +55 -73
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +9 -15
- package/_shared/pr-loop/scripts/tests/test_grant_project_claude_permissions.py +91 -1
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +88 -1
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -0
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -0
- package/_shared/pr-loop/worker-spawn.md +1 -1
- package/agents/CLAUDE.md +1 -0
- package/agents/code-verifier.md +4 -4
- package/agents/skill-writer-agent.md +84 -0
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +141 -41
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
- package/docs/CLAUDE.md +1 -0
- package/docs/references/CLAUDE.md +1 -0
- package/docs/references/code-review-enforcement.md +97 -0
- package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
- package/hooks/blocking/CLAUDE.md +8 -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 +194 -0
- package/hooks/blocking/code_review_push_gate.py +140 -0
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +340 -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/code_review_enforcement_constants.py +110 -0
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +108 -0
- package/hooks/blocking/config/verified_commit_constants.py +24 -9
- 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/pr_description_proof_of_work.py +52 -34
- 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 +185 -0
- package/hooks/blocking/test_code_review_push_gate.py +189 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +180 -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_code_verifier_tools_contract.py +28 -0
- 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_pr_description_proof_of_work.py +151 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +8 -8
- package/hooks/blocking/test_verification_verdict_store.py +920 -810
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
- package/hooks/blocking/verification_verdict_store.py +118 -5
- 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/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 +103 -0
- package/hooks/hooks.json +16 -1
- package/hooks/hooks_constants/CLAUDE.md +1 -0
- 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/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/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/validators/CLAUDE.md +1 -0
- package/hooks/validators/mypy_integration.py +63 -50
- package/hooks/validators/pyproject_config_discovery.py +101 -0
- package/hooks/validators/ruff_integration.py +211 -23
- package/hooks/validators/run_all_validators.py +21 -14
- package/hooks/validators/test_mypy_integration.py +32 -0
- package/hooks/validators/test_pyproject_config_discovery.py +94 -0
- package/hooks/validators/test_ruff_integration.py +68 -1
- package/hooks/validators/test_run_all_validators.py +25 -0
- package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
- package/package.json +1 -1
- package/rules/docstring-prose-matches-implementation.md +20 -43
- package/rules/durable-post-artifacts.md +7 -0
- package/scripts/CLAUDE.md +2 -1
- package/scripts/claude-chain.example.json +15 -3
- package/scripts/claude_chain_runner.py +130 -27
- package/scripts/claude_chain_usage.py +346 -0
- package/scripts/codec_forwarding_test_support.py +83 -0
- package/scripts/conftest.py +8 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -3
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +57 -2
- package/scripts/dev_env_scripts_constants/claude_chain_usage_constants.py +58 -0
- 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/dev_env_scripts_constants/timing.py +1 -1
- package/scripts/invoke_code_review.py +550 -38
- package/scripts/resolve_worker_spawn.py +8 -1
- package/scripts/test_claude_chain_runner.py +412 -6
- package/scripts/test_claude_chain_usage.py +534 -0
- package/scripts/test_invoke_code_review.py +298 -4
- package/scripts/test_invoke_code_review_codec.py +77 -0
- package/scripts/test_resolve_worker_spawn_codec.py +101 -0
- package/skills/autoconverge/SKILL.md +9 -3
- package/skills/autoconverge/reference/convergence.md +2 -1
- package/skills/autoconverge/reference/multi-pr.md +6 -1
- package/skills/autoconverge/reference/stop-conditions.md +16 -10
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +1 -1
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +19 -0
- package/skills/autoconverge/workflow/converge.mjs +24 -8
- package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -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/orchestrator/SKILL.md +1 -20
- package/skills/orchestrator-refresh/SKILL.md +1 -1
- package/skills/pr-converge/SKILL.md +3 -3
- 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 +3 -3
- package/skills/pr-converge/reference/per-tick.md +5 -5
- package/skills/pr-converge/reference/progress-checklist.md +1 -1
- package/skills/pr-converge/test_step5_host_branch.py +1 -1
- 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/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/team-advisor/SKILL.md +5 -4
|
@@ -41,7 +41,7 @@ def test_scan_detects_job_scratch_path_forward_slash() -> None:
|
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def test_scan_detects_worktree_path() -> None:
|
|
44
|
-
text = r"edited
|
|
44
|
+
text = r"edited C:\Users\me\.claude\worktrees\feature\file.py"
|
|
45
45
|
assert scan_text_for_volatile_marker(text) == ".claude/worktrees/"
|
|
46
46
|
|
|
47
47
|
|
|
@@ -71,6 +71,103 @@ def test_scan_clean_body_returns_none() -> None:
|
|
|
71
71
|
assert scan_text_for_volatile_marker(text) is None
|
|
72
72
|
|
|
73
73
|
|
|
74
|
+
def test_scan_bare_backticked_worktree_mention_is_allowed() -> None:
|
|
75
|
+
text = "the `.claude/worktrees/` prefix sits in ALL_TOOLING_STATE_PREFIXES"
|
|
76
|
+
assert scan_text_for_volatile_marker(text) is None
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_scan_bare_start_of_text_worktree_mention_is_allowed() -> None:
|
|
80
|
+
text = ".claude/worktrees/ entries are skipped by the manifest"
|
|
81
|
+
assert scan_text_for_volatile_marker(text) is None
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def test_scan_windows_absolute_worktree_path_is_detected() -> None:
|
|
85
|
+
text = r"C:\Users\me\.claude\worktrees\wt-1\notes.md"
|
|
86
|
+
assert scan_text_for_volatile_marker(text) == ".claude/worktrees/"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_scan_home_anchored_worktree_path_is_detected() -> None:
|
|
90
|
+
text = "~/.claude/worktrees/wt-2/file.py"
|
|
91
|
+
assert scan_text_for_volatile_marker(text) == ".claude/worktrees/"
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_scan_posix_absolute_job_scratch_path_is_detected() -> None:
|
|
95
|
+
text = "/home/me/.claude-editor/jobs/j1/log.txt"
|
|
96
|
+
assert scan_text_for_volatile_marker(text) == ".claude-editor/jobs/"
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_scan_bare_parenthesized_job_scratch_mention_is_allowed() -> None:
|
|
100
|
+
text = "(.claude-editor/jobs/) as a directory name in prose"
|
|
101
|
+
assert scan_text_for_volatile_marker(text) is None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_scan_bare_mention_before_anchored_path_is_detected() -> None:
|
|
105
|
+
text = (
|
|
106
|
+
".claude/worktrees/ is the manifest key; the live path is "
|
|
107
|
+
r"C:\Users\me\.claude\worktrees\wt-1\notes.md"
|
|
108
|
+
)
|
|
109
|
+
assert scan_text_for_volatile_marker(text) == ".claude/worktrees/"
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def test_scan_unchanged_bare_markers_still_detected() -> None:
|
|
113
|
+
assert scan_text_for_volatile_marker(r"saved to %TEMP%\x") == "%temp%"
|
|
114
|
+
assert scan_text_for_volatile_marker("log at /tmp/x") == "/tmp/"
|
|
115
|
+
assert (
|
|
116
|
+
scan_text_for_volatile_marker(r"C:\Users\x\AppData\Local\Temp\y")
|
|
117
|
+
== "appdata/local/temp"
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def test_scan_space_prefixed_relative_worktree_path_is_detected() -> None:
|
|
122
|
+
text = "see .claude/worktrees/wt-1/notes.md"
|
|
123
|
+
assert scan_text_for_volatile_marker(text) == ".claude/worktrees/"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_scan_cd_relative_worktree_path_is_detected() -> None:
|
|
127
|
+
text = "cd .claude/worktrees/wt-199 && pytest"
|
|
128
|
+
assert scan_text_for_volatile_marker(text) == ".claude/worktrees/"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def test_scan_start_of_text_relative_worktree_path_is_detected() -> None:
|
|
132
|
+
text = ".claude/worktrees/wt-1/notes.md"
|
|
133
|
+
assert scan_text_for_volatile_marker(text) == ".claude/worktrees/"
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_scan_markdown_link_relative_worktree_target_is_detected() -> None:
|
|
137
|
+
text = "[notes](.claude/worktrees/wt-1/notes.md)"
|
|
138
|
+
assert scan_text_for_volatile_marker(text) == ".claude/worktrees/"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_scan_space_prefixed_relative_job_scratch_path_is_detected() -> None:
|
|
142
|
+
text = "see .claude-editor/jobs/j1/log.txt"
|
|
143
|
+
assert scan_text_for_volatile_marker(text) == ".claude-editor/jobs/"
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_scan_backslash_relative_worktree_path_is_detected() -> None:
|
|
147
|
+
text = r"see .claude\worktrees\wt-1\notes.md"
|
|
148
|
+
assert scan_text_for_volatile_marker(text) == ".claude/worktrees/"
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_scan_prose_child_token_after_marker_is_detected() -> None:
|
|
152
|
+
text = "the .claude/worktrees/wt-name pattern"
|
|
153
|
+
assert scan_text_for_volatile_marker(text) == ".claude/worktrees/"
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_scan_relative_worktree_path_with_hyphen_child_is_detected() -> None:
|
|
157
|
+
text = "see .claude/worktrees/-scratch/notes.md"
|
|
158
|
+
assert scan_text_for_volatile_marker(text) == ".claude/worktrees/"
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_scan_placeholder_child_segment_is_allowed() -> None:
|
|
162
|
+
text = "worktrees live under `.claude/worktrees/<name>`"
|
|
163
|
+
assert scan_text_for_volatile_marker(text) is None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_scan_sentence_final_marker_period_is_allowed() -> None:
|
|
167
|
+
text = "the constant is `.claude/worktrees/`. Next."
|
|
168
|
+
assert scan_text_for_volatile_marker(text) is None
|
|
169
|
+
|
|
170
|
+
|
|
74
171
|
def test_gh_comment_with_job_scratch_path_is_blocked() -> None:
|
|
75
172
|
command = (
|
|
76
173
|
'gh pr comment 669 --body "Contact sheet at '
|
|
@@ -147,7 +244,7 @@ def test_mcp_issue_comment_body_blocked() -> None:
|
|
|
147
244
|
|
|
148
245
|
|
|
149
246
|
def test_mcp_review_comment_param_blocked() -> None:
|
|
150
|
-
tool_input: dict[str, object] = {"comment": r"see
|
|
247
|
+
tool_input: dict[str, object] = {"comment": r"see C:\Users\me\.claude\worktrees\x\file"}
|
|
151
248
|
assert _body_names_volatile_path(
|
|
152
249
|
"mcp__plugin_github_github__add_reply_to_pull_request_comment", tool_input
|
|
153
250
|
)
|
|
@@ -177,6 +274,21 @@ def test_extract_gh_post_body_texts_returns_inline_and_file(tmp_path: pathlib.Pa
|
|
|
177
274
|
assert "file body text" in all_texts
|
|
178
275
|
|
|
179
276
|
|
|
277
|
+
def test_gh_issue_edit_quoting_bare_constant_is_allowed(tmp_path: pathlib.Path) -> None:
|
|
278
|
+
body_file = tmp_path / "issue_body.md"
|
|
279
|
+
body_file.write_text(
|
|
280
|
+
"The manifest skips these tooling-state prefixes:\n"
|
|
281
|
+
' ".claude/worktrees/",\n'
|
|
282
|
+
' ".claude-editor/jobs/",\n'
|
|
283
|
+
"Neither is a machine-local path.",
|
|
284
|
+
encoding="utf-8",
|
|
285
|
+
)
|
|
286
|
+
command = f"gh issue edit 134 --body-file {body_file}"
|
|
287
|
+
all_body_texts = extract_gh_post_body_texts(command)
|
|
288
|
+
assert all_body_texts
|
|
289
|
+
assert hook_module._first_volatile_marker(all_body_texts) is None
|
|
290
|
+
|
|
291
|
+
|
|
180
292
|
def test_hook_subprocess_denies_volatile_gh_comment() -> None:
|
|
181
293
|
payload = {
|
|
182
294
|
"tool_name": "Bash",
|
|
@@ -13,6 +13,7 @@ and reading/writing verdict files.
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
import ast
|
|
16
|
+
import datetime
|
|
16
17
|
import hashlib
|
|
17
18
|
import json
|
|
18
19
|
import re
|
|
@@ -20,6 +21,7 @@ import subprocess
|
|
|
20
21
|
import sys
|
|
21
22
|
import time
|
|
22
23
|
from pathlib import Path
|
|
24
|
+
from collections.abc import Mapping
|
|
23
25
|
|
|
24
26
|
blocking_directory = str(Path(__file__).resolve().parent)
|
|
25
27
|
if blocking_directory not in sys.path:
|
|
@@ -45,6 +47,15 @@ from config.verified_commit_constants import (
|
|
|
45
47
|
ALL_FALLBACK_BASE_REFERENCES,
|
|
46
48
|
EMPTY_SURFACE_GUARD_MESSAGE,
|
|
47
49
|
GIT_TIMEOUT_SECONDS,
|
|
50
|
+
LEDGER_KEY_ALL_PASS,
|
|
51
|
+
LEDGER_KEY_BRANCH,
|
|
52
|
+
LEDGER_KEY_FINDING_CHECK_NAMES,
|
|
53
|
+
LEDGER_KEY_FINDING_COUNT,
|
|
54
|
+
LEDGER_KEY_MANIFEST_HASH,
|
|
55
|
+
LEDGER_KEY_MINTED_FROM_AGENT_ID,
|
|
56
|
+
LEDGER_KEY_REPO_ROOT,
|
|
57
|
+
LEDGER_KEY_TIMESTAMP,
|
|
58
|
+
LOGS_DIRECTORY_NAME,
|
|
48
59
|
MANIFEST_HASH_CLI_FLAG,
|
|
49
60
|
MANIFEST_HASH_FOR_BRANCH_CLI_FLAG,
|
|
50
61
|
MINIMUM_STATUS_FIELD_COUNT,
|
|
@@ -65,10 +76,12 @@ from config.verified_commit_constants import (
|
|
|
65
76
|
VERDICT_DIRECTORY_NAME,
|
|
66
77
|
VERDICT_FENCE_PATTERN,
|
|
67
78
|
VERDICT_FILE_GLOB,
|
|
79
|
+
VERDICT_FINDING_CHECK_KEY,
|
|
68
80
|
VERDICT_JSON_INDENT,
|
|
69
81
|
VERDICT_KEY_ALL_PASS,
|
|
70
82
|
VERDICT_KEY_FINDINGS,
|
|
71
83
|
VERDICT_KEY_MANIFEST_SHA256,
|
|
84
|
+
VERIFIER_VERDICTS_JSONL_FILENAME,
|
|
72
85
|
WORKTREE_LIST_BRANCH_PREFIX,
|
|
73
86
|
WORKTREE_LIST_PATH_PREFIX,
|
|
74
87
|
)
|
|
@@ -377,12 +390,36 @@ def verdict_directory() -> Path:
|
|
|
377
390
|
return Path.home() / CLAUDE_HOME_DIRECTORY_NAME / VERDICT_DIRECTORY_NAME
|
|
378
391
|
|
|
379
392
|
|
|
393
|
+
def root_key_for_repo(repo_root: str) -> str:
|
|
394
|
+
"""Derive the shared path-key that names a work tree's store file.
|
|
395
|
+
|
|
396
|
+
::
|
|
397
|
+
|
|
398
|
+
root_key_for_repo("/repo") == root_key_for_repo("/repo/sub") (same tree)
|
|
399
|
+
root_key_for_repo("/Repo") == root_key_for_repo("/repo") (case-folded)
|
|
400
|
+
|
|
401
|
+
The path is resolved, forward-slashed, and lowercased before hashing, so a
|
|
402
|
+
subdirectory of a work tree keys to the same file as its root and case
|
|
403
|
+
differences fold together. Both the verdict store and the code-review stamp
|
|
404
|
+
store key their per-work-tree files by this one derivation, so the two never
|
|
405
|
+
drift and an existing verdict file still resolves.
|
|
406
|
+
|
|
407
|
+
Args:
|
|
408
|
+
repo_root: The repository top-level directory.
|
|
409
|
+
|
|
410
|
+
Returns:
|
|
411
|
+
The lowercased hex path-key, truncated to ``ROOT_KEY_HEX_LENGTH`` chars.
|
|
412
|
+
"""
|
|
413
|
+
normalized_root = str(Path(repo_root).resolve()).replace("\\", "/").lower()
|
|
414
|
+
return hashlib.sha256(normalized_root.encode("utf-8")).hexdigest()[:ROOT_KEY_HEX_LENGTH]
|
|
415
|
+
|
|
416
|
+
|
|
380
417
|
def verdict_path_for_repo(repo_root: str) -> Path:
|
|
381
418
|
"""Derive the verdict file path for a repository work tree.
|
|
382
419
|
|
|
383
420
|
Verdicts live outside the repository (under the user's Claude home) so
|
|
384
|
-
no repo accumulates untracked files, keyed by
|
|
385
|
-
|
|
421
|
+
no repo accumulates untracked files, keyed by the shared
|
|
422
|
+
``root_key_for_repo`` derivation so every worktree gets its own verdict.
|
|
386
423
|
|
|
387
424
|
Args:
|
|
388
425
|
repo_root: The repository top-level directory.
|
|
@@ -390,9 +427,7 @@ def verdict_path_for_repo(repo_root: str) -> Path:
|
|
|
390
427
|
Returns:
|
|
391
428
|
The verdict file path for this work tree.
|
|
392
429
|
"""
|
|
393
|
-
|
|
394
|
-
root_key = hashlib.sha256(normalized_root.encode("utf-8")).hexdigest()[:ROOT_KEY_HEX_LENGTH]
|
|
395
|
-
return verdict_directory() / f"{root_key}.json"
|
|
430
|
+
return verdict_directory() / f"{root_key_for_repo(repo_root)}.json"
|
|
396
431
|
|
|
397
432
|
|
|
398
433
|
def load_valid_verdict(repo_root: str, expected_manifest_sha256: str) -> dict | None:
|
|
@@ -654,6 +689,74 @@ def workflow_verdict_covers_surface(
|
|
|
654
689
|
return False
|
|
655
690
|
|
|
656
691
|
|
|
692
|
+
def _finding_check_names(all_findings: list) -> list[str]:
|
|
693
|
+
"""Collect the ``check`` name from each finding mapping that carries one.
|
|
694
|
+
|
|
695
|
+
Args:
|
|
696
|
+
all_findings: The verifier's findings list (empty when clean).
|
|
697
|
+
|
|
698
|
+
Returns:
|
|
699
|
+
Ordered list of non-empty check name strings; entries without a
|
|
700
|
+
string ``check`` field are omitted.
|
|
701
|
+
"""
|
|
702
|
+
all_check_names: list[str] = []
|
|
703
|
+
for each_finding in all_findings:
|
|
704
|
+
if not isinstance(each_finding, Mapping):
|
|
705
|
+
continue
|
|
706
|
+
check_name = each_finding.get(VERDICT_FINDING_CHECK_KEY)
|
|
707
|
+
if isinstance(check_name, str) and check_name:
|
|
708
|
+
all_check_names.append(check_name)
|
|
709
|
+
return all_check_names
|
|
710
|
+
|
|
711
|
+
|
|
712
|
+
def _append_verdict_ledger_row(
|
|
713
|
+
repo_root: str,
|
|
714
|
+
bound_manifest_sha256: str,
|
|
715
|
+
is_all_pass: bool,
|
|
716
|
+
all_findings: list,
|
|
717
|
+
minted_from_agent_id: str,
|
|
718
|
+
) -> None:
|
|
719
|
+
"""Append one fail-safe JSONL row for a minted verdict.
|
|
720
|
+
|
|
721
|
+
Writes to ``~/.claude/logs/verifier-verdicts.jsonl``. Swallows home
|
|
722
|
+
resolution and IO errors so a ledger failure never changes the mint.
|
|
723
|
+
|
|
724
|
+
Args:
|
|
725
|
+
repo_root: The repository top-level directory.
|
|
726
|
+
bound_manifest_sha256: Hash of the surface the verdict covers.
|
|
727
|
+
is_all_pass: Whether the verifier reported a clean verdict.
|
|
728
|
+
all_findings: The verifier's findings list (empty when clean).
|
|
729
|
+
minted_from_agent_id: The subagent invocation id, kept for audit.
|
|
730
|
+
"""
|
|
731
|
+
try:
|
|
732
|
+
home_directory = Path.home()
|
|
733
|
+
except RuntimeError:
|
|
734
|
+
return
|
|
735
|
+
try:
|
|
736
|
+
ledger_path = (
|
|
737
|
+
home_directory
|
|
738
|
+
/ CLAUDE_HOME_DIRECTORY_NAME
|
|
739
|
+
/ LOGS_DIRECTORY_NAME
|
|
740
|
+
/ VERIFIER_VERDICTS_JSONL_FILENAME
|
|
741
|
+
)
|
|
742
|
+
ledger_path.parent.mkdir(parents=True, exist_ok=True)
|
|
743
|
+
branch_name = _head_branch_name(repo_root)
|
|
744
|
+
ledger_row = {
|
|
745
|
+
LEDGER_KEY_TIMESTAMP: datetime.datetime.now(datetime.timezone.utc).isoformat(),
|
|
746
|
+
LEDGER_KEY_REPO_ROOT: repo_root,
|
|
747
|
+
LEDGER_KEY_BRANCH: branch_name,
|
|
748
|
+
LEDGER_KEY_MANIFEST_HASH: bound_manifest_sha256,
|
|
749
|
+
LEDGER_KEY_ALL_PASS: is_all_pass,
|
|
750
|
+
LEDGER_KEY_FINDING_COUNT: len(all_findings),
|
|
751
|
+
LEDGER_KEY_FINDING_CHECK_NAMES: _finding_check_names(all_findings),
|
|
752
|
+
LEDGER_KEY_MINTED_FROM_AGENT_ID: minted_from_agent_id,
|
|
753
|
+
}
|
|
754
|
+
with ledger_path.open("a", encoding="utf-8") as ledger_file:
|
|
755
|
+
ledger_file.write(json.dumps(ledger_row) + "\n")
|
|
756
|
+
except OSError:
|
|
757
|
+
return
|
|
758
|
+
|
|
759
|
+
|
|
657
760
|
def write_verdict(
|
|
658
761
|
repo_root: str,
|
|
659
762
|
bound_manifest_sha256: str,
|
|
@@ -663,6 +766,9 @@ def write_verdict(
|
|
|
663
766
|
) -> Path:
|
|
664
767
|
"""Write a verdict file binding a verification outcome to a surface hash.
|
|
665
768
|
|
|
769
|
+
Also appends one durable JSONL ledger row under the Claude logs directory.
|
|
770
|
+
A ledger write failure never changes the mint outcome.
|
|
771
|
+
|
|
666
772
|
Args:
|
|
667
773
|
repo_root: The repository top-level directory.
|
|
668
774
|
bound_manifest_sha256: Hash of the surface manifest the verdict covers.
|
|
@@ -686,6 +792,13 @@ def write_verdict(
|
|
|
686
792
|
verdict_file.write_text(
|
|
687
793
|
json.dumps(verdict_record, indent=VERDICT_JSON_INDENT), encoding="utf-8"
|
|
688
794
|
)
|
|
795
|
+
_append_verdict_ledger_row(
|
|
796
|
+
repo_root,
|
|
797
|
+
bound_manifest_sha256,
|
|
798
|
+
is_all_pass,
|
|
799
|
+
all_findings,
|
|
800
|
+
minted_from_agent_id,
|
|
801
|
+
)
|
|
689
802
|
return verdict_file
|
|
690
803
|
|
|
691
804
|
|
|
@@ -81,14 +81,15 @@ def _resolved_option_value(
|
|
|
81
81
|
return value_after_option(all_following_tokens, option_index)
|
|
82
82
|
|
|
83
83
|
|
|
84
|
-
def gated_invocation_directory(
|
|
84
|
+
def gated_invocation_directory(
|
|
85
|
+
all_following_tokens: list[str],
|
|
86
|
+
all_gated_subcommands: frozenset[str] = GATED_GIT_SUBCOMMANDS,
|
|
87
|
+
) -> tuple[bool, str | None]:
|
|
85
88
|
"""Walk the quote-stripped tokens after a ``git`` word to its subcommand.
|
|
86
89
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
Returns:
|
|
91
|
-
Whether the subcommand is gated, and its ``-C``/``--work-tree`` directory.
|
|
90
|
+
Args:
|
|
91
|
+
all_following_tokens: Quote-stripped tokens after the ``git`` word.
|
|
92
|
+
all_gated_subcommands: Subcommand names that gate; defaults to commit+push.
|
|
92
93
|
"""
|
|
93
94
|
repo_directory: str | None = None
|
|
94
95
|
work_tree_directory: str | None = None
|
|
@@ -108,7 +109,7 @@ def gated_invocation_directory(all_following_tokens: list[str]) -> tuple[bool, s
|
|
|
108
109
|
if each_token.startswith("-"):
|
|
109
110
|
token_index += 1
|
|
110
111
|
continue
|
|
111
|
-
return each_token.lower() in
|
|
112
|
+
return each_token.lower() in all_gated_subcommands, repo_directory or work_tree_directory
|
|
112
113
|
return False, repo_directory or work_tree_directory
|
|
113
114
|
|
|
114
115
|
|
|
@@ -167,11 +168,16 @@ def _apply_directory_change(
|
|
|
167
168
|
|
|
168
169
|
|
|
169
170
|
def _target_directory_for_match(
|
|
170
|
-
command_text: str,
|
|
171
|
+
command_text: str,
|
|
172
|
+
git_word_match: re.Match[str],
|
|
173
|
+
active_directory: str,
|
|
174
|
+
all_gated_subcommands: frozenset[str],
|
|
171
175
|
) -> str | None:
|
|
172
176
|
"""Resolve the gated directory for one ``git`` word match, or None."""
|
|
173
177
|
all_following_tokens = _following_tokens(command_text, git_word_match.end())
|
|
174
|
-
is_gated, flagged_directory = gated_invocation_directory(
|
|
178
|
+
is_gated, flagged_directory = gated_invocation_directory(
|
|
179
|
+
all_following_tokens, all_gated_subcommands
|
|
180
|
+
)
|
|
175
181
|
if not is_gated:
|
|
176
182
|
return None
|
|
177
183
|
if flagged_directory is None:
|
|
@@ -179,18 +185,22 @@ def _target_directory_for_match(
|
|
|
179
185
|
return resolve_against(active_directory, flagged_directory)
|
|
180
186
|
|
|
181
187
|
|
|
182
|
-
def gated_repo_directories(
|
|
183
|
-
|
|
188
|
+
def gated_repo_directories(
|
|
189
|
+
command_text: str,
|
|
190
|
+
fallback_directory: str,
|
|
191
|
+
all_gated_subcommands: frozenset[str] = GATED_GIT_SUBCOMMANDS,
|
|
192
|
+
) -> list[str]:
|
|
193
|
+
"""Collect the directories of every gated git call found in a command.
|
|
184
194
|
|
|
185
195
|
Args:
|
|
186
196
|
command_text: The raw command string from the tool payload.
|
|
187
|
-
fallback_directory: The session working directory,
|
|
188
|
-
|
|
189
|
-
|
|
197
|
+
fallback_directory: The session working directory, the active directory
|
|
198
|
+
until a directory-change verb or a ``-C`` flag overrides it.
|
|
199
|
+
all_gated_subcommands: Subcommand names that gate; defaults to commit+push.
|
|
190
200
|
|
|
191
201
|
Returns:
|
|
192
|
-
One directory per detected
|
|
193
|
-
|
|
202
|
+
One directory per detected gated invocation, in order; empty when the
|
|
203
|
+
command carries no gated git verb.
|
|
194
204
|
"""
|
|
195
205
|
command_text = collapse_line_continuations(command_text)
|
|
196
206
|
active_directory = fallback_directory
|
|
@@ -199,7 +209,9 @@ def gated_repo_directories(command_text: str, fallback_directory: str) -> list[s
|
|
|
199
209
|
if each_match.group().lower().strip("\"'") in DIRECTORY_CHANGE_VERBS:
|
|
200
210
|
active_directory = _apply_directory_change(command_text, each_match, active_directory)
|
|
201
211
|
continue
|
|
202
|
-
target_directory = _target_directory_for_match(
|
|
212
|
+
target_directory = _target_directory_for_match(
|
|
213
|
+
command_text, each_match, active_directory, all_gated_subcommands
|
|
214
|
+
)
|
|
203
215
|
if target_directory is not None:
|
|
204
216
|
target_directories.append(target_directory)
|
|
205
217
|
return target_directories
|
|
@@ -59,3 +59,38 @@ def test_gated_repo_directories_gates_an_empty_work_tree_against_the_session() -
|
|
|
59
59
|
assert gated_repo_directories("git --work-tree= commit -m x", "/session") == [
|
|
60
60
|
os.path.join("/session", "")
|
|
61
61
|
]
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_default_gated_subcommands_gates_both_commit_and_push() -> None:
|
|
65
|
+
assert gated_repo_directories("git commit -m x", "/session") == ["/session"]
|
|
66
|
+
assert gated_repo_directories("git push", "/session") == ["/session"]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_push_only_subcommands_gate_push_but_not_commit() -> None:
|
|
70
|
+
push_only = frozenset({"push"})
|
|
71
|
+
assert gated_invocation_directory(["push"], push_only)[0] is True
|
|
72
|
+
assert gated_invocation_directory(["commit", "-m", "x"], push_only)[0] is False
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_commit_only_subcommands_gate_commit_but_not_push() -> None:
|
|
76
|
+
commit_only = frozenset({"commit"})
|
|
77
|
+
assert gated_invocation_directory(["commit", "-m", "x"], commit_only)[0] is True
|
|
78
|
+
assert gated_invocation_directory(["push"], commit_only)[0] is False
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def test_gated_repo_directories_threads_push_only_subcommands() -> None:
|
|
82
|
+
push_only = frozenset({"push"})
|
|
83
|
+
assert gated_repo_directories("git push", "/session", push_only) == ["/session"]
|
|
84
|
+
assert gated_repo_directories("git commit -m x", "/session", push_only) == []
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_gated_repo_directories_accepts_the_all_gated_subcommands_keyword() -> None:
|
|
88
|
+
push_only = frozenset({"push"})
|
|
89
|
+
assert gated_repo_directories(
|
|
90
|
+
"git push", "/session", all_gated_subcommands=push_only
|
|
91
|
+
) == ["/session"]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_gated_invocation_directory_accepts_the_all_gated_subcommands_keyword() -> None:
|
|
95
|
+
push_only = frozenset({"push"})
|
|
96
|
+
assert gated_invocation_directory(["push"], all_gated_subcommands=push_only)[0] is True
|
|
@@ -18,6 +18,15 @@ and scanned, since that content is what gets embedded in the post). The ``gh``
|
|
|
18
18
|
command is tokenized with ``shlex.split`` and the ``gh`` word must be the first
|
|
19
19
|
command token, so a ``gh pr comment`` that only appears as quoted data inside
|
|
20
20
|
another argument never classifies.
|
|
21
|
+
|
|
22
|
+
The marker scan runs in two tiers over the normalized body. Five bare markers
|
|
23
|
+
— the system temp locations and env tokens — match as a plain substring
|
|
24
|
+
anywhere. Two dot-relative directory markers, ``.claude-editor/jobs/`` and
|
|
25
|
+
``.claude/worktrees/``, match when either a slash sits immediately before the
|
|
26
|
+
marker or a path segment follows it. A machine-local absolute path
|
|
27
|
+
(``~/.claude/worktrees/wt/f.py``) and a relative path that names a child
|
|
28
|
+
(``see .claude/worktrees/wt-1/notes.md``) both count; a bare directory-name
|
|
29
|
+
mention with neither anchor posts cleanly.
|
|
21
30
|
"""
|
|
22
31
|
|
|
23
32
|
import json
|
|
@@ -42,30 +51,79 @@ from blocking._gh_body_arg_utils import ( # noqa: E402
|
|
|
42
51
|
)
|
|
43
52
|
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
44
53
|
from hooks_constants.volatile_path_in_post_blocker_constants import ( # noqa: E402
|
|
54
|
+
ALL_BARE_VOLATILE_PATH_MARKERS,
|
|
45
55
|
ALL_GH_POST_SUBCOMMANDS,
|
|
46
56
|
ALL_MCP_BODY_PARAM_NAMES,
|
|
47
|
-
|
|
57
|
+
ALL_PATH_ANCHORED_VOLATILE_PATH_MARKERS,
|
|
48
58
|
BASH_TOOL_NAME,
|
|
49
59
|
BODY_FILE_ENCODING,
|
|
50
60
|
CORRECTIVE_MESSAGE,
|
|
51
61
|
GH_COMMAND_NAME,
|
|
52
62
|
MCP_GITHUB_TOOL_PREFIX,
|
|
53
63
|
MINIMUM_POST_SUBCOMMAND_TOKEN_COUNT,
|
|
64
|
+
PATH_ANCHOR_CHARACTER,
|
|
65
|
+
PATH_SEGMENT_START_CHARACTERS,
|
|
54
66
|
TOKEN_JOIN_SEPARATOR,
|
|
55
67
|
)
|
|
56
68
|
|
|
57
69
|
|
|
70
|
+
def _character_starts_path_segment(character: str) -> bool:
|
|
71
|
+
"""Return whether a character can open a path segment after a marker."""
|
|
72
|
+
return bool(character) and (
|
|
73
|
+
character.isalnum() or character in PATH_SEGMENT_START_CHARACTERS
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _text_has_anchored_marker(normalized_text: str, marker: str) -> bool:
|
|
78
|
+
"""Return whether the marker appears as part of a path.
|
|
79
|
+
|
|
80
|
+
A match counts when either a slash sits immediately before the marker or a
|
|
81
|
+
path segment follows it. Every occurrence is scanned, so a bare directory
|
|
82
|
+
mention earlier in the text never masks a path occurrence later::
|
|
83
|
+
|
|
84
|
+
ok: ".claude/worktrees/ is the manifest key" -> False
|
|
85
|
+
flag: "path c:/users/me/.claude/worktrees/wt/f.py" -> True
|
|
86
|
+
flag: "see .claude/worktrees/wt-1/notes.md" -> True
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
normalized_text: The body text with backslashes folded to forward
|
|
90
|
+
slashes and lowercased.
|
|
91
|
+
marker: A dot-relative directory marker to search for.
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
True when at least one occurrence is path-anchored.
|
|
95
|
+
"""
|
|
96
|
+
search_start = 0
|
|
97
|
+
while True:
|
|
98
|
+
found_index = normalized_text.find(marker, search_start)
|
|
99
|
+
if found_index < 0:
|
|
100
|
+
return False
|
|
101
|
+
preceding_character = normalized_text[found_index - 1 : found_index]
|
|
102
|
+
following_index = found_index + len(marker)
|
|
103
|
+
following_character = normalized_text[following_index : following_index + 1]
|
|
104
|
+
is_slash_before = preceding_character == PATH_ANCHOR_CHARACTER
|
|
105
|
+
is_segment_after = _character_starts_path_segment(following_character)
|
|
106
|
+
if is_slash_before or is_segment_after:
|
|
107
|
+
return True
|
|
108
|
+
search_start = found_index + 1
|
|
109
|
+
|
|
110
|
+
|
|
58
111
|
def scan_text_for_volatile_marker(text: str) -> str | None:
|
|
59
112
|
"""Return the first volatile-path marker found in text, or None.
|
|
60
113
|
|
|
61
114
|
Backslashes normalize to forward slashes and the text lowercases before the
|
|
62
|
-
scan
|
|
115
|
+
scan. The two dot-relative directory markers count when either a slash sits
|
|
116
|
+
immediately before them or a path segment follows them; the five bare
|
|
117
|
+
markers match anywhere::
|
|
63
118
|
|
|
64
|
-
ok: "
|
|
65
|
-
flag: r"C:\\Users\\me\\.claude-editor\\jobs\\x"
|
|
119
|
+
ok: "the `.claude/worktrees/` manifest key" -> None
|
|
120
|
+
flag: r"C:\\Users\\me\\.claude-editor\\jobs\\x" -> ".claude-editor/jobs/"
|
|
121
|
+
flag: "see .claude/worktrees/wt-1/notes.md" -> ".claude/worktrees/"
|
|
122
|
+
flag: "saved to %TEMP%\\out.txt" -> "%temp%"
|
|
66
123
|
|
|
67
|
-
|
|
68
|
-
|
|
124
|
+
A dot-relative marker with neither a slash before it nor a path segment
|
|
125
|
+
after it — start of text naming the directory, a quoted config constant,
|
|
126
|
+
or a placeholder form such as ``.claude/worktrees/<name>`` — posts cleanly.
|
|
69
127
|
|
|
70
128
|
Args:
|
|
71
129
|
text: The post body text to scan.
|
|
@@ -73,8 +131,11 @@ def scan_text_for_volatile_marker(text: str) -> str | None:
|
|
|
73
131
|
Returns:
|
|
74
132
|
The matched marker string, or None when the text names no volatile path.
|
|
75
133
|
"""
|
|
76
|
-
normalized_text = text.replace("\\",
|
|
77
|
-
for each_marker in
|
|
134
|
+
normalized_text = text.replace("\\", PATH_ANCHOR_CHARACTER).lower()
|
|
135
|
+
for each_marker in ALL_PATH_ANCHORED_VOLATILE_PATH_MARKERS:
|
|
136
|
+
if _text_has_anchored_marker(normalized_text, each_marker):
|
|
137
|
+
return each_marker
|
|
138
|
+
for each_marker in ALL_BARE_VOLATILE_PATH_MARKERS:
|
|
78
139
|
if each_marker in normalized_text:
|
|
79
140
|
return each_marker
|
|
80
141
|
return None
|
|
@@ -49,6 +49,12 @@ NO_PARSEABLE_STDIN_LINES_MESSAGE: str = (
|
|
|
49
49
|
)
|
|
50
50
|
NO_PARSEABLE_STDIN_LINES_SENTINEL: str = "__no_parseable_stdin_lines__"
|
|
51
51
|
LOCAL_BRANCH_REFERENCE_PREFIX: str = "refs/heads/"
|
|
52
|
+
CODE_REVIEW_PUSH_GATE_PATH_OVERRIDE_ENV_VAR: str = "CODE_REVIEW_PUSH_GATE_PATH"
|
|
53
|
+
CODE_REVIEW_PUSH_GATE_MODULE_NAME: str = "code_review_push_gate"
|
|
54
|
+
CODE_REVIEW_PUSH_GATE_SCRIPT_FILENAME: str = "code_review_push_gate.py"
|
|
55
|
+
CODE_REVIEW_DENY_REASON_FUNCTION_NAME: str = "deny_reason_for_directory"
|
|
56
|
+
BLOCKING_DIRECTORY_NAME: str = "blocking"
|
|
57
|
+
CODE_REVIEW_STAMP_BLOCK_EXIT_CODE: int = 1
|
|
52
58
|
ALL_PROTECTED_BRANCH_PUSH_NAMES: tuple[str, ...] = ("main", "master")
|
|
53
59
|
PROTECTED_BRANCH_PUSH_BLOCK_EXIT_CODE: int = 1
|
|
54
60
|
PROTECTED_BRANCH_PUSH_BLOCK_MESSAGE: str = (
|