claude-dev-env 1.90.0 → 1.92.1
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 +1 -52
- 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 +5 -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/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/hooks.json +7 -2
- package/hooks/hooks_constants/CLAUDE.md +2 -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/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/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 +7 -0
- package/rules/nas-ssh-invocation.md +21 -0
- package/rules/proof-of-work-pr-comments.md +26 -0
- package/rules/testing.md +0 -2
- package/scripts/CLAUDE.md +1 -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 +1 -0
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +124 -0
- package/scripts/sync_to_cursor/rules.py +1 -1
- package/scripts/test_claude_chain_runner.py +472 -0
- package/skills/CLAUDE.md +3 -0
- package/skills/orchestrator/SKILL.md +188 -0
- package/skills/orchestrator-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
|
@@ -40,6 +40,73 @@ def compute_total() -> int:
|
|
|
40
40
|
return result
|
|
41
41
|
'''
|
|
42
42
|
|
|
43
|
+
WIDGET_MODULE_BASE_SOURCE = '''"""Arithmetic helpers used by the precommit gate worktree tests."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def add_one(number: int) -> int:
|
|
47
|
+
"""Return *number* plus one.
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
number: The integer to increment.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
The incremented integer.
|
|
54
|
+
"""
|
|
55
|
+
return number + 1
|
|
56
|
+
'''
|
|
57
|
+
|
|
58
|
+
WIDGET_MODULE_EXPANDED_SOURCE = '''"""Arithmetic helpers used by the precommit gate worktree tests."""
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def add_one(number: int) -> int:
|
|
62
|
+
"""Return *number* plus one.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
number: The integer to increment.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
The incremented integer.
|
|
69
|
+
"""
|
|
70
|
+
return number + 1
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def subtract_one(number: int) -> int:
|
|
74
|
+
"""Return *number* minus one.
|
|
75
|
+
|
|
76
|
+
Args:
|
|
77
|
+
number: The integer to decrement.
|
|
78
|
+
|
|
79
|
+
Returns:
|
|
80
|
+
The decremented integer.
|
|
81
|
+
"""
|
|
82
|
+
return number - 1
|
|
83
|
+
'''
|
|
84
|
+
|
|
85
|
+
WIDGET_TEST_BASE_SOURCE = '''"""Behavior tests for the widget arithmetic helpers."""
|
|
86
|
+
|
|
87
|
+
from widget import add_one
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_add_one_increments() -> None:
|
|
91
|
+
"""add_one returns its argument plus one."""
|
|
92
|
+
assert add_one(1) == 2
|
|
93
|
+
'''
|
|
94
|
+
|
|
95
|
+
WIDGET_TEST_EXPANDED_SOURCE = '''"""Behavior tests for the widget arithmetic helpers."""
|
|
96
|
+
|
|
97
|
+
from widget import add_one, subtract_one
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_add_one_increments() -> None:
|
|
101
|
+
"""add_one returns its argument plus one."""
|
|
102
|
+
assert add_one(1) == 2
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_subtract_one_decrements() -> None:
|
|
106
|
+
"""subtract_one returns its argument minus one."""
|
|
107
|
+
assert subtract_one(2) == 1
|
|
108
|
+
'''
|
|
109
|
+
|
|
43
110
|
|
|
44
111
|
def run_git(repository_root: Path, *git_arguments: str) -> None:
|
|
45
112
|
subprocess.run(
|
|
@@ -124,3 +191,25 @@ def test_commit_with_no_staged_python_files_is_allowed(tmp_path: Path) -> None:
|
|
|
124
191
|
completed_hook = run_hook("git commit -m docs", tmp_path)
|
|
125
192
|
assert completed_hook.returncode == 0
|
|
126
193
|
assert completed_hook.stdout.strip() == ""
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_worktree_commit_reads_worktree_paired_tests_not_sibling_checkout(
|
|
197
|
+
tmp_path: Path,
|
|
198
|
+
) -> None:
|
|
199
|
+
main_checkout = tmp_path / "main_checkout"
|
|
200
|
+
main_checkout.mkdir()
|
|
201
|
+
initialize_repository(main_checkout)
|
|
202
|
+
(main_checkout / "widget.py").write_text(WIDGET_MODULE_BASE_SOURCE, encoding="utf-8")
|
|
203
|
+
(main_checkout / "test_widget.py").write_text(WIDGET_TEST_BASE_SOURCE, encoding="utf-8")
|
|
204
|
+
run_git(main_checkout, "add", "widget.py", "test_widget.py")
|
|
205
|
+
run_git(main_checkout, "commit", "-m", "base")
|
|
206
|
+
worktree_root = tmp_path / "sibling_worktree"
|
|
207
|
+
run_git(main_checkout, "worktree", "add", str(worktree_root), "-b", "feature")
|
|
208
|
+
stage_file(worktree_root, "widget.py", WIDGET_MODULE_EXPANDED_SOURCE)
|
|
209
|
+
stage_file(worktree_root, "test_widget.py", WIDGET_TEST_EXPANDED_SOURCE)
|
|
210
|
+
quoted_worktree = str(worktree_root)
|
|
211
|
+
completed_hook = run_hook(
|
|
212
|
+
f'git -C "{quoted_worktree}" commit -m expand', main_checkout
|
|
213
|
+
)
|
|
214
|
+
assert completed_hook.returncode == 0
|
|
215
|
+
assert completed_hook.stdout.strip() == ""
|
|
@@ -736,6 +736,10 @@ def test_hook_subprocess_imports_real_config_when_parent_holds_shadowing_config(
|
|
|
736
736
|
real_blocking_directory / "verdict_directory_write_blocker.py",
|
|
737
737
|
staged_blocking_directory / "verdict_directory_write_blocker.py",
|
|
738
738
|
)
|
|
739
|
+
shutil.copy(
|
|
740
|
+
real_blocking_directory / "verified_commit_config_bootstrap.py",
|
|
741
|
+
staged_blocking_directory / "verified_commit_config_bootstrap.py",
|
|
742
|
+
)
|
|
739
743
|
shutil.copytree(
|
|
740
744
|
real_blocking_directory / "config",
|
|
741
745
|
staged_blocking_directory / "config",
|
|
@@ -10,6 +10,7 @@ import json
|
|
|
10
10
|
import pathlib
|
|
11
11
|
import subprocess
|
|
12
12
|
import sys
|
|
13
|
+
from collections.abc import Callable
|
|
13
14
|
|
|
14
15
|
import pytest
|
|
15
16
|
|
|
@@ -785,3 +786,13 @@ def test_manifest_hash_for_branch_cli_returns_nonzero_when_branch_absent(
|
|
|
785
786
|
)
|
|
786
787
|
assert completed_process.returncode != 0
|
|
787
788
|
assert completed_process.stdout.strip() == ""
|
|
789
|
+
|
|
790
|
+
|
|
791
|
+
def test_store_imports_under_foreign_config_shadow(
|
|
792
|
+
run_under_config_shadow: Callable[[str], subprocess.CompletedProcess[str]],
|
|
793
|
+
) -> None:
|
|
794
|
+
completed_probe = run_under_config_shadow(
|
|
795
|
+
"import verification_verdict_store\nprint('IMPORT_OK')\n"
|
|
796
|
+
)
|
|
797
|
+
assert completed_probe.returncode == 0, completed_probe.stderr
|
|
798
|
+
assert "IMPORT_OK" in completed_probe.stdout
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"""Regression tests for the verified-commit constants bootstrap.
|
|
2
|
+
|
|
3
|
+
These prove the shared constants module resolves to the sibling
|
|
4
|
+
``config/verified_commit_constants.py`` file even when a foreign ``config``
|
|
5
|
+
package sits ahead of ``blocking`` on ``sys.path``, and that a registration a
|
|
6
|
+
caller placed first is left untouched.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import subprocess
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
|
|
12
|
+
RunUnderConfigShadow = Callable[[str], subprocess.CompletedProcess[str]]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_register_binds_dotted_name_to_sibling_constants_file(
|
|
16
|
+
run_under_config_shadow: RunUnderConfigShadow,
|
|
17
|
+
) -> None:
|
|
18
|
+
completed_probe = run_under_config_shadow(
|
|
19
|
+
"import verified_commit_config_bootstrap as bootstrap\n"
|
|
20
|
+
"bootstrap.register_verified_commit_constants()\n"
|
|
21
|
+
"import config.verified_commit_constants as loaded\n"
|
|
22
|
+
"print(loaded.__file__)\n"
|
|
23
|
+
"print(loaded.DETACHED_HEAD_LABEL)\n"
|
|
24
|
+
)
|
|
25
|
+
assert completed_probe.returncode == 0, completed_probe.stderr
|
|
26
|
+
assert "IS_DECOY_CONFIG" not in completed_probe.stdout
|
|
27
|
+
resolved_file_line, detached_head_line = completed_probe.stdout.splitlines()[:2]
|
|
28
|
+
assert resolved_file_line.replace("\\", "/").endswith(
|
|
29
|
+
"blocking/config/verified_commit_constants.py"
|
|
30
|
+
)
|
|
31
|
+
assert detached_head_line == "HEAD"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_register_leaves_an_already_cached_module_untouched(
|
|
35
|
+
run_under_config_shadow: RunUnderConfigShadow,
|
|
36
|
+
) -> None:
|
|
37
|
+
completed_probe = run_under_config_shadow(
|
|
38
|
+
"import sys\n"
|
|
39
|
+
"import types\n"
|
|
40
|
+
"sentinel = types.ModuleType('config.verified_commit_constants')\n"
|
|
41
|
+
"sentinel.ORIGIN = 'caller-seeded'\n"
|
|
42
|
+
"sys.modules['config.verified_commit_constants'] = sentinel\n"
|
|
43
|
+
"import verified_commit_config_bootstrap as bootstrap\n"
|
|
44
|
+
"bootstrap.register_verified_commit_constants()\n"
|
|
45
|
+
"import config.verified_commit_constants as loaded\n"
|
|
46
|
+
"print(getattr(loaded, 'ORIGIN', 'displaced'))\n"
|
|
47
|
+
)
|
|
48
|
+
assert completed_probe.returncode == 0, completed_probe.stderr
|
|
49
|
+
assert completed_probe.stdout.strip().splitlines()[-1] == "caller-seeded"
|
|
@@ -12,6 +12,7 @@ import os
|
|
|
12
12
|
import pathlib
|
|
13
13
|
import subprocess
|
|
14
14
|
import sys
|
|
15
|
+
from collections.abc import Callable
|
|
15
16
|
|
|
16
17
|
import pytest
|
|
17
18
|
|
|
@@ -568,3 +569,13 @@ def test_minted_verdict_from_other_worktree_with_wrong_hash_denies(
|
|
|
568
569
|
deny_reason = deny_reason_for_directory(str(work_dir), str(transcript_path))
|
|
569
570
|
assert deny_reason is not None
|
|
570
571
|
assert "VERIFIED_COMMIT_GATE" in deny_reason
|
|
572
|
+
|
|
573
|
+
|
|
574
|
+
def test_gate_imports_under_foreign_config_shadow(
|
|
575
|
+
run_under_config_shadow: Callable[[str], subprocess.CompletedProcess[str]],
|
|
576
|
+
) -> None:
|
|
577
|
+
completed_probe = run_under_config_shadow(
|
|
578
|
+
"import verified_commit_gate\nprint('IMPORT_OK')\n"
|
|
579
|
+
)
|
|
580
|
+
assert completed_probe.returncode == 0, completed_probe.stderr
|
|
581
|
+
assert "IMPORT_OK" in completed_probe.stdout
|
|
@@ -21,6 +21,7 @@ import json
|
|
|
21
21
|
import pathlib
|
|
22
22
|
import subprocess
|
|
23
23
|
import sys
|
|
24
|
+
from collections.abc import Callable
|
|
24
25
|
|
|
25
26
|
_HOOK_DIR = pathlib.Path(__file__).parent
|
|
26
27
|
if str(_HOOK_DIR) not in sys.path:
|
|
@@ -389,3 +390,13 @@ def test_attested_manifest_hash_binds_over_cwd_surface(tmp_path: pathlib.Path) -
|
|
|
389
390
|
finally:
|
|
390
391
|
if verdict_path is not None and verdict_path.exists():
|
|
391
392
|
verdict_path.unlink()
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def test_minter_imports_under_foreign_config_shadow(
|
|
396
|
+
run_under_config_shadow: Callable[[str], subprocess.CompletedProcess[str]],
|
|
397
|
+
) -> None:
|
|
398
|
+
completed_probe = run_under_config_shadow(
|
|
399
|
+
"import verifier_verdict_minter\nprint('IMPORT_OK')\n"
|
|
400
|
+
)
|
|
401
|
+
assert completed_probe.returncode == 0, completed_probe.stderr
|
|
402
|
+
assert "IMPORT_OK" in completed_probe.stdout
|
|
@@ -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,
|
package/hooks/hooks.json
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
"hooks": [
|
|
8
8
|
{
|
|
9
9
|
"type": "command",
|
|
10
|
-
"command": "python3 -c \"import sys; sys.path.insert(0, r'${CLAUDE_PLUGIN_ROOT}/hooks'); from validators.run_all_validators import main; sys.exit(main())\"",
|
|
10
|
+
"command": "python3 -c \"import sys; sys.path.insert(0, r'${CLAUDE_PLUGIN_ROOT}/hooks'); from validators.run_all_validators import main; sys.exit(main())\" --pre-tool-use",
|
|
11
11
|
"timeout": 15
|
|
12
12
|
}
|
|
13
13
|
]
|
|
@@ -55,6 +55,11 @@
|
|
|
55
55
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/gh_body_arg_blocker.py",
|
|
56
56
|
"timeout": 10
|
|
57
57
|
},
|
|
58
|
+
{
|
|
59
|
+
"type": "command",
|
|
60
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/nas_ssh_binary_enforcer.py",
|
|
61
|
+
"timeout": 10
|
|
62
|
+
},
|
|
58
63
|
{
|
|
59
64
|
"type": "command",
|
|
60
65
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/volatile_path_in_post_blocker.py",
|
|
@@ -88,7 +93,7 @@
|
|
|
88
93
|
{
|
|
89
94
|
"type": "command",
|
|
90
95
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/blocking/pr_description_enforcer.py",
|
|
91
|
-
"timeout":
|
|
96
|
+
"timeout": 30
|
|
92
97
|
},
|
|
93
98
|
{
|
|
94
99
|
"type": "command",
|
|
@@ -38,6 +38,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
38
38
|
| `js_conventions_constants.py` | Banned identifier set, boolean-prefix pattern, and declaration/JSDoc patterns for the JavaScript convention checks |
|
|
39
39
|
| `messages.py` | Short user-facing notice strings shown when a Stop hook redirects agent behavior |
|
|
40
40
|
| `multi_edit_reconstruction.py` | `apply_edits()` / `edits_for_tool()` — shared helpers that reconstruct the post-edit content of an Edit or MultiEdit, imported by the blockers that judge post-edit content |
|
|
41
|
+
| `nas_ssh_binary_enforcer_constants.py` | Bash tool name, the NAS address pattern, ssh-family basenames, OpenSSH binary path suffixes, launcher-wrapper set, shell control-operator tokens and split pattern, leading-assignment and duration patterns, the batch-mode pattern, and the two deny messages for the NAS ssh binary enforcer |
|
|
41
42
|
| `open_questions_in_plans_blocker_constants.py` | Patterns for detecting unresolved open questions in plan documents |
|
|
42
43
|
| `orphan_css_class_constants.py` | Scan radius and selector patterns for the orphan-CSS-class check |
|
|
43
44
|
| `package_inventory_stale_blocker_constants.py` | Inventory document names, production code extensions, backtick token pattern, smallest inventory size, exempt names, scan budget, and block-message text for the package-inventory stale-entry blocker |
|
|
@@ -47,6 +48,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
47
48
|
| `pr_converge_bugteam_enforcer_constants.py` | State keys and timing config for the bugteam-parallel enforcer |
|
|
48
49
|
| `pr_converge_bugteam_enforcer_state.py` | State-file helpers for the bugteam enforcer |
|
|
49
50
|
| `pr_description_enforcer_constants.py` | PR-description shape rules and command patterns |
|
|
51
|
+
| `pr_description_proof_of_work_constants.py` | Proof-part detection patterns, gh command tokens, and corrective messages for the proof-of-work comment audit |
|
|
50
52
|
| `pre_tool_use_stdin.py` | `read_hook_input_dictionary_from_stdin()` — shared stdin parser for PreToolUse hooks |
|
|
51
53
|
| `precommit_code_rules_gate_constants.py` | Scope argument and exit-code constants for the precommit gate |
|
|
52
54
|
| `project_paths_reader.py` | Loads `~/.claude/project-paths.json` — the per-user project-path registry |
|
|
@@ -26,6 +26,9 @@ MERGE_TREE_CONFLICT_EXIT_CODE: int = 1
|
|
|
26
26
|
MERGE_TREE_CLEAN_EXIT_CODE: int = 0
|
|
27
27
|
MERGE_TREE_TIMEOUT_SECONDS: int = 30
|
|
28
28
|
|
|
29
|
+
ALL_MERGE_HEAD_PROBE_FLAGS: tuple[str, ...] = ("rev-parse", "--verify", "--quiet", "MERGE_HEAD")
|
|
30
|
+
ALL_UNMERGED_PATHS_DIFF_FLAGS: tuple[str, ...] = ("diff", "--name-only", "--diff-filter=U")
|
|
31
|
+
|
|
29
32
|
ALL_NAME_ONLY_WORKTREE_DIFF_FLAGS: tuple[str, ...] = (
|
|
30
33
|
"-c",
|
|
31
34
|
"core.quotePath=false",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Constants for the NAS ssh binary enforcer hook.
|
|
2
|
+
|
|
3
|
+
Holds the Bash tool name, the NAS host-token pattern, the ssh-family command
|
|
4
|
+
basenames, the Windows OpenSSH binary path suffixes, the launcher-wrapper set, the
|
|
5
|
+
shell control-operator tokens and their split pattern, the leading-assignment and
|
|
6
|
+
launcher-duration patterns, the batch-mode pattern, and the two deny messages.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import re
|
|
10
|
+
|
|
11
|
+
BASH_TOOL_NAME = "Bash"
|
|
12
|
+
|
|
13
|
+
NAS_HOST_TOKEN_PATTERN = re.compile(r"(?:^|@)192\.168\.1\.100(?::|$)")
|
|
14
|
+
"""Match the NAS address only as a connection host.
|
|
15
|
+
|
|
16
|
+
Anchored to token start or an ``@`` and followed by ``:`` or token end, so the
|
|
17
|
+
address is matched as a bare host (``192.168.1.100``), a ``user@host``
|
|
18
|
+
(``jon@192.168.1.100``), or a ``host:path`` target (``192.168.1.100:/vol1/``,
|
|
19
|
+
``jon@192.168.1.100:/vol1/``). The address inside a remote-command argument
|
|
20
|
+
(``"ping 192.168.1.100"``) or a remote path component on another host
|
|
21
|
+
(``jon@other:/backup/192.168.1.100/``) does not count as a NAS connection.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
ALL_SSH_FAMILY_COMMAND_BASENAMES = frozenset(
|
|
25
|
+
{"ssh", "scp", "sftp", "ssh.exe", "scp.exe", "sftp.exe"}
|
|
26
|
+
)
|
|
27
|
+
ALL_OPENSSH_BINARY_PATH_SUFFIXES = (
|
|
28
|
+
"/openssh/ssh.exe",
|
|
29
|
+
"/openssh/scp.exe",
|
|
30
|
+
"/openssh/sftp.exe",
|
|
31
|
+
)
|
|
32
|
+
ALL_LAUNCHER_WRAPPER_COMMANDS = frozenset({"timeout", "nohup", "nice", "stdbuf", "setsid", "env"})
|
|
33
|
+
ALL_SHELL_CONTROL_OPERATOR_TOKENS = frozenset({"&&", "||", ";", "|", "&", "|&"})
|
|
34
|
+
CONTROL_OPERATOR_SPLIT_PATTERN = re.compile(r"(&&|\|\||;|\|&|\||(?<!>)&(?!>))")
|
|
35
|
+
LEADING_ASSIGNMENT_PATTERN = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*=")
|
|
36
|
+
LAUNCHER_DURATION_PATTERN = re.compile(r"^\d+[a-z]*$", re.IGNORECASE)
|
|
37
|
+
BATCH_MODE_PATTERN = re.compile(r"batchmode\s*=\s*yes", re.IGNORECASE)
|
|
38
|
+
|
|
39
|
+
BARE_SSH_BINARY_MESSAGE = (
|
|
40
|
+
"BLOCKED [nas-ssh-binary]: Git Bash's MSYS ssh reads ~/.ssh/id_ed25519 as "
|
|
41
|
+
"world-readable through its ACL mapping, rejects the key as bad permissions, "
|
|
42
|
+
"and falls back to an interactive password prompt that hangs unattended "
|
|
43
|
+
"sessions against the NAS at 192.168.1.100.\n\n"
|
|
44
|
+
"Use the Windows OpenSSH binary, which authenticates the key without prompting:\n"
|
|
45
|
+
' "/c/Windows/System32/OpenSSH/ssh.exe" -o BatchMode=yes -o ConnectTimeout=10 '
|
|
46
|
+
'-p 9222 jon@192.168.1.100 "<cmd>"\n\n'
|
|
47
|
+
"See ~/.claude/rules/nas-ssh-invocation.md for full guidance."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
MISSING_BATCH_MODE_MESSAGE = (
|
|
51
|
+
"BLOCKED [nas-ssh-binary]: this NAS ssh command uses the Windows OpenSSH binary "
|
|
52
|
+
"but omits -o BatchMode=yes, so an authentication regression falls back to an "
|
|
53
|
+
"interactive password prompt that hangs unattended sessions against the NAS at "
|
|
54
|
+
"192.168.1.100.\n\n"
|
|
55
|
+
"Add -o BatchMode=yes so a key failure exits loudly rather than prompting:\n"
|
|
56
|
+
' "/c/Windows/System32/OpenSSH/ssh.exe" -o BatchMode=yes -o ConnectTimeout=10 '
|
|
57
|
+
'-p 9222 jon@192.168.1.100 "<cmd>"\n\n'
|
|
58
|
+
"See ~/.claude/rules/nas-ssh-invocation.md for full guidance."
|
|
59
|
+
)
|
|
@@ -41,6 +41,7 @@ ALL_HEAVY_TESTING_HEADERS: frozenset[str] = frozenset(
|
|
|
41
41
|
{TEST_PLAN_HEADER, TESTING_HEADER, TESTS_HEADER, VERIFICATION_HEADER, VALIDATION_HEADER}
|
|
42
42
|
)
|
|
43
43
|
GH_PR_COMMAND_MIN_TOKEN_COUNT: int = 3
|
|
44
|
+
ALL_PR_NUMBER_BEARING_GH_PR_SUBCOMMANDS: frozenset[str] = frozenset({"edit", "comment", "ready"})
|
|
44
45
|
BODY_FILE_STDIN_SENTINEL: str = "-"
|
|
45
46
|
ATOMIC_WRITE_TEMP_SUFFIX: str = ".tmp"
|
|
46
47
|
SELF_CLOSING_REFERENCE_MESSAGE_PREFIX: str = "PR body references its own PR number #"
|
|
@@ -134,6 +135,7 @@ __all__ = [
|
|
|
134
135
|
"FLESCH_SYLLABLES_PER_WORD_COEFFICIENT",
|
|
135
136
|
"FLESCH_WORDS_PER_SENTENCE_COEFFICIENT",
|
|
136
137
|
"GH_PR_COMMAND_MIN_TOKEN_COUNT",
|
|
138
|
+
"ALL_PR_NUMBER_BEARING_GH_PR_SUBCOMMANDS",
|
|
137
139
|
"HEADING_LINE_PATTERN",
|
|
138
140
|
"HEAVY_MIN_BODY_CHARS_FOR_CLASSIFICATION",
|
|
139
141
|
"HEAVY_SHAPE",
|