claude-dev-env 1.92.1 → 1.93.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.
Files changed (214) hide show
  1. package/CLAUDE.md +6 -2
  2. package/_shared/CLAUDE.md +2 -1
  3. package/_shared/advisor/CLAUDE.md +21 -0
  4. package/_shared/advisor/advisor-protocol.md +142 -0
  5. package/_shared/advisor/scripts/config/advisor_scripts_constants/__init__.py +1 -0
  6. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +86 -0
  7. package/_shared/advisor/scripts/model_tier_run_validator.py +246 -0
  8. package/_shared/advisor/scripts/pyproject.toml +3 -0
  9. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +302 -0
  10. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +180 -0
  11. package/_shared/advisor/scripts/tier_model_ids.py +164 -0
  12. package/_shared/pr-loop/CLAUDE.md +1 -0
  13. package/_shared/pr-loop/precatch-rubric.md +65 -0
  14. package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
  15. package/_shared/pr-loop/scripts/code_rules_gate.py +137 -5
  16. package/_shared/pr-loop/scripts/post_audit_thread.py +1 -1
  17. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -1
  18. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +16 -0
  19. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +2 -1
  20. package/_shared/pr-loop/scripts/reviewer_availability.py +42 -12
  21. package/_shared/pr-loop/scripts/reviews_disabled.py +65 -21
  22. package/_shared/pr-loop/scripts/terminology_sweep.py +69 -21
  23. package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -1
  24. package/_shared/pr-loop/scripts/tests/fixtures/{copilot_internal_user_jonecho.json → copilot_internal_user_example.json} +1 -1
  25. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +1 -1
  26. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +146 -2
  27. package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +8 -8
  28. package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +18 -5
  29. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +56 -5
  30. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +104 -17
  31. package/agents/CLAUDE.md +3 -2
  32. package/agents/clasp-deployment-orchestrator.md +2 -2
  33. package/agents/code-advisor.md +7 -5
  34. package/agents/code-verifier.md +6 -2
  35. package/agents/session-advisor.md +27 -0
  36. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
  37. package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -1
  38. package/audit-rubrics/prompts/category-a-api-contracts.md +2 -2
  39. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
  40. package/audit-rubrics/prompts/category-c-resource-cleanup.md +2 -2
  41. package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +2 -2
  42. package/audit-rubrics/prompts/category-e-dead-code.md +2 -2
  43. package/audit-rubrics/prompts/category-f-silent-failures.md +2 -2
  44. package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
  45. package/audit-rubrics/prompts/category-h-security-boundaries.md +2 -2
  46. package/audit-rubrics/prompts/category-i-concurrency.md +2 -2
  47. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +2 -2
  48. package/audit-rubrics/prompts/category-k-codebase-conflicts.md +2 -2
  49. package/audit-rubrics/prompts/category-l-behavior-equivalence.md +2 -2
  50. package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +2 -2
  51. package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +2 -2
  52. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
  53. package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +2 -2
  54. package/bin/CLAUDE.md +1 -0
  55. package/bin/expand_home_directory_tokens.mjs +75 -0
  56. package/bin/install.mjs +21 -7
  57. package/bin/install.test.mjs +220 -10
  58. package/hooks/CLAUDE.md +1 -1
  59. package/hooks/blocking/CLAUDE.md +4 -1
  60. package/hooks/blocking/_path_setup.py +13 -0
  61. package/hooks/blocking/bash_pre_tool_use_dispatcher.py +281 -0
  62. package/hooks/blocking/code_rules_dead_module_constant.py +88 -16
  63. package/hooks/blocking/code_rules_enforcer.py +6 -0
  64. package/hooks/blocking/code_rules_imports_logging.py +34 -1
  65. package/hooks/blocking/code_rules_paths_syspath.py +1 -1
  66. package/hooks/blocking/code_rules_test_layout.py +272 -0
  67. package/hooks/blocking/nas_ssh_binary_enforcer.py +22 -12
  68. package/hooks/blocking/pii_prevention_blocker.py +781 -0
  69. package/hooks/blocking/pii_scanner.py +340 -0
  70. package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
  71. package/hooks/blocking/stop_dispatcher.py +84 -0
  72. package/hooks/blocking/test__path_setup.py +39 -0
  73. package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +61 -0
  74. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +294 -0
  75. package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +1 -1
  76. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +51 -0
  77. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +103 -0
  78. package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +1 -1
  79. package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +1 -1
  80. package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +11 -11
  81. package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +1 -1
  82. package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +1 -1
  83. package/hooks/blocking/test_code_rules_enforcer_test_layout.py +111 -0
  84. package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +76 -0
  85. package/hooks/blocking/test_destructive_command_blocker.py +29 -9
  86. package/hooks/blocking/test_gh_body_arg_blocker.py +3 -3
  87. package/hooks/blocking/test_nas_ssh_binary_enforcer.py +96 -39
  88. package/hooks/blocking/test_pii_prevention_blocker.py +905 -0
  89. package/hooks/blocking/test_pii_prevention_windows_git_path.py +41 -0
  90. package/hooks/blocking/test_pii_scanner.py +165 -0
  91. package/hooks/blocking/test_pr_description_enforcer_pr_number.py +1 -1
  92. package/hooks/blocking/test_pre_tool_use_dispatcher.py +14 -13
  93. package/hooks/blocking/test_stop_dispatcher.py +187 -0
  94. package/hooks/blocking/test_verdict_directory_write_blocker.py +1 -1
  95. package/hooks/blocking/test_volatile_path_in_post_blocker.py +3 -3
  96. package/hooks/blocking/volatile_path_in_post_blocker.py +54 -5
  97. package/hooks/diagnostic/migrations/README.md +25 -26
  98. package/hooks/diagnostic/test_hook_log_extractor.py +10 -10
  99. package/hooks/hooks.json +10 -120
  100. package/hooks/hooks_constants/CLAUDE.md +12 -3
  101. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +78 -0
  102. package/hooks/hooks_constants/dead_module_constant_constants.py +1 -0
  103. package/hooks/hooks_constants/hosted_hook_runner.py +73 -0
  104. package/hooks/hooks_constants/local_identity.py +182 -0
  105. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +4 -37
  106. package/hooks/hooks_constants/pii_prevention_constants.py +299 -0
  107. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  108. package/hooks/hooks_constants/python_style_checks_constants.py +15 -0
  109. package/hooks/hooks_constants/stop_dispatcher_constants.py +26 -0
  110. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +87 -0
  111. package/hooks/hooks_constants/test_hardcoded_user_path_constants.py +2 -2
  112. package/hooks/hooks_constants/test_hosted_hook_runner.py +101 -0
  113. package/hooks/hooks_constants/test_layout_constants.py +28 -0
  114. package/hooks/hooks_constants/test_local_identity.py +88 -0
  115. package/hooks/hooks_constants/test_session_env_cleanup_constants.py +1 -1
  116. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +32 -0
  117. package/hooks/validators/README.md +5 -6
  118. package/hooks/validators/__init__.py +2 -2
  119. package/hooks/validators/python_style_checks.py +298 -243
  120. package/hooks/validators/run_all_validators.py +6 -0
  121. package/hooks/validators/test_python_style_checks.py +278 -163
  122. package/package.json +2 -2
  123. package/rules/CLAUDE.md +2 -1
  124. package/rules/bdd.md +1 -1
  125. package/rules/nas-ssh-invocation.md +6 -4
  126. package/rules/no-justification-noise.md +61 -0
  127. package/scripts/test_setup_project_paths.py +1 -1
  128. package/skills/CLAUDE.md +6 -3
  129. package/skills/_shared/pr-loop/scripts/_path_resolver.py +2 -2
  130. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
  131. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
  132. package/skills/_shared/pr-loop/scripts/test__path_resolver.py +42 -3
  133. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +11 -11
  134. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +26 -3
  135. package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +49 -25
  136. package/skills/autoconverge/CLAUDE.md +3 -3
  137. package/skills/autoconverge/SKILL.md +113 -35
  138. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  139. package/skills/autoconverge/reference/convergence.md +44 -22
  140. package/skills/autoconverge/reference/gotchas.md +11 -0
  141. package/skills/autoconverge/reference/stop-conditions.md +23 -13
  142. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  143. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +15 -19
  144. package/skills/autoconverge/workflow/converge.contract.test.mjs +43 -42
  145. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +141 -16
  146. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +2 -2
  147. package/skills/autoconverge/workflow/converge.mjs +329 -68
  148. package/skills/autoconverge/workflow/converge.path-aware.test.mjs +1 -1
  149. package/skills/autoconverge/workflow/converge.precatch.test.mjs +152 -0
  150. package/skills/autoconverge/workflow/converge.run-input.test.mjs +3 -3
  151. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +15 -15
  152. package/skills/autoconverge/workflow/test_convergence_summary.py +6 -6
  153. package/skills/bdd-protocol/SKILL.md +4 -5
  154. package/skills/bdd-protocol/references/anti-patterns.md +1 -1
  155. package/skills/bugteam/SKILL.md +8 -0
  156. package/skills/bugteam/reference/copilot-gap-analysis.md +20 -20
  157. package/skills/bugteam/reference/obstacles/audit-walk-categories.md +1 -1
  158. package/skills/bugteam/reference/team-setup.md +7 -5
  159. package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
  160. package/skills/copilot-finding-triage/SKILL.md +124 -0
  161. package/skills/copilot-finding-triage/reference/tier-rubric.md +49 -0
  162. package/skills/copilot-finding-triage/scripts/conftest.py +8 -0
  163. package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/__init__.py +0 -0
  164. package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/__init__.py +0 -0
  165. package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/notify_ntfy_constants.py +32 -0
  166. package/skills/copilot-finding-triage/scripts/notify_ntfy.py +192 -0
  167. package/skills/copilot-finding-triage/scripts/test_notify_ntfy.py +141 -0
  168. package/skills/copilot-finding-triage/templates/notification.md +34 -0
  169. package/skills/copilot-review/SKILL.md +8 -6
  170. package/skills/findbugs/SKILL.md +4 -0
  171. package/skills/fixbugs/SKILL.md +8 -7
  172. package/skills/gotcha/CLAUDE.md +2 -2
  173. package/skills/gotcha/SKILL.md +4 -4
  174. package/skills/log-audit/SKILL.md +4 -6
  175. package/skills/monitor-open-prs/CLAUDE.md +1 -1
  176. package/skills/monitor-open-prs/SKILL.md +6 -1
  177. package/skills/orchestrator/SKILL.md +74 -78
  178. package/skills/orchestrator-refresh/SKILL.md +24 -12
  179. package/skills/post-audit-findings/SKILL.md +5 -9
  180. package/skills/pr-consistency-audit/SKILL.md +5 -1
  181. package/skills/pr-converge/CLAUDE.md +1 -1
  182. package/skills/pr-converge/SKILL.md +86 -47
  183. package/skills/pr-converge/reference/CLAUDE.md +1 -1
  184. package/skills/pr-converge/reference/convergence-gates.md +20 -17
  185. package/skills/pr-converge/reference/examples.md +63 -47
  186. package/skills/pr-converge/reference/fix-protocol.md +15 -15
  187. package/skills/pr-converge/reference/ground-rules.md +11 -7
  188. package/skills/pr-converge/reference/multi-pr-orchestration.md +6 -5
  189. package/skills/pr-converge/reference/per-tick.md +129 -107
  190. package/skills/pr-converge/reference/state-schema.md +15 -10
  191. package/skills/pr-converge/scripts/check_convergence.py +5 -4
  192. package/skills/pr-converge/scripts/test_check_convergence.py +6 -4
  193. package/skills/pr-fix-protocol/SKILL.md +3 -8
  194. package/skills/pr-loop-cloud-transport/SKILL.md +102 -0
  195. package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +40 -0
  196. package/skills/pr-loop-cloud-transport/reference/substitution-matrix.md +48 -0
  197. package/skills/pr-loop-lifecycle/SKILL.md +5 -9
  198. package/skills/pr-scope-resolve/SKILL.md +3 -6
  199. package/skills/privacy-hygiene/SKILL.md +114 -0
  200. package/skills/qbug/SKILL.md +8 -8
  201. package/skills/rebase/SKILL.md +5 -1
  202. package/skills/refine/SKILL.md +4 -5
  203. package/skills/reviewer-gates/SKILL.md +7 -11
  204. package/skills/session-log/SKILL.md +4 -1
  205. package/skills/skill-builder/SKILL.md +3 -6
  206. package/skills/structure-prompt/SKILL.md +4 -5
  207. package/skills/team-advisor/SKILL.md +56 -0
  208. package/skills/test_markdown_link_integrity.py +10 -6
  209. package/skills/update/SKILL.md +5 -1
  210. package/skills/usage-pause/SKILL.md +14 -5
  211. package/skills/usage-pause/scripts/resolve_usage_window.py +83 -5
  212. package/skills/usage-pause/scripts/test_resolve_usage_window.py +185 -17
  213. package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +4 -3
  214. package/skills/verified-build/SKILL.md +4 -9
@@ -0,0 +1,101 @@
1
+ """Behavior tests for the shared in-process hosted-hook runner.
2
+
3
+ Each test writes a real probe hook to a temp directory, runs it through
4
+ run_hook_capturing_output, and asserts on the captured stdout, the crash flag,
5
+ and the restoration of the interpreter's stdin, stdout, and argv.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import sys
12
+ from pathlib import Path
13
+
14
+ _HOOKS_DIR = str(Path(__file__).resolve().parent.parent)
15
+ if _HOOKS_DIR not in sys.path:
16
+ sys.path.insert(0, _HOOKS_DIR)
17
+
18
+ from hooks_constants.hosted_hook_runner import HostedHookRun, run_hook_capturing_output # noqa: E402
19
+
20
+
21
+ def _write_probe(tmp_path: Path, body: str) -> str:
22
+ """Write a probe hook script and return its absolute path."""
23
+ probe_path = tmp_path / "probe_hook.py"
24
+ probe_path.write_text(body, encoding="utf-8")
25
+ return str(probe_path)
26
+
27
+
28
+ def test_captures_stdout_from_a_printing_hook(tmp_path: Path) -> None:
29
+ """The runner captures what a hook prints to stdout without a crash."""
30
+ printed_payload = json.dumps({"hookSpecificOutput": {"permissionDecision": "deny"}})
31
+ probe_path = _write_probe(tmp_path, f"print({printed_payload!r})\n")
32
+ hook_run = run_hook_capturing_output(probe_path, "{}")
33
+ assert hook_run.captured_stdout.strip() == printed_payload
34
+ assert hook_run.did_crash is False
35
+
36
+
37
+ def test_hook_reads_the_swapped_stdin(tmp_path: Path) -> None:
38
+ """A hook reading stdin sees the payload the runner replays."""
39
+ probe_path = _write_probe(
40
+ tmp_path,
41
+ "import json\nimport sys\nprint(json.load(sys.stdin)['tool_name'])\n",
42
+ )
43
+ hook_run = run_hook_capturing_output(probe_path, json.dumps({"tool_name": "Bash"}))
44
+ assert hook_run.captured_stdout.strip() == "Bash"
45
+ assert hook_run.did_crash is False
46
+
47
+
48
+ def test_sets_argv_to_the_hook_script_path(tmp_path: Path) -> None:
49
+ """The runner sets argv to the hook's own path so argv-branching hooks behave."""
50
+ argv_result_path = tmp_path / "observed_argv.json"
51
+ probe_path = _write_probe(
52
+ tmp_path,
53
+ "import json\nimport sys\nfrom pathlib import Path\n"
54
+ f"Path({str(argv_result_path)!r}).write_text(json.dumps(sys.argv), encoding='utf-8')\n",
55
+ )
56
+ run_hook_capturing_output(probe_path, "{}")
57
+ observed_argv = json.loads(argv_result_path.read_text(encoding="utf-8"))
58
+ assert observed_argv == [probe_path]
59
+
60
+
61
+ def test_restores_stdin_stdout_and_argv_after_the_run(tmp_path: Path) -> None:
62
+ """The runner restores stdin, stdout, and argv to their pre-call objects."""
63
+ probe_path = _write_probe(tmp_path, "print('anything')\n")
64
+ original_stdin = sys.stdin
65
+ original_stdout = sys.stdout
66
+ original_argv = sys.argv
67
+ run_hook_capturing_output(probe_path, "{}")
68
+ assert sys.stdin is original_stdin
69
+ assert sys.stdout is original_stdout
70
+ assert sys.argv is original_argv
71
+
72
+
73
+ def test_crash_reports_did_crash_and_does_not_propagate(tmp_path: Path) -> None:
74
+ """A hook that raises is contained: did_crash is True and no exception escapes."""
75
+ probe_path = _write_probe(tmp_path, "raise ValueError('boom')\n")
76
+ hook_run = run_hook_capturing_output(probe_path, "{}")
77
+ assert hook_run.did_crash is True
78
+ assert hook_run.captured_stdout == ""
79
+
80
+
81
+ def test_system_exit_zero_after_output_is_captured(tmp_path: Path) -> None:
82
+ """A hook that prints then exits zero returns its output and is not a crash."""
83
+ probe_path = _write_probe(tmp_path, "print('decided')\nimport sys\nsys.exit(0)\n")
84
+ hook_run = run_hook_capturing_output(probe_path, "{}")
85
+ assert hook_run.captured_stdout.strip() == "decided"
86
+ assert hook_run.did_crash is False
87
+
88
+
89
+ def test_system_exit_nonzero_without_output_is_not_a_crash(tmp_path: Path) -> None:
90
+ """A clean nonzero SystemExit yields no output and is not treated as a crash."""
91
+ probe_path = _write_probe(tmp_path, "import sys\nsys.exit(2)\n")
92
+ hook_run = run_hook_capturing_output(probe_path, "{}")
93
+ assert hook_run.did_crash is False
94
+ assert hook_run.captured_stdout == ""
95
+
96
+
97
+ def test_returns_a_hosted_hook_run_instance(tmp_path: Path) -> None:
98
+ """The runner returns a HostedHookRun carrying the two observable fields."""
99
+ probe_path = _write_probe(tmp_path, "print('x')\n")
100
+ hook_run = run_hook_capturing_output(probe_path, "{}")
101
+ assert isinstance(hook_run, HostedHookRun)
@@ -0,0 +1,28 @@
1
+ """Constants for the test-module dead-scaffolding checks in ``code_rules_enforcer``.
2
+
3
+ Lives under the hooks-tree ``hooks_constants`` package so module-level
4
+ UPPER_SNAKE constants satisfy the CODE_RULES "constants live in config"
5
+ requirement and share a home with the other hook-tree configuration. The
6
+ values here drive two test-file checks that catch scaffolding a removed
7
+ monkeypatch line leaves behind: a dead private module constant, and an
8
+ unused private-helper parameter.
9
+ """
10
+
11
+ import re
12
+
13
+ PRIVATE_NAME_PREFIX: str = "_"
14
+ TEST_FUNCTION_MODULE_BASENAME_PATTERN: re.Pattern[str] = re.compile(r"^(test_.+|.+_test)\.py$")
15
+ FIXTURE_DECORATOR_MARKER: str = "fixture"
16
+ SELF_PARAMETER_NAME: str = "self"
17
+ CLASS_METHOD_FIRST_PARAMETER_NAME: str = "cls"
18
+ MINIMUM_CONSTANT_NAME_LENGTH: int = 2
19
+ MAX_TEST_LAYOUT_ISSUES: int = 50
20
+ DEAD_TEST_CONSTANT_GUIDANCE: str = (
21
+ "module-level constant is defined in this test module but read by no other"
22
+ " line in the file - remove the dead constant, or reference it where its"
23
+ " value is needed (CODE_RULES §9.8)"
24
+ )
25
+ UNUSED_TEST_HELPER_PARAMETER_GUIDANCE: str = (
26
+ "parameter is declared on this private test helper but read nowhere in its"
27
+ " body - drop the dead parameter and stop forwarding it at every call site"
28
+ )
@@ -0,0 +1,88 @@
1
+ """Tests for the NAS local-identity loader used by the ssh enforcer hook."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import sys
8
+ from pathlib import Path
9
+ from unittest.mock import patch
10
+
11
+ sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
12
+
13
+ from hooks_constants import local_identity # noqa: E402
14
+
15
+ ALL_NAS_ENV_VARS = ("CLAUDE_NAS_HOST", "CLAUDE_NAS_SSH_USER", "CLAUDE_NAS_SSH_PORT")
16
+
17
+
18
+ def _clear_nas_env() -> None:
19
+ for each_env_var in ALL_NAS_ENV_VARS:
20
+ os.environ.pop(each_env_var, None)
21
+
22
+
23
+ class TestNasValuesFromEnvironment:
24
+ def should_read_host_user_and_port_from_the_environment(self) -> None:
25
+ with patch.dict(
26
+ os.environ,
27
+ {
28
+ "CLAUDE_NAS_HOST": "10.0.0.5",
29
+ "CLAUDE_NAS_SSH_USER": "tester",
30
+ "CLAUDE_NAS_SSH_PORT": "2200",
31
+ },
32
+ clear=False,
33
+ ):
34
+ assert local_identity.nas_host() == "10.0.0.5"
35
+ assert local_identity.nas_ssh_user() == "tester"
36
+ assert local_identity.nas_ssh_port() == 2200
37
+
38
+
39
+ class TestNasValuesFromLocalFile:
40
+ def should_read_host_user_and_port_from_the_local_identity_file(
41
+ self, tmp_path: Path
42
+ ) -> None:
43
+ claude_home = tmp_path / ".claude"
44
+ claude_home.mkdir()
45
+ (claude_home / "local-identity.json").write_text(
46
+ json.dumps(
47
+ {"nas": {"host": "10.1.1.9", "ssh_user": "fileuser", "ssh_port": 2222}}
48
+ ),
49
+ encoding="utf-8",
50
+ )
51
+ with patch.dict(os.environ, {}, clear=False):
52
+ _clear_nas_env()
53
+ with patch.object(Path, "home", return_value=tmp_path):
54
+ assert local_identity.nas_host() == "10.1.1.9"
55
+ assert local_identity.nas_ssh_user() == "fileuser"
56
+ assert local_identity.nas_ssh_port() == 2222
57
+
58
+
59
+ class TestNasValuesPlaceholderDefault:
60
+ def should_return_placeholders_when_no_env_and_no_file(self, tmp_path: Path) -> None:
61
+ with patch.dict(os.environ, {}, clear=False):
62
+ _clear_nas_env()
63
+ with patch.object(Path, "home", return_value=tmp_path):
64
+ assert local_identity.nas_host() == "nas.example.local"
65
+ assert local_identity.nas_ssh_user() == "operator"
66
+ assert local_identity.nas_ssh_port() == 22
67
+
68
+
69
+ class TestDenyMessagesQuoteTheResolvedHost:
70
+ def should_include_the_resolved_host_and_port_in_the_bare_binary_message(
71
+ self,
72
+ ) -> None:
73
+ with patch.dict(
74
+ os.environ,
75
+ {"CLAUDE_NAS_HOST": "10.0.0.5", "CLAUDE_NAS_SSH_PORT": "2200"},
76
+ clear=False,
77
+ ):
78
+ os.environ.pop("CLAUDE_NAS_SSH_USER", None)
79
+ message = local_identity.bare_ssh_binary_deny_message()
80
+ assert "10.0.0.5" in message
81
+ assert "-p 2200" in message
82
+ assert "BatchMode=yes" in message
83
+
84
+ def should_include_the_resolved_host_in_the_missing_batch_mode_message(self) -> None:
85
+ with patch.dict(os.environ, {"CLAUDE_NAS_HOST": "10.0.0.5"}, clear=False):
86
+ message = local_identity.missing_batch_mode_deny_message()
87
+ assert "10.0.0.5" in message
88
+ assert "BatchMode=yes" in message
@@ -41,7 +41,7 @@ class TestSessionIdPatternRejects:
41
41
  assert SESSION_ID_PATTERN.match("etc/passwd") is None
42
42
 
43
43
  def test_rejects_back_slash(self) -> None:
44
- assert SESSION_ID_PATTERN.match("Users\\jon") is None
44
+ assert SESSION_ID_PATTERN.match("Users\\example") is None
45
45
 
46
46
  def test_rejects_parent_traversal(self) -> None:
47
47
  assert SESSION_ID_PATTERN.match("..") is None
@@ -0,0 +1,32 @@
1
+ """Tests for the Stop-hook dispatcher hosted-hook roster."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import sys
6
+ from pathlib import Path
7
+
8
+ _HOOKS_DIR = str(Path(__file__).resolve().parent.parent)
9
+ if _HOOKS_DIR not in sys.path:
10
+ sys.path.insert(0, _HOOKS_DIR)
11
+
12
+ from hooks_constants.stop_dispatcher_constants import ALL_STOP_HOSTED_HOOK_PATHS # noqa: E402
13
+
14
+ _EXPECTED_STOP_ORDER = (
15
+ "blocking/hedging_language_blocker.py",
16
+ "blocking/question_to_user_enforcer.py",
17
+ "blocking/intent_only_ending_blocker.py",
18
+ "blocking/session_handoff_blocker.py",
19
+ "diagnostic/hook_log_stop_wrapper.py",
20
+ )
21
+
22
+
23
+ def test_roster_lists_all_stop_hooks_in_registration_order() -> None:
24
+ """The roster reproduces the standalone Stop chain order exactly."""
25
+ assert ALL_STOP_HOSTED_HOOK_PATHS == _EXPECTED_STOP_ORDER
26
+
27
+
28
+ def test_every_roster_path_points_at_an_existing_hook() -> None:
29
+ """Each roster path names a hook script present on disk, catching a typo."""
30
+ hooks_root = Path(__file__).resolve().parent.parent
31
+ for each_relative_path in ALL_STOP_HOSTED_HOOK_PATHS:
32
+ assert (hooks_root / each_relative_path).is_file()
@@ -6,7 +6,7 @@ AST-based Python style checks for code quality enforcement.
6
6
 
7
7
  1. **Imports at top** - All import statements must be at the top of the file
8
8
  2. **No empty line after decorators** - Decorators must be directly above functions (no blank lines)
9
- 3. **Single empty line between functions** - Exactly one blank line between top-level functions
9
+ 3. **Two empty lines between functions** - Exactly two blank lines between top-level functions
10
10
  4. **View function naming** - Functions in `views.py` with `request` parameter must end with `_view`
11
11
 
12
12
  ## Usage
@@ -39,7 +39,7 @@ import ast
39
39
  from python_style_checks import (
40
40
  check_imports_at_top,
41
41
  check_no_empty_line_after_decorators,
42
- check_single_empty_line_between_functions,
42
+ check_blank_lines_between_functions,
43
43
  check_view_function_naming,
44
44
  )
45
45
 
@@ -49,7 +49,7 @@ tree = ast.parse(source)
49
49
  # Run individual checks
50
50
  violations = check_imports_at_top(tree, "myfile.py")
51
51
  violations = check_no_empty_line_after_decorators(source, "myfile.py")
52
- violations = check_single_empty_line_between_functions(source, "myfile.py")
52
+ violations = check_blank_lines_between_functions(source, "myfile.py")
53
53
  violations = check_view_function_naming(tree, "views.py")
54
54
  ```
55
55
 
@@ -75,7 +75,7 @@ def foo() -> None:
75
75
  """Do something."""
76
76
  pass
77
77
 
78
- @decorator
78
+
79
79
  def bar() -> None:
80
80
  """Another function."""
81
81
  pass
@@ -100,8 +100,7 @@ def bar() -> None: # VIOLATION: No empty line after decorator
100
100
  def baz() -> None:
101
101
  pass
102
102
 
103
-
104
- def qux() -> None: # VIOLATION: Expected 1 empty line, found 2
103
+ def qux() -> None: # VIOLATION: Expected 2 empty lines, found 1
105
104
  pass
106
105
 
107
106
  # View naming (in views.py)
@@ -2,18 +2,18 @@
2
2
 
3
3
  from .python_style_checks import (
4
4
  Violation,
5
+ check_blank_lines_between_functions,
5
6
  check_imports_at_top,
6
7
  check_no_empty_line_after_decorators,
7
- check_single_empty_line_between_functions,
8
8
  check_view_function_naming,
9
9
  validate_file,
10
10
  )
11
11
 
12
12
  __all__ = [
13
13
  "Violation",
14
+ "check_blank_lines_between_functions",
14
15
  "check_imports_at_top",
15
16
  "check_no_empty_line_after_decorators",
16
- "check_single_empty_line_between_functions",
17
17
  "check_view_function_naming",
18
18
  "validate_file",
19
19
  ]