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.
- package/CLAUDE.md +53 -48
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +4 -2
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
- package/_shared/pr-loop/worker-spawn.md +1 -1
- package/agents/CLAUDE.md +3 -1
- package/agents/caveman.md +0 -1
- package/agents/clasp-deployment-orchestrator.md +0 -1
- package/agents/clean-coder.md +0 -1
- package/agents/code-advisor.md +0 -1
- package/agents/code-quality-agent.md +1 -2
- package/agents/code-verifier.md +3 -4
- package/agents/deep-research.md +0 -1
- package/agents/docs-agent.md +0 -1
- package/agents/git-commit-crafter.md +0 -1
- package/agents/issue-tracker.md +42 -0
- package/agents/plan-packet-validator.md +0 -1
- package/agents/pr-description-writer.md +0 -1
- package/agents/skill-writer-agent.md +84 -0
- package/agents/test_agent_frontmatter.py +67 -18
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +105 -3
- package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +29 -13
- package/bin/CLAUDE.md +68 -5
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install-constants.mjs +88 -0
- package/bin/install.mjs +1138 -114
- package/bin/install.prune.test.mjs +869 -19
- package/bin/install.test.mjs +906 -2
- package/commands/implement.md +1 -1
- package/commands/right-size.md +1 -1
- package/docs/CLAUDE.md +2 -0
- package/docs/host-pool-health-monitor.md +102 -0
- package/docs/references/CLAUDE.md +5 -2
- package/docs/references/advisor-tool.md +13 -0
- package/docs/references/code-review-enforcement.md +107 -0
- package/docs/references/team-advisor-skill.md +14 -0
- package/docs/wsl-docker-cowork-starter-matrix.md +89 -0
- package/hooks/blocking/CLAUDE.md +9 -1
- package/hooks/blocking/code_review_enforcement_config_bootstrap.py +53 -0
- package/hooks/blocking/code_review_gate_deny.py +74 -0
- package/hooks/blocking/code_review_pr_create_gate.py +198 -0
- package/hooks/blocking/code_review_push_gate.py +145 -0
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +348 -0
- package/hooks/blocking/code_review_stamp_store.py +233 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +7 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +15 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +212 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +138 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +49 -0
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +38 -0
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +39 -27
- package/hooks/blocking/config/__init__.py +5 -5
- package/hooks/blocking/config/code_review_enforcement_constants.py +113 -0
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +113 -0
- package/hooks/blocking/config/verified_commit_constants.py +160 -155
- package/hooks/blocking/conftest.py +2 -0
- package/hooks/blocking/convergence_gate_blocker.py +112 -23
- package/hooks/blocking/destructive_command_blocker.py +19 -6
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
- package/hooks/blocking/pr_description_proof_of_work.py +52 -34
- package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +4 -1
- package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +62 -0
- package/hooks/blocking/test_code_review_gate_deny.py +54 -0
- package/hooks/blocking/test_code_review_pr_create_gate.py +199 -0
- package/hooks/blocking/test_code_review_push_gate.py +205 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +199 -0
- package/hooks/blocking/test_code_review_stamp_store.py +205 -0
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +124 -2
- package/hooks/blocking/test_convergence_gate_blocker.py +153 -5
- package/hooks/blocking/test_destructive_command_blocker.py +1 -1
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +45 -0
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
- package/hooks/blocking/test_pr_description_proof_of_work.py +151 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +17 -8
- package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
- package/hooks/blocking/test_verification_verdict_store.py +974 -903
- package/hooks/blocking/test_verified_commit_gate.py +581 -581
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +114 -2
- package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
- package/hooks/blocking/verification_verdict_store.py +1039 -1014
- package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +29 -17
- package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +35 -0
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
- package/hooks/blocking/verifier_verdict_minter.py +280 -280
- package/hooks/blocking/volatile_path_in_post_blocker.py +69 -8
- package/hooks/git-hooks/git_hooks_constants/__init__.py +6 -0
- package/hooks/git-hooks/pre_push.py +89 -2
- package/hooks/git-hooks/test_pre_push.py +128 -0
- package/hooks/hooks.json +26 -1
- package/hooks/hooks_constants/CLAUDE.md +3 -1
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +8 -0
- package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +26 -11
- package/hooks/hooks_constants/convergence_gate_blocker_constants.py +20 -3
- package/hooks/hooks_constants/destructive_command_segment_constants.py +3 -1
- package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
- package/hooks/hooks_constants/pyproject_config_discovery_constants.py +16 -0
- package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +24 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +6 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +8 -1
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
- package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
- package/hooks/pyproject.toml +1 -0
- package/hooks/validators/CLAUDE.md +2 -0
- package/hooks/validators/config/__init__.py +0 -0
- package/hooks/validators/config/directory_exemption_constants.py +183 -0
- package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
- package/hooks/validators/conftest.py +4 -0
- package/hooks/validators/mypy_integration.py +63 -50
- package/hooks/validators/pyproject_config_discovery.py +101 -0
- package/hooks/validators/ruff_integration.py +257 -25
- package/hooks/validators/run_all_validators.py +223 -19
- package/hooks/validators/test_directory_exemption_constants.py +185 -0
- package/hooks/validators/test_mypy_integration.py +32 -0
- package/hooks/validators/test_pyproject_config_discovery.py +94 -0
- package/hooks/validators/test_python_antipattern_checks.py +110 -5
- package/hooks/validators/test_ruff_integration.py +160 -2
- package/hooks/validators/test_run_all_validators.py +140 -68
- package/hooks/validators/test_run_all_validators_config_discovery.py +123 -0
- package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
- package/package.json +10 -2
- package/rules/CLAUDE.md +1 -0
- package/rules/docstring-prose-matches-implementation.md +45 -67
- package/rules/durable-post-artifacts.md +7 -0
- package/rules/state-what-is.md +25 -0
- package/rules/verified-commit-gate-skip.md +1 -1
- package/scripts/CLAUDE.md +1 -0
- package/scripts/Capture-PoolHealth.ps1 +410 -0
- package/scripts/_code_review_test_support.py +404 -0
- package/scripts/claude_chain_runner.py +141 -1
- package/scripts/codec_forwarding_test_support.py +83 -0
- package/scripts/conftest.py +23 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -2
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +53 -1
- package/scripts/dev_env_scripts_constants/code_review_constants.py +129 -12
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +55 -0
- package/scripts/invoke_code_review.py +550 -38
- package/scripts/resolve_worker_spawn.py +626 -619
- package/scripts/spawn_grok_batch.py +672 -672
- package/scripts/test_claude_chain_runner.py +131 -0
- package/scripts/test_invoke_code_review_chain.py +70 -0
- package/scripts/test_invoke_code_review_cli.py +192 -0
- package/scripts/test_invoke_code_review_codec.py +77 -0
- package/scripts/test_invoke_code_review_contract.py +256 -0
- package/scripts/test_invoke_code_review_git.py +123 -0
- package/scripts/test_invoke_code_review_mode.py +99 -0
- package/scripts/test_resolve_worker_spawn.py +1014 -1014
- package/scripts/test_resolve_worker_spawn_codec.py +101 -0
- package/skills/CLAUDE.md +2 -0
- package/skills/auditing-claude-config/SKILL.md +114 -114
- package/skills/autoconverge/SKILL.md +427 -421
- package/skills/autoconverge/reference/convergence.md +26 -4
- package/skills/autoconverge/reference/multi-pr.md +6 -1
- package/skills/autoconverge/reference/stop-conditions.md +16 -10
- package/skills/autoconverge/workflow/CLAUDE.md +1 -0
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +4 -4
- package/skills/autoconverge/workflow/converge.codex-gate.test.mjs +175 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1244
- package/skills/autoconverge/workflow/converge.mjs +191 -8
- package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
- package/skills/autoconverge/workflow/converge_multi.mjs +7 -3
- package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +5 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
- package/skills/closeout/SKILL.md +33 -50
- package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
- package/skills/codex-review/scripts/run_codex_review.py +233 -1
- package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
- package/skills/condensing-instructions/SKILL.md +81 -0
- package/skills/copilot-review/SKILL.md +119 -119
- package/skills/e-code-review/SKILL.md +52 -0
- package/skills/e-code-review/reference/fix.md +54 -0
- package/skills/e-code-review/reference/loop.md +43 -0
- package/skills/e-code-review/reference/low.md +57 -0
- package/skills/e-code-review/reference/medium.md +153 -0
- package/skills/e-code-review/reference/xhigh.md +182 -0
- package/skills/e-simplify/SKILL.md +97 -0
- package/skills/fresh-branch/CLAUDE.md +2 -0
- package/skills/fresh-branch/SKILL.md +2 -0
- package/skills/fresh-branch/scripts/create_fresh_branch.py +78 -180
- package/skills/fresh-branch/scripts/fresh_branch_git_commands.py +285 -0
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -0
- package/skills/fresh-branch/scripts/pytest.ini +4 -0
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +98 -0
- package/skills/fresh-branch/scripts/test_fresh_branch_git_commands.py +310 -0
- package/skills/issue-tracker/SKILL.md +92 -0
- package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
- package/skills/issue-tracker/reference/handoff-schema.md +64 -0
- package/skills/issue-tracker/reference/operation-matrix.md +41 -0
- package/skills/orchestrator/SKILL.md +162 -21
- package/skills/orchestrator/scripts/status_gate.py +625 -0
- package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
- package/skills/orchestrator/scripts/test_status_gate.py +439 -0
- package/skills/orchestrator-refresh/SKILL.md +110 -35
- package/skills/plan-to-pr/SKILL.md +155 -0
- package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
- package/skills/plan-to-pr/reference/model-routing.md +36 -0
- package/skills/plan-to-pr/reference/packet-contract.md +43 -0
- package/skills/plan-to-pr/reference/packet-schema.json +57 -0
- package/skills/plan-to-pr/reference/process-inventory.md +22 -0
- package/skills/plan-to-pr/reference/review-loop.md +33 -0
- package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
- package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
- package/skills/plan-to-pr/reference/task-seeds.md +14 -0
- package/skills/plan-to-pr/reference/task-ticket.md +38 -0
- package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
- package/skills/plan-to-pr/scripts/config/constants.py +193 -0
- package/skills/plan-to-pr/scripts/create_packet.py +173 -0
- package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
- package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
- package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
- package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
- package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
- package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
- package/skills/plan-to-pr/scripts/validate_run.py +173 -0
- package/skills/plan-to-pr/test_skill_contract.py +207 -0
- package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
- package/skills/pr-converge/SKILL.md +472 -469
- package/skills/pr-converge/reference/examples.md +3 -3
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +7 -4
- package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
- package/skills/pr-converge/reference/per-tick.md +5 -5
- package/skills/pr-converge/reference/progress-checklist.md +1 -1
- package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
- package/skills/pr-converge/test_step5_host_branch.py +1 -1
- package/skills/pr-fix-protocol/SKILL.md +1 -1
- package/skills/privacy-hygiene/SKILL.md +68 -68
- package/skills/prototype/SKILL.md +86 -0
- package/skills/prototype/reference/honest-limitations.md +23 -0
- package/skills/prototype/reference/promotion-tasks.md +23 -0
- package/skills/prototype/scripts/build_sandbox_settings.py +249 -0
- package/skills/prototype/scripts/conftest.py +15 -0
- package/skills/prototype/scripts/launch_sandbox.py +205 -0
- package/skills/prototype/scripts/probe_sandbox_safety.py +311 -0
- package/skills/prototype/scripts/prototype_scripts_constants/__init__.py +1 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +41 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +23 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +45 -0
- package/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +10 -0
- package/skills/prototype/scripts/test_build_sandbox_settings.py +275 -0
- package/skills/prototype/scripts/test_launch_sandbox.py +303 -0
- package/skills/prototype/scripts/test_probe_sandbox_safety.py +284 -0
- package/skills/prototype/workflows/promotion.md +27 -0
- package/skills/prototype/workflows/sandbox.md +35 -0
- package/skills/release-notes-html/SKILL.md +164 -0
- package/skills/skill-builder/CLAUDE.md +3 -3
- package/skills/skill-builder/SKILL.md +5 -5
- package/skills/skill-builder/references/CLAUDE.md +1 -1
- package/skills/skill-builder/references/delegation-map.md +3 -3
- package/skills/skill-builder/references/description-field.md +1 -1
- package/skills/skill-builder/references/skill-modularity.md +2 -3
- package/skills/skill-builder/workflows/CLAUDE.md +1 -1
- package/skills/skill-builder/workflows/improve-skill.md +1 -1
- package/skills/skill-builder/workflows/new-skill.md +2 -2
- package/skills/task-build/CLAUDE.md +8 -7
- package/skills/task-build/SKILL.md +16 -8
- package/skills/task-build/reference/tool-routing.md +19 -0
- package/skills/team-advisor/SKILL.md +2 -2
- package/scripts/test_invoke_code_review.py +0 -672
- 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) ==
|
|
637
|
-
f"Write tool must apply to all
|
|
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) ==
|
|
651
|
-
f"expected
|
|
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 (
|
|
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) ==
|
|
659
|
-
f"MultiEdit tool must apply to exactly
|
|
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
|
|
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 = (
|