claude-dev-env 1.89.0 → 1.92.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 +0 -13
- package/_shared/pr-loop/scripts/code_rules_gate.py +106 -14
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +26 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +160 -0
- package/agents/clean-coder.md +2 -2
- package/agents/code-verifier.md +0 -1
- package/agents/test_agent_frontmatter.py +78 -0
- package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
- package/audit-rubrics/prompts/category-j-code-rules-compliance.md +1 -1
- package/bin/install.mjs +1 -0
- package/docs/CODE_RULES.md +2 -2
- package/hooks/blocking/CLAUDE.md +6 -2
- package/hooks/blocking/code_rules_comments.py +2 -2
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +44 -0
- package/hooks/blocking/config/verified_commit_constants.py +2 -0
- package/hooks/blocking/conftest.py +115 -0
- package/hooks/blocking/nas_ssh_binary_enforcer.py +191 -0
- package/hooks/blocking/pr_description_enforcer.py +46 -22
- package/hooks/blocking/pr_description_pr_number.py +5 -3
- package/hooks/blocking/pr_description_proof_of_work.py +367 -0
- package/hooks/blocking/precommit_code_rules_gate.py +5 -1
- package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +8 -2
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +71 -0
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +168 -0
- package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +175 -0
- package/hooks/blocking/test_pr_description_proof_of_work.py +162 -0
- package/hooks/blocking/test_precommit_code_rules_gate.py +89 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +4 -0
- package/hooks/blocking/test_verification_verdict_store.py +11 -0
- package/hooks/blocking/test_verified_commit_config_bootstrap.py +49 -0
- package/hooks/blocking/test_verified_commit_gate.py +11 -0
- package/hooks/blocking/test_verifier_verdict_minter.py +11 -0
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +210 -0
- package/hooks/blocking/verdict_directory_write_blocker.py +6 -0
- package/hooks/blocking/verification_verdict_store.py +73 -5
- package/hooks/blocking/verified_commit_config_bootstrap.py +51 -0
- package/hooks/blocking/verified_commit_gate.py +6 -0
- package/hooks/blocking/verifier_verdict_minter.py +6 -0
- package/hooks/blocking/volatile_path_in_post_blocker.py +351 -0
- package/hooks/hooks.json +32 -2
- package/hooks/hooks_constants/CLAUDE.md +4 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +3 -0
- package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -0
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +59 -0
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +2 -0
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +111 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +48 -0
- package/hooks/lifecycle/CLAUDE.md +3 -1
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +146 -0
- package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +178 -0
- package/hooks/validators/run_all_validators.py +216 -4
- package/hooks/validators/test_run_all_validators_pretooluse.py +102 -0
- package/package.json +1 -1
- package/rules/CLAUDE.md +3 -0
- package/rules/durable-post-artifacts.md +35 -0
- package/rules/nas-ssh-invocation.md +21 -0
- package/rules/proof-of-work-pr-comments.md +26 -0
- package/scripts/CLAUDE.md +2 -0
- package/scripts/claude-chain.example.json +8 -0
- package/scripts/claude_chain_runner.py +400 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -0
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +124 -0
- package/scripts/dev_env_scripts_constants/gh_artifact_upload_constants.py +43 -0
- package/scripts/gh_artifact_upload.py +256 -0
- package/scripts/sync_to_cursor/rules.py +1 -1
- package/scripts/test_claude_chain_runner.py +472 -0
- package/scripts/tests/test_gh_artifact_upload.py +205 -0
- package/skills/CLAUDE.md +3 -0
- package/skills/team-advisor/SKILL.md +188 -0
- package/skills/team-advisor-refresh/SKILL.md +25 -0
- package/skills/usage-pause/SKILL.md +108 -0
- package/skills/usage-pause/scripts/resolve_usage_window.py +462 -0
- package/skills/usage-pause/scripts/test_resolve_usage_window.py +278 -0
- package/skills/usage-pause/scripts/usage_pause_constants/__init__.py +1 -0
- package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +65 -0
- package/system-prompts/software-engineer.xml +3 -2
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Unit and integration tests for the volatile_path_in_post_blocker PreToolUse hook."""
|
|
2
|
+
|
|
3
|
+
import importlib.util
|
|
4
|
+
import json
|
|
5
|
+
import pathlib
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
|
|
9
|
+
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
10
|
+
if str(_HOOK_DIR) not in sys.path:
|
|
11
|
+
sys.path.insert(0, str(_HOOK_DIR))
|
|
12
|
+
|
|
13
|
+
hook_spec = importlib.util.spec_from_file_location(
|
|
14
|
+
"volatile_path_in_post_blocker",
|
|
15
|
+
_HOOK_DIR / "volatile_path_in_post_blocker.py",
|
|
16
|
+
)
|
|
17
|
+
assert hook_spec is not None
|
|
18
|
+
assert hook_spec.loader is not None
|
|
19
|
+
hook_module = importlib.util.module_from_spec(hook_spec)
|
|
20
|
+
hook_spec.loader.exec_module(hook_module)
|
|
21
|
+
|
|
22
|
+
scan_text_for_volatile_marker = hook_module.scan_text_for_volatile_marker
|
|
23
|
+
extract_gh_post_body_texts = hook_module.extract_gh_post_body_texts
|
|
24
|
+
extract_mcp_body_texts = hook_module.extract_mcp_body_texts
|
|
25
|
+
_collect_body_texts = hook_module._collect_body_texts
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _body_names_volatile_path(tool_name: str, tool_input: dict[str, object]) -> bool:
|
|
29
|
+
all_body_texts = _collect_body_texts(tool_name, tool_input)
|
|
30
|
+
return hook_module._first_volatile_marker(all_body_texts) is not None
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_scan_detects_job_scratch_path_backslash() -> None:
|
|
34
|
+
text = r"See C:\Users\jon\.claude-editor\jobs\95762cea\tmp\staging\contact_sheet.png"
|
|
35
|
+
assert scan_text_for_volatile_marker(text) == ".claude-editor/jobs/"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_scan_detects_job_scratch_path_forward_slash() -> None:
|
|
39
|
+
text = "artifact at /home/user/.claude-editor/jobs/abc/tmp/out.png"
|
|
40
|
+
assert scan_text_for_volatile_marker(text) == ".claude-editor/jobs/"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_scan_detects_worktree_path() -> None:
|
|
44
|
+
text = r"edited .claude\worktrees\feature\file.py"
|
|
45
|
+
assert scan_text_for_volatile_marker(text) == ".claude/worktrees/"
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_scan_detects_appdata_temp_case_insensitive() -> None:
|
|
49
|
+
text = r"C:\Users\jon\AppData\Local\Temp\bugteam\worktree"
|
|
50
|
+
assert scan_text_for_volatile_marker(text) == "appdata/local/temp"
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_scan_detects_unix_tmp() -> None:
|
|
54
|
+
assert scan_text_for_volatile_marker("output written to /tmp/run/out.log") == "/tmp/"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_scan_detects_percent_temp_token() -> None:
|
|
58
|
+
assert scan_text_for_volatile_marker("saved to %TEMP%\\out.txt") == "%temp%"
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_scan_detects_env_temp_token() -> None:
|
|
62
|
+
assert scan_text_for_volatile_marker("path is $env:TEMP\\out.txt") == "$env:temp"
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def test_scan_detects_claude_job_dir_token() -> None:
|
|
66
|
+
assert scan_text_for_volatile_marker("see $CLAUDE_JOB_DIR/staging/report.md") == "$claude_job_dir"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_scan_clean_body_returns_none() -> None:
|
|
70
|
+
text = "The failing table is pasted below:\n| case | result |\n| a | pass |"
|
|
71
|
+
assert scan_text_for_volatile_marker(text) is None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_gh_comment_with_job_scratch_path_is_blocked() -> None:
|
|
75
|
+
command = (
|
|
76
|
+
'gh pr comment 669 --body "Contact sheet at '
|
|
77
|
+
r'C:\Users\jon\.claude-editor\jobs\95762cea\tmp\staging\contact_sheet.png"'
|
|
78
|
+
)
|
|
79
|
+
assert _body_names_volatile_path("Bash", {"command": command})
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_gh_comment_with_clean_body_is_allowed() -> None:
|
|
83
|
+
command = 'gh pr comment 669 --body "All checks pass. Table pasted inline above."'
|
|
84
|
+
assert not _body_names_volatile_path("Bash", {"command": command})
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def test_gh_pr_create_short_flag_blocked() -> None:
|
|
88
|
+
command = 'gh pr create --title "T" -b "logs under /tmp/run/out.log"'
|
|
89
|
+
assert _body_names_volatile_path("Bash", {"command": command})
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def test_gh_issue_create_body_equals_form_blocked() -> None:
|
|
93
|
+
command = 'gh issue create --title "T" --body="dump in %TEMP%\\dump.json"'
|
|
94
|
+
assert _body_names_volatile_path("Bash", {"command": command})
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_body_file_content_is_scanned_and_blocked(tmp_path: pathlib.Path) -> None:
|
|
98
|
+
body_file = tmp_path / "body.md"
|
|
99
|
+
body_file.write_text(
|
|
100
|
+
"Artifact: $CLAUDE_JOB_DIR/tmp/contact_sheet.png",
|
|
101
|
+
encoding="utf-8",
|
|
102
|
+
)
|
|
103
|
+
command = f"gh pr comment 669 --body-file {body_file}"
|
|
104
|
+
assert _body_names_volatile_path("Bash", {"command": command})
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_body_file_clean_content_is_allowed(tmp_path: pathlib.Path) -> None:
|
|
108
|
+
body_file = tmp_path / "body.md"
|
|
109
|
+
body_file.write_text("Everything is green. Results inline above.", encoding="utf-8")
|
|
110
|
+
command = f"gh pr comment 669 --body-file {body_file}"
|
|
111
|
+
assert not _body_names_volatile_path("Bash", {"command": command})
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_non_post_gh_command_is_untouched() -> None:
|
|
115
|
+
assert not _body_names_volatile_path("Bash", {"command": "gh pr list --repo owner/repo"})
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_gh_pr_view_with_tmp_in_flag_is_untouched() -> None:
|
|
119
|
+
command = "gh pr view 10 --json body --jq .body > /tmp/out.json"
|
|
120
|
+
assert not _body_names_volatile_path("Bash", {"command": command})
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def test_substring_mention_of_gh_post_does_not_classify() -> None:
|
|
124
|
+
command = 'echo "example: gh pr comment 42 --body \\"see /tmp/x\\"" >> notes.txt'
|
|
125
|
+
assert not _body_names_volatile_path("Bash", {"command": command})
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_gh_word_not_first_token_does_not_classify() -> None:
|
|
129
|
+
command = r'echo gh pr comment 42 --body "see C:\.claude-editor\jobs\x"'
|
|
130
|
+
assert not _body_names_volatile_path("Bash", {"command": command})
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def test_env_assignment_prefix_still_classifies() -> None:
|
|
134
|
+
command = 'GH_TOKEN=abc gh pr comment 669 --body "log at /tmp/run.log"'
|
|
135
|
+
assert _body_names_volatile_path("Bash", {"command": command})
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_unparseable_command_is_allowed() -> None:
|
|
139
|
+
assert not _body_names_volatile_path(
|
|
140
|
+
"Bash", {"command": "gh pr comment 1 --body 'unterminated"}
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def test_mcp_issue_comment_body_blocked() -> None:
|
|
145
|
+
tool_input: dict[str, object] = {"body": "artifact at $CLAUDE_JOB_DIR/tmp/out.png"}
|
|
146
|
+
assert _body_names_volatile_path("mcp__plugin_github_github__add_issue_comment", tool_input)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_mcp_review_comment_param_blocked() -> None:
|
|
150
|
+
tool_input: dict[str, object] = {"comment": r"see .claude\worktrees\x\file"}
|
|
151
|
+
assert _body_names_volatile_path(
|
|
152
|
+
"mcp__plugin_github_github__add_reply_to_pull_request_comment", tool_input
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_mcp_clean_body_allowed() -> None:
|
|
157
|
+
tool_input: dict[str, object] = {"body": "LGTM, results pasted inline."}
|
|
158
|
+
assert not _body_names_volatile_path("mcp__plugin_github_github__add_issue_comment", tool_input)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def test_mcp_read_tool_without_body_allowed() -> None:
|
|
162
|
+
tool_input: dict[str, object] = {"pullNumber": 10}
|
|
163
|
+
assert not _body_names_volatile_path("mcp__plugin_github_github__pull_request_read", tool_input)
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_extract_mcp_body_texts_skips_non_string_values() -> None:
|
|
167
|
+
tool_input: dict[str, object] = {"body": None, "comment": 42}
|
|
168
|
+
assert extract_mcp_body_texts(tool_input) == []
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def test_extract_gh_post_body_texts_returns_inline_and_file(tmp_path: pathlib.Path) -> None:
|
|
172
|
+
body_file = tmp_path / "b.md"
|
|
173
|
+
body_file.write_text("file body text", encoding="utf-8")
|
|
174
|
+
command = f'gh pr create --title "T" --body "inline body" --body-file {body_file}'
|
|
175
|
+
all_texts = extract_gh_post_body_texts(command)
|
|
176
|
+
assert "inline body" in all_texts
|
|
177
|
+
assert "file body text" in all_texts
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_hook_subprocess_denies_volatile_gh_comment() -> None:
|
|
181
|
+
payload = {
|
|
182
|
+
"tool_name": "Bash",
|
|
183
|
+
"tool_input": {
|
|
184
|
+
"command": 'gh pr comment 669 --body "art at $CLAUDE_JOB_DIR/tmp/x.png"',
|
|
185
|
+
},
|
|
186
|
+
}
|
|
187
|
+
completion = subprocess.run(
|
|
188
|
+
[sys.executable, str(_HOOK_DIR / "volatile_path_in_post_blocker.py")],
|
|
189
|
+
input=json.dumps(payload),
|
|
190
|
+
capture_output=True,
|
|
191
|
+
text=True,
|
|
192
|
+
check=False,
|
|
193
|
+
)
|
|
194
|
+
decision = json.loads(completion.stdout)
|
|
195
|
+
assert decision["hookSpecificOutput"]["permissionDecision"] == "deny"
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def test_hook_subprocess_allows_clean_gh_comment() -> None:
|
|
199
|
+
payload = {
|
|
200
|
+
"tool_name": "Bash",
|
|
201
|
+
"tool_input": {"command": 'gh pr comment 669 --body "all green, results inline"'},
|
|
202
|
+
}
|
|
203
|
+
completion = subprocess.run(
|
|
204
|
+
[sys.executable, str(_HOOK_DIR / "volatile_path_in_post_blocker.py")],
|
|
205
|
+
input=json.dumps(payload),
|
|
206
|
+
capture_output=True,
|
|
207
|
+
text=True,
|
|
208
|
+
check=False,
|
|
209
|
+
)
|
|
210
|
+
assert completion.stdout.strip() == ""
|
|
@@ -41,6 +41,12 @@ hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
|
41
41
|
if hooks_directory not in sys.path:
|
|
42
42
|
sys.path.append(hooks_directory)
|
|
43
43
|
|
|
44
|
+
from verified_commit_config_bootstrap import ( # noqa: E402
|
|
45
|
+
register_verified_commit_constants,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
register_verified_commit_constants()
|
|
49
|
+
|
|
44
50
|
from config.verified_commit_constants import ( # noqa: E402
|
|
45
51
|
ALL_GATED_TOOL_NAMES,
|
|
46
52
|
ALL_VERDICT_PATH_SEGMENT_BODIES,
|
|
@@ -25,14 +25,22 @@ blocking_directory = str(Path(__file__).resolve().parent)
|
|
|
25
25
|
if blocking_directory not in sys.path:
|
|
26
26
|
sys.path.insert(0, blocking_directory)
|
|
27
27
|
|
|
28
|
+
from verified_commit_config_bootstrap import ( # noqa: E402
|
|
29
|
+
register_verified_commit_constants,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
register_verified_commit_constants()
|
|
33
|
+
|
|
28
34
|
from config.verified_commit_constants import (
|
|
29
35
|
AGENT_META_SIDECAR_SUFFIX,
|
|
30
36
|
AGENT_META_TYPE_KEY,
|
|
31
37
|
AGENT_TRANSCRIPT_GLOB,
|
|
32
38
|
BRANCH_REFERENCE_PREFIX,
|
|
39
|
+
BRANCH_REMOTE_CONFIG_KEY_TEMPLATE,
|
|
33
40
|
BRANCH_WORKTREE_ABSENT_MESSAGE,
|
|
34
41
|
CLAUDE_HOME_DIRECTORY_NAME,
|
|
35
42
|
CONFTEST_FILE_NAME,
|
|
43
|
+
DETACHED_HEAD_LABEL,
|
|
36
44
|
DOCS_ONLY_EXTENSIONS,
|
|
37
45
|
ALL_FALLBACK_BASE_REFERENCES,
|
|
38
46
|
EMPTY_SURFACE_GUARD_MESSAGE,
|
|
@@ -122,13 +130,65 @@ def _tracked_upstream_reference(repo_root: str) -> str | None:
|
|
|
122
130
|
)
|
|
123
131
|
|
|
124
132
|
|
|
133
|
+
def _head_branch_name(repo_root: str) -> str | None:
|
|
134
|
+
"""Read the short name of the branch HEAD points at.
|
|
135
|
+
|
|
136
|
+
Args:
|
|
137
|
+
repo_root: The repository top-level directory.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
The short branch name, or None when HEAD is detached or git fails.
|
|
141
|
+
"""
|
|
142
|
+
branch_name = run_git(repo_root, "rev-parse", "--abbrev-ref", "HEAD")
|
|
143
|
+
if not branch_name or branch_name == DETACHED_HEAD_LABEL:
|
|
144
|
+
return None
|
|
145
|
+
return branch_name
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _upstream_is_own_remote_counterpart(repo_root: str, tracked_upstream: str) -> bool:
|
|
149
|
+
"""Decide whether HEAD's upstream is the current branch's own remote ref.
|
|
150
|
+
|
|
151
|
+
::
|
|
152
|
+
|
|
153
|
+
branch feature tracks origin/feature -> True (own counterpart)
|
|
154
|
+
branch feature tracks origin/develop -> False (different branch)
|
|
155
|
+
|
|
156
|
+
A branch tracking its own remote counterpart makes the merge base against
|
|
157
|
+
that ref resolve to the branch tip itself, masking the branch's real state
|
|
158
|
+
versus the default branch — so the caller ranks that ref below the
|
|
159
|
+
default-branch fallbacks.
|
|
160
|
+
|
|
161
|
+
Args:
|
|
162
|
+
repo_root: The repository top-level directory.
|
|
163
|
+
tracked_upstream: HEAD's configured upstream tracking reference.
|
|
164
|
+
|
|
165
|
+
Returns:
|
|
166
|
+
True when the upstream names ``<remote>/<current-branch>``; False when
|
|
167
|
+
it names a different branch, HEAD is detached, or git fails.
|
|
168
|
+
"""
|
|
169
|
+
branch_name = _head_branch_name(repo_root)
|
|
170
|
+
if branch_name is None:
|
|
171
|
+
return False
|
|
172
|
+
configured_remote = run_git(
|
|
173
|
+
repo_root, "config", BRANCH_REMOTE_CONFIG_KEY_TEMPLATE.format(branch_name=branch_name)
|
|
174
|
+
)
|
|
175
|
+
if not configured_remote:
|
|
176
|
+
return False
|
|
177
|
+
return tracked_upstream == f"{configured_remote}/{branch_name}"
|
|
178
|
+
|
|
179
|
+
|
|
125
180
|
def candidate_base_references(repo_root: str) -> tuple[str, ...]:
|
|
126
181
|
"""Collect the upstream references to probe for the merge base, in order.
|
|
127
182
|
|
|
128
|
-
Probes ``origin/HEAD`` first, then
|
|
129
|
-
|
|
130
|
-
found
|
|
131
|
-
|
|
183
|
+
Probes ``origin/HEAD`` first, then a tracked upstream that names a
|
|
184
|
+
different branch (so a differently-named default like ``origin/develop`` is
|
|
185
|
+
found), then the fixed ``origin/main`` / ``origin/master`` fallbacks. A
|
|
186
|
+
tracked upstream that is the current branch's own remote counterpart
|
|
187
|
+
(``origin/<current-branch>``) ranks last, below the fallbacks: on a PR
|
|
188
|
+
branch whose fork carries ``origin/main`` the merge base comes from
|
|
189
|
+
``origin/main`` rather than the branch's stale pushed ref, while a repo
|
|
190
|
+
whose default branch is the tracked branch itself (no ``origin/main`` or
|
|
191
|
+
``origin/master`` present) still falls through to that own-counterpart ref.
|
|
132
192
|
|
|
133
193
|
Args:
|
|
134
194
|
repo_root: The repository top-level directory.
|
|
@@ -138,10 +198,18 @@ def candidate_base_references(repo_root: str) -> tuple[str, ...]:
|
|
|
138
198
|
"""
|
|
139
199
|
upstream_head = run_git(repo_root, "symbolic-ref", "--quiet", "refs/remotes/origin/HEAD")
|
|
140
200
|
tracked_upstream = _tracked_upstream_reference(repo_root)
|
|
201
|
+
high_priority_upstream: tuple[str, ...] = ()
|
|
202
|
+
demoted_upstream: tuple[str, ...] = ()
|
|
203
|
+
if tracked_upstream:
|
|
204
|
+
if _upstream_is_own_remote_counterpart(repo_root, tracked_upstream):
|
|
205
|
+
demoted_upstream = (tracked_upstream,)
|
|
206
|
+
else:
|
|
207
|
+
high_priority_upstream = (tracked_upstream,)
|
|
141
208
|
ordered_references = (
|
|
142
209
|
((upstream_head,) if upstream_head else ())
|
|
143
|
-
+
|
|
210
|
+
+ high_priority_upstream
|
|
144
211
|
+ ALL_FALLBACK_BASE_REFERENCES
|
|
212
|
+
+ demoted_upstream
|
|
145
213
|
)
|
|
146
214
|
return tuple(dict.fromkeys(ordered_references))
|
|
147
215
|
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""Deterministic loader for the verified-commit constants module.
|
|
2
|
+
|
|
3
|
+
The blocking hooks import their shared constants as
|
|
4
|
+
``from config.verified_commit_constants import ...``. In the installed hook
|
|
5
|
+
tree a second, unrelated ``config`` package can sit ahead of this package on
|
|
6
|
+
``sys.path`` and win that dotted name by path order, so the import binds to the
|
|
7
|
+
wrong file and raises ImportError on any constant the stale copy lacks. This
|
|
8
|
+
module binds the dotted name to the sibling ``config/verified_commit_constants``
|
|
9
|
+
file by explicit 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_verified_commit_constants() -> None:
|
|
20
|
+
"""Bind ``config.verified_commit_constants`` to the sibling config file.
|
|
21
|
+
|
|
22
|
+
::
|
|
23
|
+
|
|
24
|
+
sys.path = ["<installed hooks>", "<blocking>"] # foreign config first
|
|
25
|
+
register_verified_commit_constants()
|
|
26
|
+
from config.verified_commit_constants import DETACHED_HEAD_LABEL
|
|
27
|
+
ok: bound to <blocking>/config/verified_commit_constants.py
|
|
28
|
+
|
|
29
|
+
A ``from config.verified_commit_constants import`` that follows this call
|
|
30
|
+
reads the entry straight from ``sys.modules``, so it resolves to this file
|
|
31
|
+
whatever else owns the ``config`` name. An entry already cached is left in
|
|
32
|
+
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
|
+
config_module_dotted_name = "config.verified_commit_constants"
|
|
39
|
+
if config_module_dotted_name in sys.modules:
|
|
40
|
+
return
|
|
41
|
+
constants_file_path = (
|
|
42
|
+
Path(__file__).resolve().parent / "config" / "verified_commit_constants.py"
|
|
43
|
+
)
|
|
44
|
+
module_spec = importlib.util.spec_from_file_location(
|
|
45
|
+
config_module_dotted_name, constants_file_path
|
|
46
|
+
)
|
|
47
|
+
if module_spec is None or module_spec.loader is None:
|
|
48
|
+
return
|
|
49
|
+
constants_module = importlib.util.module_from_spec(module_spec)
|
|
50
|
+
sys.modules[config_module_dotted_name] = constants_module
|
|
51
|
+
module_spec.loader.exec_module(constants_module)
|
|
@@ -42,6 +42,12 @@ _hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
|
42
42
|
if _hooks_dir not in sys.path:
|
|
43
43
|
sys.path.insert(0, _hooks_dir)
|
|
44
44
|
|
|
45
|
+
from verified_commit_config_bootstrap import ( # noqa: E402
|
|
46
|
+
register_verified_commit_constants,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
register_verified_commit_constants()
|
|
50
|
+
|
|
45
51
|
from config.verified_commit_constants import (
|
|
46
52
|
ALL_GIT_BINARY_NAMES,
|
|
47
53
|
CORRECTIVE_MESSAGE,
|
|
@@ -35,6 +35,12 @@ blocking_directory = str(Path(__file__).resolve().parent)
|
|
|
35
35
|
if blocking_directory not in sys.path:
|
|
36
36
|
sys.path.insert(0, blocking_directory)
|
|
37
37
|
|
|
38
|
+
from verified_commit_config_bootstrap import ( # noqa: E402
|
|
39
|
+
register_verified_commit_constants,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
register_verified_commit_constants()
|
|
43
|
+
|
|
38
44
|
from config.verified_commit_constants import (
|
|
39
45
|
MINTING_AGENT_TYPE,
|
|
40
46
|
VERDICT_KEY_MANIFEST_SHA256,
|