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.
- package/CLAUDE.md +53 -49
- 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 +2 -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 +0 -1
- 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/test_agent_frontmatter.py +67 -18
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +143 -141
- 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 +1 -0
- package/docs/host-pool-health-monitor.md +102 -0
- package/docs/references/CLAUDE.md +4 -2
- package/docs/references/advisor-tool.md +13 -0
- package/docs/references/code-review-enforcement.md +10 -0
- package/docs/references/team-advisor-skill.md +14 -0
- package/hooks/blocking/CLAUDE.md +1 -0
- package/hooks/blocking/code_review_pr_create_gate.py +7 -3
- package/hooks/blocking/code_review_push_gate.py +9 -4
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +8 -0
- package/hooks/blocking/config/__init__.py +5 -5
- package/hooks/blocking/config/code_review_enforcement_constants.py +4 -1
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +5 -0
- package/hooks/blocking/config/verified_commit_constants.py +160 -159
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
- package/hooks/blocking/test_code_review_pr_create_gate.py +14 -0
- package/hooks/blocking/test_code_review_push_gate.py +16 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +19 -0
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
- package/hooks/blocking/test_verdict_directory_write_blocker.py +808 -808
- package/hooks/blocking/test_verification_verdict_store.py +54 -0
- 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/verdict_directory_write_blocker.py +687 -687
- package/hooks/blocking/verification_verdict_store.py +1039 -1036
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
- package/hooks/blocking/verifier_verdict_minter.py +280 -280
- package/hooks/git-hooks/test_pre_push.py +25 -0
- package/hooks/hooks.json +10 -0
- package/hooks/hooks_constants/CLAUDE.md +2 -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/ruff_integration_constants.py +16 -0
- 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 +1 -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/ruff_integration.py +49 -5
- package/hooks/validators/run_all_validators.py +206 -9
- package/hooks/validators/test_directory_exemption_constants.py +185 -0
- package/hooks/validators/test_python_antipattern_checks.py +110 -5
- package/hooks/validators/test_ruff_integration.py +92 -1
- package/hooks/validators/test_run_all_validators.py +115 -68
- 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 -44
- 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/conftest.py +16 -1
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +9 -0
- package/scripts/resolve_worker_spawn.py +626 -626
- 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_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/skills/CLAUDE.md +2 -0
- package/skills/auditing-claude-config/SKILL.md +114 -114
- package/skills/autoconverge/SKILL.md +427 -427
- package/skills/autoconverge/reference/convergence.md +24 -3
- package/skills/autoconverge/workflow/CLAUDE.md +1 -0
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +3 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1263
- package/skills/autoconverge/workflow/converge.mjs +167 -0
- package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -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/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/workflows/promotion.md +1 -1
- package/skills/release-notes-html/SKILL.md +164 -0
- 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/scripts/test_invoke_code_review.py +0 -966
- 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
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
35
|
-
from state_description_blocker import (
|
|
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
|
|
36
|
+
from state_description_blocker import evaluate as evaluate_state_description
|
|
39
37
|
|
|
40
|
-
from hooks_constants.pre_tool_use_dispatcher_constants import (
|
|
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
|
|
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
|
-
|
|
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
|
|
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 = (
|