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.
Files changed (64) hide show
  1. package/CLAUDE.md +1 -52
  2. package/agents/clean-coder.md +2 -2
  3. package/agents/code-verifier.md +0 -1
  4. package/agents/test_agent_frontmatter.py +78 -0
  5. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
  6. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +1 -1
  7. package/bin/install.mjs +1 -0
  8. package/docs/CODE_RULES.md +2 -2
  9. package/hooks/blocking/CLAUDE.md +5 -2
  10. package/hooks/blocking/code_rules_comments.py +2 -2
  11. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +44 -0
  12. package/hooks/blocking/config/verified_commit_constants.py +2 -0
  13. package/hooks/blocking/conftest.py +115 -0
  14. package/hooks/blocking/nas_ssh_binary_enforcer.py +191 -0
  15. package/hooks/blocking/pr_description_enforcer.py +46 -22
  16. package/hooks/blocking/pr_description_pr_number.py +5 -3
  17. package/hooks/blocking/pr_description_proof_of_work.py +367 -0
  18. package/hooks/blocking/precommit_code_rules_gate.py +5 -1
  19. package/hooks/blocking/test_code_rules_enforcer_comment_string_awareness.py +8 -2
  20. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +71 -0
  21. package/hooks/blocking/test_nas_ssh_binary_enforcer.py +168 -0
  22. package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +175 -0
  23. package/hooks/blocking/test_pr_description_proof_of_work.py +162 -0
  24. package/hooks/blocking/test_precommit_code_rules_gate.py +89 -0
  25. package/hooks/blocking/test_verdict_directory_write_blocker.py +4 -0
  26. package/hooks/blocking/test_verification_verdict_store.py +11 -0
  27. package/hooks/blocking/test_verified_commit_config_bootstrap.py +49 -0
  28. package/hooks/blocking/test_verified_commit_gate.py +11 -0
  29. package/hooks/blocking/test_verifier_verdict_minter.py +11 -0
  30. package/hooks/blocking/verdict_directory_write_blocker.py +6 -0
  31. package/hooks/blocking/verification_verdict_store.py +73 -5
  32. package/hooks/blocking/verified_commit_config_bootstrap.py +51 -0
  33. package/hooks/blocking/verified_commit_gate.py +6 -0
  34. package/hooks/blocking/verifier_verdict_minter.py +6 -0
  35. package/hooks/hooks.json +7 -2
  36. package/hooks/hooks_constants/CLAUDE.md +2 -0
  37. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  38. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +3 -0
  39. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +59 -0
  40. package/hooks/hooks_constants/pr_description_enforcer_constants.py +2 -0
  41. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +111 -0
  42. package/hooks/validators/run_all_validators.py +216 -4
  43. package/hooks/validators/test_run_all_validators_pretooluse.py +102 -0
  44. package/package.json +1 -1
  45. package/rules/CLAUDE.md +7 -0
  46. package/rules/nas-ssh-invocation.md +21 -0
  47. package/rules/proof-of-work-pr-comments.md +26 -0
  48. package/rules/testing.md +0 -2
  49. package/scripts/CLAUDE.md +1 -0
  50. package/scripts/claude-chain.example.json +8 -0
  51. package/scripts/claude_chain_runner.py +400 -0
  52. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -0
  53. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +124 -0
  54. package/scripts/sync_to_cursor/rules.py +1 -1
  55. package/scripts/test_claude_chain_runner.py +472 -0
  56. package/skills/CLAUDE.md +3 -0
  57. package/skills/orchestrator/SKILL.md +188 -0
  58. package/skills/orchestrator-refresh/SKILL.md +25 -0
  59. package/skills/usage-pause/SKILL.md +108 -0
  60. package/skills/usage-pause/scripts/resolve_usage_window.py +462 -0
  61. package/skills/usage-pause/scripts/test_resolve_usage_window.py +278 -0
  62. package/skills/usage-pause/scripts/usage_pause_constants/__init__.py +1 -0
  63. package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +65 -0
  64. 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 HEAD's configured upstream tracking
129
- reference (so a non-standard default branch like ``origin/develop`` is
130
- found regardless of its name), then the fixed ``origin/main`` /
131
- ``origin/master`` fallbacks for checkouts with no tracking ref set.
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
- + ((tracked_upstream,) if tracked_upstream else ())
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": 10
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 |
@@ -24,6 +24,7 @@ ALL_CONFIG_DIRECTORY_NAMES = frozenset(
24
24
  "bugteam_scripts_constants",
25
25
  "implement_scripts_constants",
26
26
  "log_audit_constants",
27
+ "usage_pause_constants",
27
28
  "dev_env_scripts_constants",
28
29
  }
29
30
  )
@@ -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",