claude-dev-env 2.3.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 (290) hide show
  1. package/CLAUDE.md +53 -48
  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 +3 -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 +3 -4
  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/skill-writer-agent.md +84 -0
  31. package/agents/test_agent_frontmatter.py +67 -18
  32. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +105 -3
  33. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
  34. package/bin/CLAUDE.md +68 -5
  35. package/bin/ever-shipped-skills.mjs +1 -0
  36. package/bin/install-constants.mjs +88 -0
  37. package/bin/install.mjs +1138 -114
  38. package/bin/install.prune.test.mjs +869 -19
  39. package/bin/install.test.mjs +906 -2
  40. package/commands/implement.md +1 -1
  41. package/commands/right-size.md +1 -1
  42. package/docs/CLAUDE.md +2 -0
  43. package/docs/host-pool-health-monitor.md +102 -0
  44. package/docs/references/CLAUDE.md +5 -2
  45. package/docs/references/advisor-tool.md +13 -0
  46. package/docs/references/code-review-enforcement.md +107 -0
  47. package/docs/references/team-advisor-skill.md +14 -0
  48. package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
  49. package/hooks/blocking/CLAUDE.md +9 -1
  50. package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
  51. package/hooks/blocking/code_review_gate_deny.py +74 -0
  52. package/hooks/blocking/code_review_pr_create_gate.py +198 -0
  53. package/hooks/blocking/code_review_push_gate.py +145 -0
  54. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +348 -0
  55. package/hooks/blocking/code_review_stamp_store.py +233 -0
  56. package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
  57. package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
  58. package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
  59. package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
  60. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
  61. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
  62. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
  63. package/hooks/blocking/config/__init__.py +5 -5
  64. package/hooks/blocking/config/code_review_enforcement_constants.py +113 -0
  65. package/hooks/blocking/config/test_code_review_enforcement_constants.py +113 -0
  66. package/hooks/blocking/config/verified_commit_constants.py +160 -155
  67. package/hooks/blocking/conftest.py +2 -0
  68. package/hooks/blocking/convergence_gate_blocker.py +112 -23
  69. package/hooks/blocking/destructive_command_blocker.py +19 -6
  70. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
  71. package/hooks/blocking/pr_description_proof_of_work.py +52 -34
  72. package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
  73. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
  74. package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
  75. package/hooks/blocking/test_code_review_gate_deny.py +54 -0
  76. package/hooks/blocking/test_code_review_pr_create_gate.py +199 -0
  77. package/hooks/blocking/test_code_review_push_gate.py +205 -0
  78. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +199 -0
  79. package/hooks/blocking/test_code_review_stamp_store.py +205 -0
  80. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
  81. package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
  82. package/hooks/blocking/test_destructive_command_blocker.py +1 -1
  83. package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
  84. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
  85. package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
  86. package/hooks/blocking/test_pre_tool_use_dispatcher.py +17 -8
  87. package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
  88. package/hooks/blocking/test_verification_verdict_store.py +974 -903
  89. package/hooks/blocking/test_verified_commit_gate.py +581 -581
  90. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
  91. package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
  92. package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
  93. package/hooks/blocking/verification_verdict_store.py +1039 -1014
  94. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
  95. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
  96. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
  97. package/hooks/blocking/verifier_verdict_minter.py +280 -280
  98. package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
  99. package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
  100. package/hooks/git-hooks/pre_push.py +89 -2
  101. package/hooks/git-hooks/test_pre_push.py +128 -0
  102. package/hooks/hooks.json +26 -1
  103. package/hooks/hooks_constants/CLAUDE.md +3 -1
  104. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
  105. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  106. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
  107. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
  108. package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
  109. package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
  110. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
  111. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
  112. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  113. package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
  114. package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
  115. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
  116. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
  117. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
  118. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
  119. package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
  120. package/hooks/pyproject.toml +1 -0
  121. package/hooks/validators/CLAUDE.md +2 -0
  122. package/hooks/validators/config/__init__.py +0 -0
  123. package/hooks/validators/config/directory_exemption_constants.py +183 -0
  124. package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
  125. package/hooks/validators/conftest.py +4 -0
  126. package/hooks/validators/mypy_integration.py +63 -50
  127. package/hooks/validators/pyproject_config_discovery.py +101 -0
  128. package/hooks/validators/ruff_integration.py +257 -25
  129. package/hooks/validators/run_all_validators.py +223 -19
  130. package/hooks/validators/test_directory_exemption_constants.py +185 -0
  131. package/hooks/validators/test_mypy_integration.py +32 -0
  132. package/hooks/validators/test_pyproject_config_discovery.py +94 -0
  133. package/hooks/validators/test_python_antipattern_checks.py +110 -5
  134. package/hooks/validators/test_ruff_integration.py +160 -2
  135. package/hooks/validators/test_run_all_validators.py +140 -68
  136. package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
  137. package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
  138. package/package.json +10 -2
  139. package/rules/CLAUDE.md +1 -0
  140. package/rules/docstring-prose-matches-implementation.md +45 -67
  141. package/rules/durable-post-artifacts.md +7 -0
  142. package/rules/state-what-is.md +25 -0
  143. package/rules/verified-commit-gate-skip.md +1 -1
  144. package/scripts/CLAUDE.md +1 -0
  145. package/scripts/Capture-PoolHealth.ps1 +410 -0
  146. package/scripts/_code_review_test_support.py +404 -0
  147. package/scripts/claude_chain_runner.py +141 -1
  148. package/scripts/codec_forwarding_test_support.py +83 -0
  149. package/scripts/conftest.py +23 -0
  150. package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -2
  151. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +53 -1
  152. package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
  153. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
  154. package/scripts/invoke_code_review.py +550 -38
  155. package/scripts/resolve_worker_spawn.py +626 -619
  156. package/scripts/spawn_grok_batch.py +672 -672
  157. package/scripts/test_claude_chain_runner.py +131 -0
  158. package/scripts/test_invoke_code_review_chain.py +70 -0
  159. package/scripts/test_invoke_code_review_cli.py +192 -0
  160. package/scripts/test_invoke_code_review_codec.py +77 -0
  161. package/scripts/test_invoke_code_review_contract.py +256 -0
  162. package/scripts/test_invoke_code_review_git.py +123 -0
  163. package/scripts/test_invoke_code_review_mode.py +99 -0
  164. package/scripts/test_resolve_worker_spawn.py +1014 -1014
  165. package/scripts/test_resolve_worker_spawn_codec.py +101 -0
  166. package/skills/CLAUDE.md +2 -0
  167. package/skills/auditing-claude-config/SKILL.md +114 -114
  168. package/skills/autoconverge/SKILL.md +427 -421
  169. package/skills/autoconverge/reference/convergence.md +26 -4
  170. package/skills/autoconverge/reference/multi-pr.md +6 -1
  171. package/skills/autoconverge/reference/stop-conditions.md +16 -10
  172. package/skills/autoconverge/workflow/CLAUDE.md +1 -0
  173. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
  174. package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
  175. package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1244
  176. package/skills/autoconverge/workflow/converge.mjs +191 -8
  177. package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
  178. package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
  179. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
  180. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
  181. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
  182. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
  183. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
  184. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
  185. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
  186. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
  187. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
  188. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
  189. package/skills/closeout/SKILL.md +33 -50
  190. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
  191. package/skills/codex-review/scripts/run_codex_review.py +233 -1
  192. package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
  193. package/skills/condensing-instructions/SKILL.md +81 -0
  194. package/skills/copilot-review/SKILL.md +119 -119
  195. package/skills/e-code-review/SKILL.md +52 -0
  196. package/skills/e-code-review/reference/fix.md +54 -0
  197. package/skills/e-code-review/reference/loop.md +43 -0
  198. package/skills/e-code-review/reference/low.md +57 -0
  199. package/skills/e-code-review/reference/medium.md +153 -0
  200. package/skills/e-code-review/reference/xhigh.md +182 -0
  201. package/skills/e-simplify/SKILL.md +97 -0
  202. package/skills/fresh-branch/CLAUDE.md +2 -0
  203. package/skills/fresh-branch/SKILL.md +2 -0
  204. package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
  205. package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
  206. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
  207. package/skills/fresh-branch/scripts/pytest.ini +4 -0
  208. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
  209. package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
  210. package/skills/issue-tracker/SKILL.md +92 -0
  211. package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
  212. package/skills/issue-tracker/reference/handoff-schema.md +64 -0
  213. package/skills/issue-tracker/reference/operation-matrix.md +41 -0
  214. package/skills/orchestrator/SKILL.md +162 -21
  215. package/skills/orchestrator/scripts/status_gate.py +625 -0
  216. package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
  217. package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
  218. package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
  219. package/skills/orchestrator/scripts/test_status_gate.py +439 -0
  220. package/skills/orchestrator-refresh/SKILL.md +110 -35
  221. package/skills/plan-to-pr/SKILL.md +155 -0
  222. package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
  223. package/skills/plan-to-pr/reference/model-routing.md +36 -0
  224. package/skills/plan-to-pr/reference/packet-contract.md +43 -0
  225. package/skills/plan-to-pr/reference/packet-schema.json +57 -0
  226. package/skills/plan-to-pr/reference/process-inventory.md +22 -0
  227. package/skills/plan-to-pr/reference/review-loop.md +33 -0
  228. package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
  229. package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
  230. package/skills/plan-to-pr/reference/task-seeds.md +14 -0
  231. package/skills/plan-to-pr/reference/task-ticket.md +38 -0
  232. package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
  233. package/skills/plan-to-pr/scripts/config/constants.py +193 -0
  234. package/skills/plan-to-pr/scripts/create_packet.py +173 -0
  235. package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
  236. package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
  237. package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
  238. package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
  239. package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
  240. package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
  241. package/skills/plan-to-pr/scripts/validate_run.py +173 -0
  242. package/skills/plan-to-pr/test_skill_contract.py +207 -0
  243. package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
  244. package/skills/pr-converge/SKILL.md +472 -469
  245. package/skills/pr-converge/reference/examples.md +3 -3
  246. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  247. package/skills/pr-converge/reference/ground-rules.md +7 -4
  248. package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
  249. package/skills/pr-converge/reference/per-tick.md +5 -5
  250. package/skills/pr-converge/reference/progress-checklist.md +1 -1
  251. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
  252. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
  253. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
  254. package/skills/pr-converge/test_step5_host_branch.py +1 -1
  255. package/skills/pr-fix-protocol/SKILL.md +1 -1
  256. package/skills/privacy-hygiene/SKILL.md +68 -68
  257. package/skills/prototype/SKILL.md +86 -0
  258. package/skills/prototype/reference/honest-limitations.md +23 -0
  259. package/skills/prototype/reference/promotion-tasks.md +23 -0
  260. package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
  261. package/skills/prototype/scripts/conftest.py +15 -0
  262. package/skills/prototype/scripts/launch_sandbox.py +205 -0
  263. package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
  264. package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
  265. package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
  266. package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
  267. package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
  268. package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
  269. package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
  270. package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
  271. package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
  272. package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
  273. package/skills/prototype/workflows/promotion.md +27 -0
  274. package/skills/prototype/workflows/sandbox.md +35 -0
  275. package/skills/release-notes-html/SKILL.md +164 -0
  276. package/skills/skill-builder/CLAUDE.md +3 -3
  277. package/skills/skill-builder/SKILL.md +5 -5
  278. package/skills/skill-builder/references/CLAUDE.md +1 -1
  279. package/skills/skill-builder/references/delegation-map.md +3 -3
  280. package/skills/skill-builder/references/description-field.md +1 -1
  281. package/skills/skill-builder/references/skill-modularity.md +2 -3
  282. package/skills/skill-builder/workflows/CLAUDE.md +1 -1
  283. package/skills/skill-builder/workflows/improve-skill.md +1 -1
  284. package/skills/skill-builder/workflows/new-skill.md +2 -2
  285. package/skills/task-build/CLAUDE.md +8 -7
  286. package/skills/task-build/SKILL.md +16 -8
  287. package/skills/task-build/reference/tool-routing.md +19 -0
  288. package/skills/team-advisor/SKILL.md +2 -2
  289. package/scripts/test_invoke_code_review.py +0 -672
  290. package/skills/closeout/reference/issue-body-templates.md +0 -108
@@ -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
@@ -17,6 +17,7 @@ _HOOKS_ROOT = _HOOK_DIR.parent
17
17
  if str(_HOOKS_ROOT) not in sys.path:
18
18
  sys.path.insert(0, str(_HOOKS_ROOT))
19
19
 
20
+ from blocking import convergence_gate_blocker as convergence_module # noqa: E402
20
21
  from blocking import pr_description_proof_of_work as proof_module # noqa: E402
21
22
  from blocking.pr_description_proof_of_work import ( # noqa: E402
22
23
  audit_proof_comment_body,
@@ -70,6 +71,34 @@ def _install_fake_gh(monkeypatch, all_comment_bodies, all_diff_names, diff_text)
70
71
  monkeypatch.setattr(proof_module, "_run_gh_command", _fake_run_gh_command)
71
72
 
72
73
 
74
+ def _install_recording_fake_gh(monkeypatch, all_comment_bodies, all_diff_names, diff_text):
75
+ all_comment_records = [{"body": each_body} for each_body in all_comment_bodies]
76
+ all_recorded_argument_lists: list[list[str]] = []
77
+
78
+ def _fake_run_gh_command(all_gh_arguments):
79
+ all_recorded_argument_lists.append(list(all_gh_arguments))
80
+ if all_gh_arguments and all_gh_arguments[0] == "api":
81
+ return json.dumps([all_comment_records])
82
+ if list(all_gh_arguments[:2]) == ["pr", "view"]:
83
+ return json.dumps({"number": 77})
84
+ if "--name-only" in all_gh_arguments:
85
+ return "\n".join(all_diff_names)
86
+ if list(all_gh_arguments[:2]) == ["pr", "diff"]:
87
+ return diff_text
88
+ return None
89
+
90
+ monkeypatch.setattr(proof_module, "_run_gh_command", _fake_run_gh_command)
91
+ return all_recorded_argument_lists
92
+
93
+
94
+ def _argument_lists_with_leading(all_recorded_argument_lists, first_token, second_token):
95
+ return [
96
+ each_arguments
97
+ for each_arguments in all_recorded_argument_lists
98
+ if list(each_arguments[:2]) == [first_token, second_token]
99
+ ]
100
+
101
+
73
102
  def test_is_proof_shaped_body_detects_verification_heading() -> None:
74
103
  assert is_proof_shaped_body(PASSING_PROOF_COMMENT_BODY)
75
104
 
@@ -159,4 +188,126 @@ def test_ready_gate_resolves_pr_number_from_current_branch(
159
188
  monkeypatch: pytest.MonkeyPatch,
160
189
  ) -> None:
161
190
  _install_fake_gh(monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], "")
191
+ monkeypatch.setattr(
192
+ convergence_module,
193
+ "_resolve_current_branch_pr_number",
194
+ lambda all_target_repo, cwd: 77,
195
+ )
162
196
  assert evaluate_pr_ready_gate("gh pr ready") is None
197
+
198
+
199
+ def test_ready_gate_reads_comments_from_repo_named_by_flag(
200
+ monkeypatch: pytest.MonkeyPatch,
201
+ ) -> None:
202
+ all_recorded = _install_recording_fake_gh(
203
+ monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], ""
204
+ )
205
+ evaluate_pr_ready_gate("gh pr ready 42 --repo other-owner/other-repo")
206
+ all_api_calls = _argument_lists_with_leading(
207
+ all_recorded, "api", "repos/other-owner/other-repo/issues/42/comments"
208
+ )
209
+ assert all_api_calls
210
+ for each_arguments in all_api_calls:
211
+ assert "{owner}" not in each_arguments[1]
212
+ assert "{repo}" not in each_arguments[1]
213
+
214
+
215
+ def test_ready_gate_resolves_pr_number_from_repo_named_by_flag(
216
+ monkeypatch: pytest.MonkeyPatch,
217
+ ) -> None:
218
+ _install_fake_gh(monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], "")
219
+ all_captured_repo_pairs: list[tuple[str, str] | None] = []
220
+
221
+ def fake_resolver(all_target_repo: tuple[str, str] | None, cwd: str | None) -> int:
222
+ all_captured_repo_pairs.append(all_target_repo)
223
+ return 77
224
+
225
+ monkeypatch.setattr(convergence_module, "_resolve_current_branch_pr_number", fake_resolver)
226
+ evaluate_pr_ready_gate("gh pr ready --repo other-owner/other-repo")
227
+ assert all_captured_repo_pairs == [("other-owner", "other-repo")]
228
+
229
+
230
+ def test_ready_gate_reads_comments_from_repo_named_by_url(
231
+ monkeypatch: pytest.MonkeyPatch,
232
+ ) -> None:
233
+ all_recorded = _install_recording_fake_gh(
234
+ monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], ""
235
+ )
236
+ evaluate_pr_ready_gate("gh pr ready https://github.com/url-owner/url-repo/pull/99")
237
+ all_api_calls = _argument_lists_with_leading(
238
+ all_recorded, "api", "repos/url-owner/url-repo/issues/99/comments"
239
+ )
240
+ assert all_api_calls
241
+ all_view_calls = _argument_lists_with_leading(all_recorded, "pr", "view")
242
+ assert not all_view_calls
243
+
244
+
245
+ def test_ready_gate_leaves_bare_command_bound_to_current_directory(
246
+ monkeypatch: pytest.MonkeyPatch,
247
+ ) -> None:
248
+ all_recorded = _install_recording_fake_gh(
249
+ monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], ""
250
+ )
251
+ evaluate_pr_ready_gate("gh pr ready 123")
252
+ all_api_calls = _argument_lists_with_leading(
253
+ all_recorded, "api", "repos/{owner}/{repo}/issues/123/comments"
254
+ )
255
+ assert all_api_calls
256
+ for each_arguments in all_recorded:
257
+ assert "--repo" not in each_arguments
258
+
259
+
260
+ def test_ready_gate_ignores_repo_flag_of_chained_command(
261
+ monkeypatch: pytest.MonkeyPatch,
262
+ ) -> None:
263
+ all_recorded = _install_recording_fake_gh(
264
+ monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], ""
265
+ )
266
+ evaluate_pr_ready_gate(
267
+ "gh pr ready 161 && gh pr comment 999 --repo other-owner/other-repo"
268
+ )
269
+ all_api_calls = _argument_lists_with_leading(
270
+ all_recorded, "api", "repos/{owner}/{repo}/issues/161/comments"
271
+ )
272
+ assert all_api_calls
273
+ for each_arguments in all_recorded:
274
+ assert "other-owner/other-repo" not in each_arguments
275
+
276
+
277
+ def test_ready_gate_reads_comments_from_repo_named_by_full_url_flag(
278
+ monkeypatch: pytest.MonkeyPatch,
279
+ ) -> None:
280
+ all_recorded = _install_recording_fake_gh(
281
+ monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], ""
282
+ )
283
+ evaluate_pr_ready_gate("gh pr ready 42 --repo https://github.com/other-owner/other-repo")
284
+ all_api_calls = _argument_lists_with_leading(
285
+ all_recorded, "api", "repos/other-owner/other-repo/issues/42/comments"
286
+ )
287
+ assert all_api_calls
288
+
289
+
290
+ def test_ready_gate_reads_comments_from_repo_named_by_short_url_flag(
291
+ monkeypatch: pytest.MonkeyPatch,
292
+ ) -> None:
293
+ all_recorded = _install_recording_fake_gh(
294
+ monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], ""
295
+ )
296
+ evaluate_pr_ready_gate("gh pr ready 42 -R https://github.com/other-owner/other-repo")
297
+ all_api_calls = _argument_lists_with_leading(
298
+ all_recorded, "api", "repos/other-owner/other-repo/issues/42/comments"
299
+ )
300
+ assert all_api_calls
301
+
302
+
303
+ def test_ready_gate_reads_comments_from_repo_named_by_ssh_flag(
304
+ monkeypatch: pytest.MonkeyPatch,
305
+ ) -> None:
306
+ all_recorded = _install_recording_fake_gh(
307
+ monkeypatch, [PASSING_PROOF_COMMENT_BODY], ["src/parser.py"], ""
308
+ )
309
+ evaluate_pr_ready_gate("gh pr ready 42 --repo git@github.com:other-owner/other-repo")
310
+ all_api_calls = _argument_lists_with_leading(
311
+ all_recorded, "api", "repos/other-owner/other-repo/issues/42/comments"
312
+ )
313
+ assert all_api_calls
@@ -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
 
@@ -633,8 +642,8 @@ def test_dispatcher_write_applies_both_groups() -> None:
633
642
  assert "blocking/plain_language_blocker.py" in all_write_script_paths, (
634
643
  "plain_language_blocker (Group B) must be in Write applicable set"
635
644
  )
636
- assert len(all_write_entries) == 20, (
637
- f"Write tool must apply to all 20 hosted hooks, got {len(all_write_entries)}"
645
+ assert len(all_write_entries) == 21, (
646
+ f"Write tool must apply to all 21 hosted hooks, got {len(all_write_entries)}"
638
647
  )
639
648
 
640
649
 
@@ -647,16 +656,16 @@ def test_dispatcher_edit_applies_both_groups() -> None:
647
656
  assert "blocking/stale_comment_reference_blocker.py" in all_edit_script_paths, (
648
657
  "stale_comment_reference_blocker belongs in the Edit applicable set"
649
658
  )
650
- assert len(all_edit_entries) == 21, (
651
- f"expected 21 Edit entries, got {len(all_edit_entries)}"
659
+ assert len(all_edit_entries) == 22, (
660
+ f"expected 22 Edit entries, got {len(all_edit_entries)}"
652
661
  )
653
662
 
654
663
 
655
664
  def test_dispatcher_multi_edit_applies_only_group_b() -> None:
656
- """MultiEdit tool triggers only Group B (9 hooks), not Group A."""
665
+ """MultiEdit tool triggers only Group B (10 hooks), not Group A."""
657
666
  all_multi_edit_entries = _applicable_entries_for_tool(MULTI_EDIT_TOOL_NAME)
658
- assert len(all_multi_edit_entries) == 9, (
659
- f"MultiEdit tool must apply to exactly 9 Group-B hooks, got {len(all_multi_edit_entries)}"
667
+ assert len(all_multi_edit_entries) == 10, (
668
+ f"MultiEdit tool must apply to exactly 10 Group-B hooks, got {len(all_multi_edit_entries)}"
660
669
  )
661
670
 
662
671
 
@@ -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 21 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 = (