claude-dev-env 1.90.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/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 +2 -0
- package/rules/nas-ssh-invocation.md +21 -0
- package/rules/proof-of-work-pr-comments.md +26 -0
- 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/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
|
@@ -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",
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Configuration constants for the proof-of-work PR-comment audit.
|
|
2
|
+
|
|
3
|
+
Holds the proof-part detection patterns, the gap and linkage word lists,
|
|
4
|
+
the gh command tokens the audit runs to read a PR's comments and diff, and
|
|
5
|
+
the corrective messages returned when a proof part is missing or when
|
|
6
|
+
``gh pr ready`` runs with no passing proof comment on the PR.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
|
|
13
|
+
ALL_PROOF_HEADING_KEYWORDS: tuple[str, ...] = ("proof", "verification")
|
|
14
|
+
|
|
15
|
+
PR_READY_INVOCATION_PATTERN: re.Pattern[str] = re.compile(r"(?:^|[;&|])\s*gh\s+pr\s+ready\b")
|
|
16
|
+
PR_READY_UNDO_FLAG: str = "--undo"
|
|
17
|
+
|
|
18
|
+
GH_EXECUTABLE: str = "gh"
|
|
19
|
+
GH_API_SUBCOMMAND: str = "api"
|
|
20
|
+
GH_PAGINATE_FLAG: str = "--paginate"
|
|
21
|
+
GH_SLURP_FLAG: str = "--slurp"
|
|
22
|
+
PR_COMMENTS_API_PATH_TEMPLATE: str = "repos/{{owner}}/{{repo}}/issues/{pr_number}/comments"
|
|
23
|
+
ALL_PR_DIFF_SUBCOMMANDS: tuple[str, ...] = ("pr", "diff")
|
|
24
|
+
PR_DIFF_NAME_ONLY_FLAG: str = "--name-only"
|
|
25
|
+
ALL_PR_VIEW_NUMBER_ARGUMENTS: tuple[str, ...] = ("pr", "view", "--json", "number")
|
|
26
|
+
PR_NUMBER_JSON_FIELD: str = "number"
|
|
27
|
+
COMMENT_BODY_JSON_FIELD: str = "body"
|
|
28
|
+
GH_COMMAND_TIMEOUT_SECONDS: int = 6
|
|
29
|
+
MAX_DIFF_SCAN_CHARS: int = 200000
|
|
30
|
+
|
|
31
|
+
ALL_VISUAL_FILE_SUFFIXES: frozenset[str] = frozenset(
|
|
32
|
+
{".png", ".jpg", ".jpeg", ".gif", ".webp", ".avif", ".svg", ".ico", ".html", ".css"}
|
|
33
|
+
)
|
|
34
|
+
HEX_COLOR_ADDED_LINE_PATTERN: re.Pattern[str] = re.compile(r"^\+.*#[0-9A-Fa-f]{6}\b", re.MULTILINE)
|
|
35
|
+
IMAGE_EMBED_PATTERN: re.Pattern[str] = re.compile(r"!\[[^\]]*\]\([^)\s]+\)")
|
|
36
|
+
ISSUE_REFERENCE_PATTERN: re.Pattern[str] = re.compile(r"#\d+")
|
|
37
|
+
PLAN_LINKAGE_KEYWORD_PATTERN: re.Pattern[str] = re.compile(
|
|
38
|
+
r"\b(?:issue|phase|plan|parent|advanc\w*|milestone|part of)\b", re.IGNORECASE
|
|
39
|
+
)
|
|
40
|
+
DIGIT_PATTERN: re.Pattern[str] = re.compile(r"\d")
|
|
41
|
+
ALL_HONEST_GAP_PHRASES: tuple[str, ...] = (
|
|
42
|
+
"gap",
|
|
43
|
+
"limitation",
|
|
44
|
+
"cannot",
|
|
45
|
+
"does not show",
|
|
46
|
+
"not shown",
|
|
47
|
+
"unverified",
|
|
48
|
+
"not covered",
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
PROOF_PART_COMMAND_MESSAGE: str = (
|
|
52
|
+
"proof comment missing a fenced command block showing the exact command(s) run on real data"
|
|
53
|
+
)
|
|
54
|
+
PROOF_PART_MEASURED_MESSAGE: str = (
|
|
55
|
+
"proof comment missing a measured-value element -- add a table row or bullet fact carrying "
|
|
56
|
+
"the numbers read from the produced artifact"
|
|
57
|
+
)
|
|
58
|
+
PROOF_PART_PLAN_LINKAGE_MESSAGE: str = (
|
|
59
|
+
"proof comment missing a plan-linkage sentence -- add one line naming the parent issue or "
|
|
60
|
+
"phase this PR advances, with its #number"
|
|
61
|
+
)
|
|
62
|
+
PROOF_PART_VISUAL_MESSAGE: str = (
|
|
63
|
+
"proof comment missing a visual element on a visual change -- embed an image (color "
|
|
64
|
+
"swatches, before/after screenshots) for values a human cannot read at a glance"
|
|
65
|
+
)
|
|
66
|
+
PROOF_PART_HONEST_GAPS_MESSAGE: str = (
|
|
67
|
+
"proof comment missing an honest-gaps statement -- say plainly what this offline proof "
|
|
68
|
+
"cannot show and what covers that gap"
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
PR_READY_GATE_MESSAGE_TEMPLATE: str = (
|
|
72
|
+
"BLOCKED: [PROOF_OF_WORK] PR #{pr_number} carries no passing proof-of-work comment. "
|
|
73
|
+
"Post one comment with a proof or verification heading carrying: "
|
|
74
|
+
"(1) the exact command(s) run on real data in a fenced code block, "
|
|
75
|
+
"(2) measured outcomes from the produced artifact as a table row or bullet facts, "
|
|
76
|
+
"(3) one sentence naming the parent issue or phase this PR advances, with its #number, "
|
|
77
|
+
"(4) an image embed when the diff touches images, HTML, CSS, or hex color values, and "
|
|
78
|
+
"(5) an honest statement of what the offline proof cannot show. "
|
|
79
|
+
"Then re-run gh pr ready."
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
__all__ = [
|
|
83
|
+
"ALL_HONEST_GAP_PHRASES",
|
|
84
|
+
"ALL_PROOF_HEADING_KEYWORDS",
|
|
85
|
+
"ALL_PR_DIFF_SUBCOMMANDS",
|
|
86
|
+
"ALL_PR_VIEW_NUMBER_ARGUMENTS",
|
|
87
|
+
"ALL_VISUAL_FILE_SUFFIXES",
|
|
88
|
+
"COMMENT_BODY_JSON_FIELD",
|
|
89
|
+
"DIGIT_PATTERN",
|
|
90
|
+
"GH_API_SUBCOMMAND",
|
|
91
|
+
"GH_COMMAND_TIMEOUT_SECONDS",
|
|
92
|
+
"GH_EXECUTABLE",
|
|
93
|
+
"GH_PAGINATE_FLAG",
|
|
94
|
+
"GH_SLURP_FLAG",
|
|
95
|
+
"HEX_COLOR_ADDED_LINE_PATTERN",
|
|
96
|
+
"IMAGE_EMBED_PATTERN",
|
|
97
|
+
"ISSUE_REFERENCE_PATTERN",
|
|
98
|
+
"MAX_DIFF_SCAN_CHARS",
|
|
99
|
+
"PLAN_LINKAGE_KEYWORD_PATTERN",
|
|
100
|
+
"PROOF_PART_COMMAND_MESSAGE",
|
|
101
|
+
"PROOF_PART_HONEST_GAPS_MESSAGE",
|
|
102
|
+
"PROOF_PART_MEASURED_MESSAGE",
|
|
103
|
+
"PROOF_PART_PLAN_LINKAGE_MESSAGE",
|
|
104
|
+
"PROOF_PART_VISUAL_MESSAGE",
|
|
105
|
+
"PR_COMMENTS_API_PATH_TEMPLATE",
|
|
106
|
+
"PR_DIFF_NAME_ONLY_FLAG",
|
|
107
|
+
"PR_NUMBER_JSON_FIELD",
|
|
108
|
+
"PR_READY_GATE_MESSAGE_TEMPLATE",
|
|
109
|
+
"PR_READY_INVOCATION_PATTERN",
|
|
110
|
+
"PR_READY_UNDO_FLAG",
|
|
111
|
+
]
|