claude-dev-env 2.4.0 → 2.5.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 (195) hide show
  1. package/CLAUDE.md +53 -49
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +4 -2
  4. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
  5. package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
  6. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
  7. package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
  8. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
  9. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
  10. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
  11. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
  12. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
  13. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
  14. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
  15. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
  16. package/_shared/pr-loop/worker-spawn.md +1 -1
  17. package/agents/CLAUDE.md +2 -1
  18. package/agents/caveman.md +0 -1
  19. package/agents/clasp-deployment-orchestrator.md +0 -1
  20. package/agents/clean-coder.md +0 -1
  21. package/agents/code-advisor.md +0 -1
  22. package/agents/code-quality-agent.md +1 -2
  23. package/agents/code-verifier.md +0 -1
  24. package/agents/deep-research.md +0 -1
  25. package/agents/docs-agent.md +0 -1
  26. package/agents/git-commit-crafter.md +0 -1
  27. package/agents/issue-tracker.md +42 -0
  28. package/agents/plan-packet-validator.md +0 -1
  29. package/agents/pr-description-writer.md +0 -1
  30. package/agents/test_agent_frontmatter.py +67 -18
  31. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +143 -141
  32. package/bin/CLAUDE.md +68 -5
  33. package/bin/ever-shipped-skills.mjs +1 -0
  34. package/bin/install-constants.mjs +88 -0
  35. package/bin/install.mjs +1138 -114
  36. package/bin/install.prune.test.mjs +869 -19
  37. package/bin/install.test.mjs +906 -2
  38. package/commands/implement.md +1 -1
  39. package/commands/right-size.md +1 -1
  40. package/docs/CLAUDE.md +1 -0
  41. package/docs/host-pool-health-monitor.md +102 -0
  42. package/docs/references/CLAUDE.md +4 -2
  43. package/docs/references/advisor-tool.md +13 -0
  44. package/docs/references/code-review-enforcement.md +10 -0
  45. package/docs/references/team-advisor-skill.md +14 -0
  46. package/hooks/blocking/CLAUDE.md +1 -0
  47. package/hooks/blocking/code_review_pr_create_gate.py +7 -3
  48. package/hooks/blocking/code_review_push_gate.py +9 -4
  49. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +8 -0
  50. package/hooks/blocking/config/__init__.py +5 -5
  51. package/hooks/blocking/config/code_review_enforcement_constants.py +4 -1
  52. package/hooks/blocking/config/test_code_review_enforcement_constants.py +5 -0
  53. package/hooks/blocking/config/verified_commit_constants.py +160 -159
  54. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
  55. package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
  56. package/hooks/blocking/test_code_review_pr_create_gate.py +14 -0
  57. package/hooks/blocking/test_code_review_push_gate.py +16 -0
  58. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +19 -0
  59. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
  60. package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
  61. package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
  62. package/hooks/blocking/test_verification_verdict_store.py +54 -0
  63. package/hooks/blocking/test_verified_commit_gate.py +581 -581
  64. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
  65. package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
  66. package/hooks/blocking/verification_verdict_store.py +1039 -1036
  67. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
  68. package/hooks/blocking/verifier_verdict_minter.py +280 -280
  69. package/hooks/git-hooks/test_pre_push.py +25 -0
  70. package/hooks/hooks.json +10 -0
  71. package/hooks/hooks_constants/CLAUDE.md +2 -1
  72. package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
  73. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
  74. package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
  75. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
  76. package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
  77. package/hooks/pyproject.toml +1 -0
  78. package/hooks/validators/CLAUDE.md +1 -0
  79. package/hooks/validators/config/__init__.py +0 -0
  80. package/hooks/validators/config/directory_exemption_constants.py +183 -0
  81. package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
  82. package/hooks/validators/conftest.py +4 -0
  83. package/hooks/validators/ruff_integration.py +49 -5
  84. package/hooks/validators/run_all_validators.py +206 -9
  85. package/hooks/validators/test_directory_exemption_constants.py +185 -0
  86. package/hooks/validators/test_python_antipattern_checks.py +110 -5
  87. package/hooks/validators/test_ruff_integration.py +92 -1
  88. package/hooks/validators/test_run_all_validators.py +115 -68
  89. package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
  90. package/package.json +10 -2
  91. package/rules/CLAUDE.md +1 -0
  92. package/rules/docstring-prose-matches-implementation.md +45 -44
  93. package/rules/state-what-is.md +25 -0
  94. package/rules/verified-commit-gate-skip.md +1 -1
  95. package/scripts/CLAUDE.md +1 -0
  96. package/scripts/Capture-PoolHealth.ps1 +410 -0
  97. package/scripts/_code_review_test_support.py +404 -0
  98. package/scripts/claude_chain_runner.py +141 -1
  99. package/scripts/conftest.py +16 -1
  100. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
  101. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +9 -0
  102. package/scripts/resolve_worker_spawn.py +626 -626
  103. package/scripts/spawn_grok_batch.py +672 -672
  104. package/scripts/test_claude_chain_runner.py +131 -0
  105. package/scripts/test_invoke_code_review_chain.py +70 -0
  106. package/scripts/test_invoke_code_review_cli.py +192 -0
  107. package/scripts/test_invoke_code_review_contract.py +256 -0
  108. package/scripts/test_invoke_code_review_git.py +123 -0
  109. package/scripts/test_invoke_code_review_mode.py +99 -0
  110. package/scripts/test_resolve_worker_spawn.py +1014 -1014
  111. package/skills/CLAUDE.md +2 -0
  112. package/skills/auditing-claude-config/SKILL.md +114 -114
  113. package/skills/autoconverge/SKILL.md +427 -427
  114. package/skills/autoconverge/reference/convergence.md +24 -3
  115. package/skills/autoconverge/workflow/CLAUDE.md +1 -0
  116. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +3 -3
  117. package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1263
  118. package/skills/autoconverge/workflow/converge.mjs +167 -0
  119. package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
  120. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
  121. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
  122. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
  123. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
  124. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
  125. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
  126. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
  127. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
  128. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
  129. package/skills/closeout/SKILL.md +33 -50
  130. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
  131. package/skills/codex-review/scripts/run_codex_review.py +233 -1
  132. package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
  133. package/skills/condensing-instructions/SKILL.md +81 -0
  134. package/skills/copilot-review/SKILL.md +119 -119
  135. package/skills/e-code-review/SKILL.md +52 -0
  136. package/skills/e-code-review/reference/fix.md +54 -0
  137. package/skills/e-code-review/reference/loop.md +43 -0
  138. package/skills/e-code-review/reference/low.md +57 -0
  139. package/skills/e-code-review/reference/medium.md +153 -0
  140. package/skills/e-code-review/reference/xhigh.md +182 -0
  141. package/skills/e-simplify/SKILL.md +97 -0
  142. package/skills/issue-tracker/SKILL.md +92 -0
  143. package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
  144. package/skills/issue-tracker/reference/handoff-schema.md +64 -0
  145. package/skills/issue-tracker/reference/operation-matrix.md +41 -0
  146. package/skills/orchestrator/SKILL.md +162 -21
  147. package/skills/orchestrator/scripts/status_gate.py +625 -0
  148. package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
  149. package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
  150. package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
  151. package/skills/orchestrator/scripts/test_status_gate.py +439 -0
  152. package/skills/orchestrator-refresh/SKILL.md +110 -35
  153. package/skills/plan-to-pr/SKILL.md +155 -0
  154. package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
  155. package/skills/plan-to-pr/reference/model-routing.md +36 -0
  156. package/skills/plan-to-pr/reference/packet-contract.md +43 -0
  157. package/skills/plan-to-pr/reference/packet-schema.json +57 -0
  158. package/skills/plan-to-pr/reference/process-inventory.md +22 -0
  159. package/skills/plan-to-pr/reference/review-loop.md +33 -0
  160. package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
  161. package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
  162. package/skills/plan-to-pr/reference/task-seeds.md +14 -0
  163. package/skills/plan-to-pr/reference/task-ticket.md +38 -0
  164. package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
  165. package/skills/plan-to-pr/scripts/config/constants.py +193 -0
  166. package/skills/plan-to-pr/scripts/create_packet.py +173 -0
  167. package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
  168. package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
  169. package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
  170. package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
  171. package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
  172. package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
  173. package/skills/plan-to-pr/scripts/validate_run.py +173 -0
  174. package/skills/plan-to-pr/test_skill_contract.py +207 -0
  175. package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
  176. package/skills/pr-converge/SKILL.md +472 -469
  177. package/skills/pr-converge/reference/examples.md +3 -3
  178. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  179. package/skills/pr-converge/reference/ground-rules.md +7 -4
  180. package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
  181. package/skills/pr-converge/reference/per-tick.md +5 -5
  182. package/skills/pr-converge/reference/progress-checklist.md +1 -1
  183. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
  184. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
  185. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
  186. package/skills/pr-converge/test_step5_host_branch.py +1 -1
  187. package/skills/pr-fix-protocol/SKILL.md +1 -1
  188. package/skills/privacy-hygiene/SKILL.md +68 -68
  189. package/skills/prototype/workflows/promotion.md +1 -1
  190. package/skills/release-notes-html/SKILL.md +164 -0
  191. package/skills/task-build/CLAUDE.md +8 -7
  192. package/skills/task-build/SKILL.md +16 -8
  193. package/skills/task-build/reference/tool-routing.md +19 -0
  194. package/scripts/test_invoke_code_review.py +0 -966
  195. package/skills/closeout/reference/issue-body-templates.md +0 -108
@@ -7,10 +7,10 @@ declared in the constants module, aggregates the results, and emits one deny
7
7
  decision when any hook denied (carrying every denying reason) or exits zero to
8
8
  allow.
9
9
 
10
- The per-hook coverage matrix:
11
- - Write -> 20 hooks from ALL_HOSTED_HOOK_ENTRIES that list Write
12
- - Edit -> 21 hooks from ALL_HOSTED_HOOK_ENTRIES that list Edit
13
- - MultiEdit -> 9 hooks from ALL_HOSTED_HOOK_ENTRIES that list MultiEdit
10
+ Each tool runs the ``ALL_HOSTED_HOOK_ENTRIES`` whose ``applicable_tool_names``
11
+ list that tool; that roster is the live source of the per-tool coverage, and the
12
+ dispatcher roster tests in ``test_pre_tool_use_dispatcher.py`` assert the counts,
13
+ rather than a hardcoded matrix here that would drift as hooks are added.
14
14
  """
15
15
 
16
16
  from __future__ import annotations
@@ -24,20 +24,18 @@ from collections.abc import Callable
24
24
  from dataclasses import dataclass, field
25
25
  from pathlib import Path
26
26
 
27
- _hooks_directory = str(Path(__file__).resolve().parent.parent)
28
- if _hooks_directory not in sys.path:
29
- sys.path.insert(0, _hooks_directory)
27
+ import _path_setup # noqa: F401
30
28
 
31
- from plain_language_blocker import ( # noqa: E402
29
+ from plain_language_blocker import (
32
30
  build_deny_payload as build_plain_language_deny_payload,
33
31
  )
34
- from plain_language_blocker import evaluate as evaluate_plain_language # noqa: E402
35
- from state_description_blocker import ( # noqa: E402
32
+ from plain_language_blocker import evaluate as evaluate_plain_language
33
+ from state_description_blocker import (
36
34
  build_deny_payload as build_state_description_deny_payload,
37
35
  )
38
- from state_description_blocker import evaluate as evaluate_state_description # noqa: E402
36
+ from state_description_blocker import evaluate as evaluate_state_description
39
37
 
40
- from hooks_constants.pre_tool_use_dispatcher_constants import ( # noqa: E402
38
+ from hooks_constants.pre_tool_use_dispatcher_constants import (
41
39
  ALL_HOSTED_HOOK_ENTRIES,
42
40
  ALLOW_DECISION,
43
41
  BLOCKING_CRASH_DENY_REASON,
@@ -49,7 +47,7 @@ from hooks_constants.pre_tool_use_dispatcher_constants import ( # noqa: E402
49
47
  STATE_DESCRIPTION_BLOCKER_MODULE_NAME,
50
48
  HostedHookEntry,
51
49
  )
52
- from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin # noqa: E402
50
+ from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
53
51
 
54
52
  NativeEvaluator = Callable[[dict[str, object]], str | None]
55
53
  DenyPayloadBuilder = Callable[[str], dict[str, object]]
@@ -70,6 +68,18 @@ class NativeHook:
70
68
  build_deny_payload: DenyPayloadBuilder
71
69
 
72
70
 
71
+ _native_hook_by_module_name: dict[str, NativeHook] = {
72
+ STATE_DESCRIPTION_BLOCKER_MODULE_NAME: NativeHook(
73
+ evaluate=evaluate_state_description,
74
+ build_deny_payload=build_state_description_deny_payload,
75
+ ),
76
+ PLAIN_LANGUAGE_BLOCKER_MODULE_NAME: NativeHook(
77
+ evaluate=evaluate_plain_language,
78
+ build_deny_payload=build_plain_language_deny_payload,
79
+ ),
80
+ }
81
+
82
+
73
83
  @dataclass
74
84
  class HostedHookResult:
75
85
  """Outcome of running one hosted hook inside the dispatcher process.
@@ -477,17 +487,7 @@ def _run_one_hosted_hook(
477
487
  The HostedHookResult for this hook's run.
478
488
  """
479
489
  if each_entry.native_module_name is not None:
480
- native_hook_by_module_name: dict[str, NativeHook] = {
481
- STATE_DESCRIPTION_BLOCKER_MODULE_NAME: NativeHook(
482
- evaluate=evaluate_state_description,
483
- build_deny_payload=build_state_description_deny_payload,
484
- ),
485
- PLAIN_LANGUAGE_BLOCKER_MODULE_NAME: NativeHook(
486
- evaluate=evaluate_plain_language,
487
- build_deny_payload=build_plain_language_deny_payload,
488
- ),
489
- }
490
- native_hook = native_hook_by_module_name[each_entry.native_module_name]
490
+ native_hook = _native_hook_by_module_name[each_entry.native_module_name]
491
491
  return run_native_hook(native_hook, payload_by_key, each_entry.is_blocking)
492
492
  script_path = _resolve_hook_script_path(each_entry.script_relative_path)
493
493
  return run_hosted_hook(script_path, payload_text, each_entry.is_blocking)
@@ -43,6 +43,12 @@ store_module = _load_module("code_review_stamp_store")
43
43
  deny_module = _load_module("code_review_gate_deny")
44
44
 
45
45
 
46
+ @pytest.fixture(autouse=True)
47
+ def enable_code_review_enforcement(monkeypatch: pytest.MonkeyPatch) -> None:
48
+ """Behavior tests exercise the gates with enforcement on."""
49
+ monkeypatch.setattr(gate_module, "CODE_REVIEW_ENFORCEMENT_ENABLED", True)
50
+
51
+
46
52
  def _run_git(repository_directory: Path, *git_arguments: str) -> None:
47
53
  subprocess.run(
48
54
  ["git", "-C", str(repository_directory), *git_arguments],
@@ -183,3 +189,11 @@ def test_build_deny_payload_shape() -> None:
183
189
  deny_payload = deny_module.build_code_review_deny_payload("blocked reason")
184
190
  assert deny_payload["hookSpecificOutput"]["permissionDecision"] == "deny"
185
191
  assert deny_payload["hookSpecificOutput"]["permissionDecisionReason"] == "blocked reason"
192
+
193
+
194
+ def test_enforcement_off_allows_pr_create_without_stamp(
195
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
196
+ ) -> None:
197
+ monkeypatch.setattr(gate_module, "CODE_REVIEW_ENFORCEMENT_ENABLED", False)
198
+ work_directory = _prepared_repo(monkeypatch, tmp_path)
199
+ assert gate_module.deny_reason_for_directory(str(work_directory)) is None
@@ -46,6 +46,12 @@ gate_module = _load_module("code_review_push_gate")
46
46
  store_module = _load_module("code_review_stamp_store")
47
47
 
48
48
 
49
+ @pytest.fixture(autouse=True)
50
+ def enable_code_review_enforcement(monkeypatch: pytest.MonkeyPatch) -> None:
51
+ """Behavior tests exercise the gates with enforcement on."""
52
+ monkeypatch.setattr(gate_module, "CODE_REVIEW_ENFORCEMENT_ENABLED", True)
53
+
54
+
49
55
  def _run_git(repository_directory: Path, *git_arguments: str) -> None:
50
56
  subprocess.run(
51
57
  ["git", "-C", str(repository_directory), *git_arguments],
@@ -187,3 +193,13 @@ def test_code_review_skip_marker_allows_push(
187
193
  work_directory = _prepared_code_repo(monkeypatch, tmp_path)
188
194
  emitted = _run_main(monkeypatch, f"git push origin main {BYPASS_MARKER}", work_directory)
189
195
  assert emitted == ""
196
+
197
+
198
+ def test_enforcement_off_allows_push_without_stamp(
199
+ monkeypatch: pytest.MonkeyPatch, tmp_path: Path
200
+ ) -> None:
201
+ monkeypatch.setattr(gate_module, "CODE_REVIEW_ENFORCEMENT_ENABLED", False)
202
+ work_directory = _prepared_code_repo(monkeypatch, tmp_path)
203
+ assert gate_module.deny_reason_for_directory(str(work_directory)) is None
204
+ emitted = _run_main(monkeypatch, "git push origin main", work_directory)
205
+ assert emitted == ""
@@ -53,6 +53,12 @@ def _load_module(module_name: str) -> ModuleType:
53
53
  blocker_module = _load_module("code_review_stamp_directory_write_blocker")
54
54
 
55
55
 
56
+ @pytest.fixture(autouse=True)
57
+ def enable_code_review_enforcement(monkeypatch: pytest.MonkeyPatch) -> None:
58
+ """Behavior tests exercise the write-blocker with enforcement on."""
59
+ monkeypatch.setattr(blocker_module, "CODE_REVIEW_ENFORCEMENT_ENABLED", True)
60
+
61
+
56
62
  def _isolate_home(monkeypatch: pytest.MonkeyPatch, fake_home: Path) -> None:
57
63
  home_text = str(fake_home)
58
64
  monkeypatch.setenv("HOME", home_text)
@@ -178,3 +184,16 @@ def test_chr_chain_obfuscated_stamp_path_is_blocked() -> None:
178
184
 
179
185
  def test_benign_decode_to_other_path_is_not_blocked() -> None:
180
186
  assert not blocker_module.references_stamp_directory(_hex_forge(BENIGN_OBFUSCATION_PATH))
187
+
188
+
189
+ def test_enforcement_off_allows_stamp_directory_access(
190
+ monkeypatch: pytest.MonkeyPatch,
191
+ ) -> None:
192
+ monkeypatch.setattr(blocker_module, "CODE_REVIEW_ENFORCEMENT_ENABLED", False)
193
+ decision = blocker_module.decision_for_payload(
194
+ {
195
+ "tool_name": "Bash",
196
+ "tool_input": {"command": "rm -rf ~/.claude/code-review-stamps"},
197
+ }
198
+ )
199
+ assert decision is None
@@ -0,0 +1,231 @@
1
+ """Tests for orchestrator_refresh_reschedule_gate PreToolUse hook."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import importlib.util
6
+ import io
7
+ import json
8
+ import sys
9
+ from pathlib import Path
10
+ from types import ModuleType
11
+
12
+ import pytest
13
+
14
+ BLOCKING_DIRECTORY = Path(__file__).resolve().parent
15
+ HOOKS_DIRECTORY = BLOCKING_DIRECTORY.parent
16
+ if str(HOOKS_DIRECTORY) not in sys.path:
17
+ sys.path.insert(0, str(HOOKS_DIRECTORY))
18
+
19
+ SKILL_SCRIPTS = HOOKS_DIRECTORY.parent / "skills" / "orchestrator" / "scripts"
20
+ if str(SKILL_SCRIPTS) not in sys.path:
21
+ sys.path.insert(0, str(SKILL_SCRIPTS))
22
+
23
+ from status_gate_constants.config.constants import RUN_STATUS_ACTIVE, RUN_STATUS_DONE # noqa: E402
24
+ from status_gate import write_status_file # noqa: E402
25
+
26
+
27
+ def load_gate_module() -> ModuleType:
28
+ module_path = BLOCKING_DIRECTORY / "orchestrator_refresh_reschedule_gate.py"
29
+ spec = importlib.util.spec_from_file_location(
30
+ "orchestrator_refresh_reschedule_gate",
31
+ module_path,
32
+ )
33
+ assert spec is not None
34
+ assert spec.loader is not None
35
+ gate_module = importlib.util.module_from_spec(spec)
36
+ sys.modules[spec.name] = gate_module
37
+ spec.loader.exec_module(gate_module)
38
+ return gate_module
39
+
40
+
41
+ def run_main_with_payload(
42
+ gate_module: ModuleType,
43
+ payload: dict[str, object],
44
+ monkeypatch: pytest.MonkeyPatch,
45
+ ) -> tuple[int, str]:
46
+ stdin_buffer = io.StringIO(json.dumps(payload))
47
+ stdout_buffer = io.StringIO()
48
+ monkeypatch.setattr(sys, "stdin", stdin_buffer)
49
+ monkeypatch.setattr(sys, "stdout", stdout_buffer)
50
+ exit_code = 0
51
+ try:
52
+ gate_module.main()
53
+ except SystemExit as system_exit:
54
+ exit_code = int(system_exit.code or 0)
55
+ return exit_code, stdout_buffer.getvalue()
56
+
57
+
58
+ class TestOrchestratorRefreshRescheduleGate:
59
+ def should_allow_when_status_is_active(
60
+ self,
61
+ temporary_directory: Path,
62
+ monkeypatch: pytest.MonkeyPatch,
63
+ ) -> None:
64
+ status_file_path = temporary_directory / "status.json"
65
+ write_status_file(status_file_path, RUN_STATUS_ACTIVE, "", is_rearm_pending=False)
66
+ monkeypatch.setenv("ORCHESTRATOR_RUN_STATUS_FILE", str(status_file_path))
67
+ gate_module = load_gate_module()
68
+ payload = {
69
+ "tool_name": "ScheduleWakeup",
70
+ "tool_input": {"prompt": "/orchestrator-refresh", "delaySeconds": 1200},
71
+ "cwd": str(temporary_directory),
72
+ }
73
+ exit_code, stdout_text = run_main_with_payload(
74
+ gate_module, payload, monkeypatch
75
+ )
76
+ assert exit_code == 0
77
+ assert stdout_text == ""
78
+
79
+ def should_deny_when_status_is_done(
80
+ self,
81
+ temporary_directory: Path,
82
+ monkeypatch: pytest.MonkeyPatch,
83
+ ) -> None:
84
+ status_file_path = temporary_directory / "status.json"
85
+ write_status_file(status_file_path, RUN_STATUS_DONE, "", is_rearm_pending=False)
86
+ monkeypatch.setenv("ORCHESTRATOR_RUN_STATUS_FILE", str(status_file_path))
87
+ gate_module = load_gate_module()
88
+ payload = {
89
+ "tool_name": "ScheduleWakeup",
90
+ "tool_input": {"prompt": "/orchestrator-refresh re-arm"},
91
+ "cwd": str(temporary_directory),
92
+ }
93
+ exit_code, stdout_text = run_main_with_payload(
94
+ gate_module, payload, monkeypatch
95
+ )
96
+ assert exit_code == 0
97
+ denial = json.loads(stdout_text)
98
+ assert denial["hookSpecificOutput"]["permissionDecision"] == "deny"
99
+
100
+ def should_deny_when_status_file_missing(
101
+ self,
102
+ temporary_directory: Path,
103
+ monkeypatch: pytest.MonkeyPatch,
104
+ ) -> None:
105
+ missing_path = temporary_directory / "nope.json"
106
+ monkeypatch.setenv("ORCHESTRATOR_RUN_STATUS_FILE", str(missing_path))
107
+ gate_module = load_gate_module()
108
+ payload = {
109
+ "tool_name": "ScheduleWakeup",
110
+ "tool_input": {"prompt": "/orchestrator-refresh now"},
111
+ "cwd": str(temporary_directory),
112
+ }
113
+ _exit_code, stdout_text = run_main_with_payload(
114
+ gate_module, payload, monkeypatch
115
+ )
116
+ denial = json.loads(stdout_text)
117
+ assert denial["hookSpecificOutput"]["permissionDecision"] == "deny"
118
+
119
+ def should_deny_when_rearm_slot_already_pending(
120
+ self,
121
+ temporary_directory: Path,
122
+ monkeypatch: pytest.MonkeyPatch,
123
+ ) -> None:
124
+ status_file_path = temporary_directory / "status.json"
125
+ write_status_file(
126
+ status_file_path, RUN_STATUS_ACTIVE, "", is_rearm_pending=True
127
+ )
128
+ monkeypatch.setenv("ORCHESTRATOR_RUN_STATUS_FILE", str(status_file_path))
129
+ gate_module = load_gate_module()
130
+ payload = {
131
+ "tool_name": "ScheduleWakeup",
132
+ "tool_input": {"prompt": "/orchestrator-refresh", "delaySeconds": 1200},
133
+ "cwd": str(temporary_directory),
134
+ }
135
+ _exit_code, stdout_text = run_main_with_payload(
136
+ gate_module, payload, monkeypatch
137
+ )
138
+ denial = json.loads(stdout_text)
139
+ assert denial["hookSpecificOutput"]["permissionDecision"] == "deny"
140
+ assert "rearm_already_pending" in denial["hookSpecificOutput"][
141
+ "permissionDecisionReason"
142
+ ]
143
+
144
+ def should_always_deny_cron_create_for_orchestrator_refresh(
145
+ self,
146
+ temporary_directory: Path,
147
+ monkeypatch: pytest.MonkeyPatch,
148
+ ) -> None:
149
+ status_file_path = temporary_directory / "status.json"
150
+ write_status_file(status_file_path, RUN_STATUS_ACTIVE, "", is_rearm_pending=False)
151
+ monkeypatch.setenv("ORCHESTRATOR_RUN_STATUS_FILE", str(status_file_path))
152
+ gate_module = load_gate_module()
153
+ payload = {
154
+ "tool_name": "CronCreate",
155
+ "tool_input": {"prompt": "/orchestrator-refresh"},
156
+ "cwd": str(temporary_directory),
157
+ }
158
+ _exit_code, stdout_text = run_main_with_payload(
159
+ gate_module, payload, monkeypatch
160
+ )
161
+ denial = json.loads(stdout_text)
162
+ assert denial["hookSpecificOutput"]["permissionDecision"] == "deny"
163
+ assert "cron_create_forbidden" in denial["hookSpecificOutput"][
164
+ "permissionDecisionReason"
165
+ ]
166
+
167
+ def should_ignore_unrelated_schedule_prompts(
168
+ self,
169
+ temporary_directory: Path,
170
+ monkeypatch: pytest.MonkeyPatch,
171
+ ) -> None:
172
+ gate_module = load_gate_module()
173
+ payload = {
174
+ "tool_name": "ScheduleWakeup",
175
+ "tool_input": {"prompt": "/pr-converge"},
176
+ "cwd": str(temporary_directory),
177
+ }
178
+ exit_code, stdout_text = run_main_with_payload(
179
+ gate_module, payload, monkeypatch
180
+ )
181
+ assert exit_code == 0
182
+ assert stdout_text == ""
183
+
184
+ def should_ignore_prose_mention_of_orchestrator_refresh(
185
+ self,
186
+ temporary_directory: Path,
187
+ monkeypatch: pytest.MonkeyPatch,
188
+ ) -> None:
189
+ gate_module = load_gate_module()
190
+ payload = {
191
+ "tool_name": "ScheduleWakeup",
192
+ "tool_input": {
193
+ "prompt": "See docs about /orchestrator-refresh for later"
194
+ },
195
+ "cwd": str(temporary_directory),
196
+ }
197
+ exit_code, stdout_text = run_main_with_payload(
198
+ gate_module, payload, monkeypatch
199
+ )
200
+ assert exit_code == 0
201
+ assert stdout_text == ""
202
+
203
+ def should_allow_when_prompt_carries_run_slug_for_scoped_status(
204
+ self,
205
+ temporary_directory: Path,
206
+ monkeypatch: pytest.MonkeyPatch,
207
+ ) -> None:
208
+ run_slug = "demo-run"
209
+ status_file_path = temporary_directory.joinpath(
210
+ "docs", "plans", run_slug, ".orchestrator-run-status.json"
211
+ )
212
+ write_status_file(status_file_path, RUN_STATUS_ACTIVE, run_slug, is_rearm_pending=False)
213
+ monkeypatch.delenv("ORCHESTRATOR_RUN_STATUS_FILE", raising=False)
214
+ gate_module = load_gate_module()
215
+ payload = {
216
+ "tool_name": "ScheduleWakeup",
217
+ "tool_input": {
218
+ "prompt": f"/orchestrator-refresh --run-slug {run_slug}"
219
+ },
220
+ "cwd": str(temporary_directory),
221
+ }
222
+ exit_code, stdout_text = run_main_with_payload(
223
+ gate_module, payload, monkeypatch
224
+ )
225
+ assert exit_code == 0
226
+ assert stdout_text == ""
227
+
228
+
229
+ @pytest.fixture
230
+ def temporary_directory(tmp_path: Path) -> Path:
231
+ return tmp_path
@@ -300,6 +300,15 @@ def test_plain_language_denial_on_multi_edit_of_markdown_file() -> None:
300
300
  _assert_dispatcher_matches_individual_hooks(payload_text, MULTI_EDIT_TOOL_NAME)
301
301
 
302
302
 
303
+ def test_dispatcher_docstring_points_at_roster_not_hardcoded_counts() -> None:
304
+ """The dispatcher docstring names the roster, not per-tool counts that drift."""
305
+ dispatcher_source = Path(_DISPATCHER_SCRIPT).read_text(encoding="utf-8")
306
+ assert "ALL_HOSTED_HOOK_ENTRIES" in dispatcher_source
307
+ assert "-> 20 hooks" not in dispatcher_source
308
+ assert "-> 21 hooks" not in dispatcher_source
309
+ assert "-> 9 hooks" not in dispatcher_source
310
+
311
+
303
312
  def test_multi_edit_runs_only_group_b_hooks() -> None:
304
313
  """Dispatcher invokes only Group-B hooks on MultiEdit, not Group-A hooks.
305
314
 
@@ -667,7 +676,7 @@ def test_proceed_after_run_all_validators_removal_allows() -> None:
667
676
  it was never a PreToolUse hook and never hosted by the PreToolUse dispatcher.
668
677
  A Python Write payload that run_all_validators would have flagged (mypy errors, for
669
678
  instance) still produces ALLOW from the PreToolUse dispatcher because the PreToolUse
670
- dispatcher covers only its 22 hosted blocking hooks — none of which includes the
679
+ dispatcher covers only its 23 hosted blocking hooks — none of which includes the
671
680
  validators runner.
672
681
  """
673
682
  python_content_with_type_error = (