claude-dev-env 2.9.0 → 2.11.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 +2 -2
- package/_shared/advisor/CLAUDE.md +3 -2
- package/_shared/advisor/advisor-protocol.md +74 -108
- package/_shared/advisor/reference/advisor-block.md +37 -0
- package/_shared/advisor/reference/cli-chain.md +45 -0
- package/_shared/advisor/reference/consult-format.md +41 -0
- package/_shared/advisor/reference/lifecycle.md +21 -0
- package/_shared/advisor/reference/sol-rung.md +31 -0
- package/_shared/advisor/reference/spawn-walk-log.md +31 -0
- package/_shared/advisor/reference/third-party-bind.md +30 -0
- package/_shared/advisor/reference/warm-up.md +33 -0
- package/_shared/advisor/scripts/codex_sol_advisor.py +449 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/advisor_route_constants.py +21 -0
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +19 -17
- package/_shared/advisor/scripts/config/advisor_scripts_constants/sol_advisor_constants.py +28 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +32 -9
- package/_shared/advisor/scripts/tests/test_codex_sol_advisor.py +474 -0
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +79 -0
- package/_shared/advisor/scripts/tests/test_tier_model_ids.py +39 -17
- package/_shared/advisor/scripts/tier_model_ids.py +24 -0
- package/_shared/pr-loop/CLAUDE.md +1 -1
- package/_shared/pr-loop/audit-contract.md +17 -6
- package/_shared/pr-loop/audit-reply-template.md +4 -4
- package/_shared/pr-loop/code-rules-gate.md +3 -5
- package/_shared/pr-loop/fix-protocol.md +2 -3
- package/_shared/pr-loop/gh-payloads.md +1 -1
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
- package/_shared/pr-loop/scripts/README.md +1 -1
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/gate_running.py +16 -1
- package/_shared/pr-loop/scripts/code_rules_gate_parts/git_blob_readers.py +11 -5
- package/_shared/pr-loop/scripts/preflight.py +9 -4
- package/_shared/pr-loop/scripts/reviews_disabled.py +50 -22
- package/_shared/pr-loop/scripts/tests/conftest.py +20 -0
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +6 -6
- package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +50 -6
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +1 -1
- package/_shared/pr-loop/state-schema.md +5 -14
- package/agents/CLAUDE.md +2 -2
- package/agents/clean-coder.md +58 -548
- package/agents/code-quality-agent.md +10 -2
- package/agents/code-verifier.md +1 -1
- package/agents/test_agent_frontmatter.py +32 -40
- package/audit-rubrics/CLAUDE.md +2 -1
- package/audit-rubrics/audit-categories.json +704 -0
- package/audit-rubrics/prompts/category-i-concurrency.md +1 -1
- package/bin/CLAUDE.md +16 -5
- package/bin/ever-shipped-skills.mjs +2 -0
- package/bin/install-plan.mjs +402 -0
- package/bin/install-transaction.mjs +455 -0
- package/bin/install.mjs +593 -147
- package/bin/install.plan.test.mjs +194 -0
- package/bin/install.profile-root.test.mjs +154 -0
- package/bin/install.profiles.test.mjs +253 -0
- package/bin/install.settings-defaults.test.mjs +200 -0
- package/bin/install.transaction.test.mjs +400 -0
- package/bin/install.uninstall-transaction.test.mjs +418 -0
- package/bin/merge_managed_permissions.mjs +130 -0
- package/bin/resolve-install-root.mjs +181 -0
- package/bin/select-install-targets.mjs +401 -0
- package/commands/CLAUDE.md +0 -2
- package/docs/references/CLAUDE.md +3 -1
- package/docs/references/advisor-tool.md +25 -7
- package/docs/references/prose-style-enforcement.md +25 -0
- package/docs/references/team-advisor-skill.md +3 -3
- package/docs/references/weak-executor-advisor.md +91 -0
- package/hooks/blocking/CLAUDE.md +6 -6
- package/hooks/blocking/_path_setup.py +9 -5
- package/hooks/blocking/code_rules_docstrings.py +124 -30
- package/hooks/blocking/code_rules_enforcer.py +161 -16
- package/hooks/blocking/code_rules_shared.py +40 -23
- package/hooks/blocking/config/CLAUDE.md +3 -5
- package/hooks/blocking/config/prose_style_enforcement_constants.py +38 -0
- package/hooks/blocking/config/test_prose_style_enforcement_constants.py +45 -0
- package/hooks/blocking/eli11_reply_enforcer.py +70 -113
- package/hooks/blocking/hedging_language_blocker.py +103 -20
- package/hooks/blocking/hook_prose_detector_consistency.py +6 -0
- package/hooks/blocking/intent_only_ending_blocker.py +6 -0
- package/hooks/blocking/plain_language_blocker.py +139 -20
- package/hooks/blocking/pre_tool_use_dispatcher.py +102 -20
- package/hooks/blocking/state_description_blocker.py +7 -1
- package/hooks/blocking/tdd_enforcer.py +8 -0
- package/hooks/blocking/test__path_setup.py +28 -0
- package/hooks/blocking/test_code_rules_enforcer_agent_home_tooling.py +99 -0
- package/hooks/blocking/test_code_rules_enforcer_docstring_args_span_scope.py +232 -10
- package/hooks/blocking/test_code_rules_enforcer_ephemeral.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_join_separator_magic.py +41 -0
- package/hooks/blocking/test_code_rules_enforcer_string_magic.py +98 -0
- package/hooks/blocking/test_eli11_reply_enforcer.py +98 -165
- package/hooks/blocking/test_fable_spawn_gate.py +18 -11
- package/hooks/blocking/test_hedging_language_blocker.py +120 -1
- package/hooks/blocking/test_hook_prose_detector_consistency.py +28 -8
- package/hooks/blocking/test_intent_only_ending_blocker.py +27 -2
- package/hooks/blocking/test_package_inventory_stale_blocker.py +11 -4
- package/hooks/blocking/test_plain_language_blocker.py +129 -19
- package/hooks/blocking/test_plain_language_blocker_allowlist.py +70 -26
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +99 -26
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +87 -50
- package/hooks/blocking/test_state_description_blocker.py +45 -2
- package/hooks/blocking/test_stop_dispatcher.py +11 -7
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +12 -12
- package/hooks/blocking/volatile_path_in_post_blocker.py +2 -2
- package/hooks/hooks.json +15 -0
- package/hooks/hooks_constants/CLAUDE.md +14 -3
- package/hooks/hooks_constants/ask_user_question_shape.py +281 -0
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +2 -1
- package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +5 -12
- package/hooks/hooks_constants/hedging_uncertainty_constants.py +42 -0
- package/hooks/hooks_constants/issue_tracker_session_starter_constants.py +23 -0
- package/hooks/hooks_constants/orchestrator_auto_starter_constants.py +23 -0
- package/hooks/hooks_constants/piped_pytest_blocker_constants.py +4 -1
- package/hooks/hooks_constants/plain_language_blocker_constants.py +4 -1
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +6 -0
- package/hooks/hooks_constants/project_paths_reader.py +31 -4
- package/hooks/hooks_constants/prose_matcher_precision_constants.py +40 -0
- package/hooks/hooks_constants/pytest_invocation.py +354 -0
- package/hooks/hooks_constants/session_start_injector.py +163 -0
- package/hooks/hooks_constants/session_start_injector_constants.py +46 -0
- package/hooks/hooks_constants/shell_command_pipeline.py +397 -0
- package/hooks/hooks_constants/shell_command_segments.py +5 -0
- package/hooks/hooks_constants/test_ask_user_question_shape.py +167 -0
- package/hooks/hooks_constants/test_project_paths_reader.py +29 -0
- package/hooks/hooks_constants/test_prose_metrics_parity.py +8 -0
- package/hooks/hooks_constants/test_pytest_invocation.py +130 -0
- package/hooks/hooks_constants/test_session_start_injector.py +168 -0
- package/hooks/hooks_constants/test_shell_command_pipeline.py +135 -0
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +1 -1
- package/hooks/hooks_constants/working_style_prompt_constants.py +30 -0
- package/hooks/observability/CLAUDE.md +2 -0
- package/hooks/observability/prose_matcher_advisory.py +237 -0
- package/hooks/observability/test_prose_matcher_advisory.py +143 -0
- package/hooks/session/CLAUDE.md +9 -1
- package/hooks/session/_path_setup.py +13 -0
- package/hooks/session/issue_tracker_session_starter.py +135 -0
- package/hooks/session/orchestrator_auto_starter.py +100 -0
- package/hooks/session/test__path_setup.py +28 -0
- package/hooks/session/test_issue_tracker_session_starter.py +104 -0
- package/hooks/session/test_orchestrator_auto_starter.py +99 -0
- package/hooks/session/test_working_style_prompt.py +47 -0
- package/hooks/session/untracked_repo_detector.py +1 -24
- package/hooks/session/working_style_prompt.py +36 -0
- package/hooks/validators/_path_setup.py +19 -0
- package/hooks/validators/run_all_validators.py +8 -13
- package/installable-surfaces.manifest.json +21 -0
- package/output-styles/CLAUDE.md +1 -3
- package/package.json +4 -2
- package/rules/CLAUDE.md +1 -0
- package/rules/durable-post-artifacts.md +2 -2
- package/rules/eli11-replies.md +6 -1
- package/rules/hedging-claims.md +4 -2
- package/rules/long-horizon-autonomy.md +3 -1
- package/rules/opus5-communication-contract.md +45 -0
- package/rules/plain-language.md +2 -2
- package/rules/research-mode.md +1 -1
- package/scripts/CLAUDE.md +11 -0
- package/scripts/Sync-RepoMain.ps1 +215 -0
- package/scripts/active_capability_references.py +218 -0
- package/scripts/ci/windows-installer-lifecycle.ps1 +78 -0
- package/scripts/claude_chain_runner.py +394 -6
- package/scripts/claude_chain_usage.py +1 -1
- package/scripts/codex_compat_materializer.py +105 -85
- package/scripts/dev_env_scripts_constants/CLAUDE.md +2 -0
- package/scripts/dev_env_scripts_constants/active_capability_constants.py +46 -0
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +74 -0
- package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
- package/scripts/profile-isolation-launchers/config/mcp-bundles.json +25 -0
- package/scripts/profile-isolation-launchers/config/profile-isolation-constants.mjs +60 -0
- package/scripts/profile-isolation-launchers/config/profiles.manifest.json +54 -0
- package/scripts/profile-isolation-launchers/config/shared-allowlist.json +64 -0
- package/scripts/profile-isolation-launchers/launcher-runtime.mjs +180 -0
- package/scripts/profile-isolation-launchers/lib/profile-manifest.mjs +288 -0
- package/scripts/profile-isolation-launchers/mcp-bundles.mjs +275 -0
- package/scripts/profile-isolation-launchers/profile-isolation-contract.test.mjs +221 -0
- package/scripts/profile-isolation-launchers/tests/launcher-runtime.test.mjs +108 -0
- package/scripts/profile-isolation-launchers/tests/mcp-bundles.test.mjs +147 -0
- package/scripts/profile-isolation-launchers/tests/shortcut-contract.test.ps1 +102 -0
- package/scripts/profile-isolation-launchers/tests/version-compatibility.test.mjs +210 -0
- package/scripts/profile-isolation-launchers/version-compatibility.mjs +299 -0
- package/scripts/profile-isolation-launchers/windows/shortcut-inventory.ps1 +127 -0
- package/scripts/profile-isolation-launchers/windows/shortcut-manifest.json +51 -0
- package/scripts/profile-isolation-launchers/windows/shortcut-reconcile.ps1 +77 -0
- package/scripts/spawn_grok_batch.py +3 -0
- package/scripts/test_active_capability_references.py +108 -0
- package/scripts/test_claude_chain_runner.py +414 -82
- package/scripts/test_claude_chain_usage.py +12 -12
- package/scripts/test_resolve_worker_spawn.py +2 -2
- package/scripts/test_verify_installable_package.py +208 -0
- package/scripts/tests/test_codex_compat_materializer.py +33 -0
- package/scripts/verify_installable_package.py +612 -0
- package/settings.json +10 -0
- package/skills/CLAUDE.md +2 -0
- package/skills/_shared/advisor/CLAUDE.md +1 -1
- package/skills/_shared/advisor/scripts/README.md +2 -0
- package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -0
- package/skills/_shared/pr-loop/scripts/audit_category_schema.py +355 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +32 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +7 -19
- package/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +94 -0
- package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +21 -0
- package/skills/autoconverge/reference/convergence.md +2 -1
- package/skills/autoconverge/reference/stop-conditions.md +5 -3
- package/skills/beat-sheet/SKILL.md +54 -0
- package/skills/beat-sheet/reference/visual-beats.md +29 -0
- package/skills/bugteam/CONSTRAINTS.md +4 -4
- package/skills/bugteam/EXAMPLES.md +1 -1
- package/skills/bugteam/reference/README.md +1 -1
- package/skills/e-code-review/SKILL.md +26 -5
- package/skills/e-code-review/reference/effort-evaluation.md +35 -0
- package/skills/e-code-review/reference/medium.md +15 -4
- package/skills/e-code-review/scripts/config/e_code_review_effort_constants/__init__.py +41 -0
- package/skills/e-code-review/scripts/config/e_code_review_effort_constants/effort_constants.py +40 -0
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
- package/skills/e-code-review/scripts/effort_defaults_evidence.json +186 -0
- package/skills/e-code-review/scripts/effort_evaluation.py +362 -0
- package/skills/e-code-review/scripts/finding_pipeline.py +140 -0
- package/skills/e-code-review/scripts/fixtures/demanding.json +26 -0
- package/skills/e-code-review/scripts/fixtures/easy.json +14 -0
- package/skills/e-code-review/scripts/fixtures/medium.json +20 -0
- package/skills/e-code-review/scripts/grok_code_review.py +16 -7
- package/skills/e-code-review/scripts/test_effort_evaluation.py +180 -0
- package/skills/e-code-review/scripts/test_finding_pipeline.py +197 -0
- package/skills/e-code-review/scripts/test_grok_code_review.py +77 -0
- package/skills/grokify/SKILL.md +1 -1
- package/skills/grokify/templates/handoff-template.md +2 -2
- package/skills/orchestrator/SKILL.md +5 -4
- package/skills/plan-to-pr/scripts/create_packet.py +4 -4
- package/skills/plan-to-pr/scripts/load_skill_constants.py +41 -0
- package/skills/plan-to-pr/scripts/validate_packet.py +4 -4
- package/skills/plan-to-pr/scripts/validate_protocol.py +4 -1
- package/skills/plan-to-pr/scripts/validate_run.py +4 -1
- package/skills/pr-converge/scripts/check_convergence.py +21 -19
- package/skills/pr-converge/scripts/check_convergence_availability.py +50 -7
- package/skills/pr-converge/scripts/conftest.py +35 -0
- package/skills/pr-converge/scripts/test_check_convergence_availability.py +65 -0
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +11 -1
- package/skills/pr-converge/scripts/test_check_convergence_contract.py +9 -2
- package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
- package/skills/rebase/SKILL.md +15 -3
- package/skills/reviewer-gates/SKILL.md +2 -2
- package/skills/show/SKILL.md +51 -0
- package/skills/show/references/accessibility.md +7 -0
- package/skills/show/references/art.md +3 -0
- package/skills/show/references/charts.md +3 -0
- package/skills/show/references/core-design.md +14 -0
- package/skills/show/references/erds.md +3 -0
- package/skills/show/references/flowcharts.md +3 -0
- package/skills/show/references/host-and-html.md +3 -0
- package/skills/show/references/illustrative-diagrams.md +10 -0
- package/skills/show/references/interaction.md +3 -0
- package/skills/show/references/mockups.md +3 -0
- package/skills/show/references/quality-gates.md +7 -0
- package/skills/show/references/structural-diagrams.md +3 -0
- package/skills/show/references/subject-inventory.md +21 -0
- package/skills/show/references/svg-contract.md +22 -0
- package/skills/show/routing.yaml +30 -0
- package/skills/show/samples/pr1262-v2.svg +222 -0
- package/skills/show/scripts/README.md +6 -0
- package/skills/show/scripts/validate-artifact.py +91 -0
- package/skills/show/scripts/validate-package.py +18 -0
- package/skills/show/templates/html-widget.html +4 -0
- package/skills/show/templates/svg-base.svg +19 -0
- package/skills/show/tests/fixtures/css-var.svg +6 -0
- package/skills/show/tests/fixtures/dead-ref.svg +7 -0
- package/skills/show/tests/fixtures/filled-glyph.svg +8 -0
- package/skills/show/tests/fixtures/inherited-fill.svg +18 -0
- package/skills/show/tests/fixtures/invalid.svg +1 -0
- package/skills/show/tests/fixtures/large-canvas.svg +21 -0
- package/skills/show/tests/fixtures/unfilled-connector.svg +15 -0
- package/skills/show/tests/fixtures/valid.html +1 -0
- package/skills/show/tests/test_validate-artifact.py +74 -0
- package/skills/show/tests/test_validators.py +59 -0
- package/skills/show/workflows/create-visual.md +13 -0
- package/skills/show/workflows/review-visual.md +20 -0
- package/skills/split-pr/SKILL.md +85 -0
- package/skills/split-pr/reference/path-layers.md +16 -0
- package/skills/split-pr/reference/proposal-format.md +15 -0
- package/skills/split-pr/reference/split-further-loop.md +10 -0
- package/skills/split-pr/reference/splitting-principles.md +26 -0
- package/skills/split-pr/scripts/analyze_pr.py +279 -0
- package/skills/split-pr/scripts/categorize_files.py +106 -0
- package/skills/split-pr/scripts/config/__init__.py +1 -0
- package/skills/split-pr/scripts/config/dependency_constants.py +14 -0
- package/skills/split-pr/scripts/config/git_operations_constants.py +36 -0
- package/skills/split-pr/scripts/config/packing_constants.py +61 -0
- package/skills/split-pr/scripts/config/plan_constants.py +49 -0
- package/skills/split-pr/scripts/config/split_pr_constants.py +110 -0
- package/skills/split-pr/scripts/execute_split_slices.py +82 -0
- package/skills/split-pr/scripts/pack_files_into_slices.py +212 -0
- package/skills/split-pr/scripts/split_pr_dependency_graph.py +70 -0
- package/skills/split-pr/scripts/split_pr_git_operations.py +184 -0
- package/skills/split-pr/scripts/split_pr_layer_order.py +58 -0
- package/skills/split-pr/scripts/split_pr_paginate.py +119 -0
- package/skills/split-pr/scripts/split_pr_process_runner.py +52 -0
- package/skills/split-pr/scripts/split_pr_script_types.py +126 -0
- package/skills/split-pr/scripts/split_pr_title.py +41 -0
- package/skills/split-pr/scripts/test_analyze_pr.py +228 -0
- package/skills/split-pr/scripts/test_categorize_files.py +55 -0
- package/skills/split-pr/scripts/test_categorize_files_packing.py +59 -0
- package/skills/split-pr/scripts/test_execute_split_slices.py +99 -0
- package/skills/split-pr/scripts/test_split_pr_dependency_graph.py +47 -0
- package/skills/split-pr/scripts/test_split_pr_git_operations.py +125 -0
- package/skills/split-pr/scripts/test_split_pr_layer_order.py +36 -0
- package/skills/split-pr/scripts/test_split_pr_paginate.py +65 -0
- package/skills/split-pr/scripts/test_split_pr_script_types.py +73 -0
- package/skills/split-pr/scripts/test_split_pr_title.py +28 -0
- package/skills/split-pr/scripts/test_verify_dependency_graph.py +46 -0
- package/skills/split-pr/scripts/test_verify_plan.py +56 -0
- package/skills/split-pr/scripts/test_verify_plan_contract.py +50 -0
- package/skills/split-pr/scripts/test_verify_plan_path_normalization.py +45 -0
- package/skills/split-pr/scripts/verify_dependency_graph.py +111 -0
- package/skills/split-pr/scripts/verify_plan.py +139 -0
- package/skills/team-advisor/SKILL.md +7 -4
- package/skills/team-advisor/reference/advisor-docs-review.md +207 -0
- package/system-prompts/software-engineer.xml +11 -2
- package/commands/initialize.md +0 -90
- package/commands/stubcheck.md +0 -88
- package/output-styles/caveman-agent.md +0 -37
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
"""Tests for hedging_language_blocker hook response shape."""
|
|
2
2
|
|
|
3
3
|
import importlib.util
|
|
4
|
+
import io
|
|
4
5
|
import json
|
|
5
6
|
import os
|
|
6
7
|
import subprocess
|
|
7
8
|
import sys
|
|
8
9
|
import tempfile
|
|
9
10
|
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
10
13
|
HOOK_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "hedging_language_blocker.py")
|
|
11
14
|
_HOOKS_DIR = os.path.dirname(HOOK_SCRIPT_PATH)
|
|
12
15
|
_HOOKS_ROOT = os.path.join(_HOOKS_DIR, "..")
|
|
@@ -30,14 +33,22 @@ CLEAN_MESSAGE = "This is verified by the source document."
|
|
|
30
33
|
EMPTY_MESSAGE = ""
|
|
31
34
|
|
|
32
35
|
|
|
33
|
-
def run_hook_with_message(
|
|
36
|
+
def run_hook_with_message(
|
|
37
|
+
assistant_message: str, *, is_prose_style_enabled: bool = True
|
|
38
|
+
) -> subprocess.CompletedProcess:
|
|
34
39
|
hook_input_payload = json.dumps({"last_assistant_message": assistant_message})
|
|
40
|
+
environment_by_key = os.environ.copy()
|
|
41
|
+
if is_prose_style_enabled:
|
|
42
|
+
environment_by_key["CLAUDE_PROSE_STYLE_ENFORCEMENT"] = "1"
|
|
43
|
+
else:
|
|
44
|
+
environment_by_key.pop("CLAUDE_PROSE_STYLE_ENFORCEMENT", None)
|
|
35
45
|
return subprocess.run(
|
|
36
46
|
[sys.executable, HOOK_SCRIPT_PATH],
|
|
37
47
|
input=hook_input_payload,
|
|
38
48
|
capture_output=True,
|
|
39
49
|
text=True,
|
|
40
50
|
check=False,
|
|
51
|
+
env=environment_by_key,
|
|
41
52
|
)
|
|
42
53
|
|
|
43
54
|
|
|
@@ -58,6 +69,8 @@ def run_hook_with_patched_search_paths(
|
|
|
58
69
|
wrapper_file_path = wrapper_file.name
|
|
59
70
|
|
|
60
71
|
hook_input_payload = json.dumps({"last_assistant_message": assistant_message})
|
|
72
|
+
environment_by_key = os.environ.copy()
|
|
73
|
+
environment_by_key["CLAUDE_PROSE_STYLE_ENFORCEMENT"] = "1"
|
|
61
74
|
try:
|
|
62
75
|
completed_process = subprocess.run(
|
|
63
76
|
[sys.executable, wrapper_file_path],
|
|
@@ -65,6 +78,7 @@ def run_hook_with_patched_search_paths(
|
|
|
65
78
|
capture_output=True,
|
|
66
79
|
text=True,
|
|
67
80
|
check=False,
|
|
81
|
+
env=environment_by_key,
|
|
68
82
|
)
|
|
69
83
|
finally:
|
|
70
84
|
os.unlink(wrapper_file_path)
|
|
@@ -80,6 +94,49 @@ def test_user_facing_notice_matches_config_messages_module():
|
|
|
80
94
|
assert module.USER_FACING_NOTICE == USER_FACING_NOTICE
|
|
81
95
|
|
|
82
96
|
|
|
97
|
+
def test_hedging_scan_is_default_off() -> None:
|
|
98
|
+
completed_process = run_hook_with_message(
|
|
99
|
+
HEDGING_MESSAGE, is_prose_style_enabled=False
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
assert completed_process.returncode == 0
|
|
103
|
+
assert completed_process.stdout == ""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_default_off_emits_privacy_safe_advisory_candidates(
|
|
107
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
108
|
+
) -> None:
|
|
109
|
+
all_emitted: list[tuple[str, str, str]] = []
|
|
110
|
+
|
|
111
|
+
def _record_advisory(
|
|
112
|
+
matcher_id: str, surface: str, context_text: str, **_kwargs: object
|
|
113
|
+
) -> dict[str, object]:
|
|
114
|
+
all_emitted.append((matcher_id, surface, context_text))
|
|
115
|
+
return {}
|
|
116
|
+
|
|
117
|
+
monkeypatch.setattr(
|
|
118
|
+
hedging_language_blocker, "emit_advisory_candidate", _record_advisory
|
|
119
|
+
)
|
|
120
|
+
monkeypatch.setattr(
|
|
121
|
+
hedging_language_blocker,
|
|
122
|
+
"prose_style_enforcement_enabled_in_environment",
|
|
123
|
+
lambda: False,
|
|
124
|
+
)
|
|
125
|
+
monkeypatch.setattr(
|
|
126
|
+
hedging_language_blocker.sys,
|
|
127
|
+
"stdin",
|
|
128
|
+
io.StringIO(json.dumps({"last_assistant_message": HEDGING_MESSAGE})),
|
|
129
|
+
)
|
|
130
|
+
with pytest.raises(SystemExit) as exit_info:
|
|
131
|
+
hedging_language_blocker.main()
|
|
132
|
+
assert exit_info.value.code == 0
|
|
133
|
+
assert all_emitted
|
|
134
|
+
matcher_id, surface, context_text = all_emitted[0]
|
|
135
|
+
assert matcher_id == "hedging_word"
|
|
136
|
+
assert surface == "Stop"
|
|
137
|
+
assert "likely" in context_text
|
|
138
|
+
|
|
139
|
+
|
|
83
140
|
def test_hedging_message_emits_block_with_short_user_notice():
|
|
84
141
|
completed_process = run_hook_with_message(HEDGING_MESSAGE)
|
|
85
142
|
|
|
@@ -140,3 +197,65 @@ def test_empty_message_passes_through_with_no_output():
|
|
|
140
197
|
|
|
141
198
|
assert completed_process.returncode == 0
|
|
142
199
|
assert completed_process.stdout == ""
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_explicit_unverified_label_in_same_sentence_passes() -> None:
|
|
203
|
+
completed_process = run_hook_with_message(
|
|
204
|
+
"This claim is unverified; the deploy is probably blocked."
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
assert completed_process.returncode == 0
|
|
208
|
+
assert completed_process.stdout == ""
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_i_dont_know_label_in_same_sentence_passes() -> None:
|
|
212
|
+
completed_process = run_hook_with_message(
|
|
213
|
+
"I don't know whether the port is probably open."
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
assert completed_process.returncode == 0
|
|
217
|
+
assert completed_process.stdout == ""
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_supported_probability_without_hedge_word_passes() -> None:
|
|
221
|
+
completed_process = run_hook_with_message(
|
|
222
|
+
"The suite reports 0.92 precision on the labeled fixture set in "
|
|
223
|
+
"test_prose_matcher_advisory.py."
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
assert completed_process.returncode == 0
|
|
227
|
+
assert completed_process.stdout == ""
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_label_in_one_sentence_does_not_exempt_bare_hedge_in_another() -> None:
|
|
231
|
+
completed_process = run_hook_with_message(
|
|
232
|
+
"This claim is unverified. The deploy is probably blocked."
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
assert completed_process.returncode == 0
|
|
236
|
+
parsed_response = json.loads(completed_process.stdout)
|
|
237
|
+
assert parsed_response["decision"] == "block"
|
|
238
|
+
assert "probably" in parsed_response["reason"]
|
|
239
|
+
assert "explicit uncertainty label" in parsed_response["reason"]
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_bare_probably_still_blocks_with_positive_corrective() -> None:
|
|
243
|
+
completed_process = run_hook_with_message("The deploy is probably blocked.")
|
|
244
|
+
|
|
245
|
+
assert completed_process.returncode == 0
|
|
246
|
+
parsed_response = json.loads(completed_process.stdout)
|
|
247
|
+
assert parsed_response["decision"] == "block"
|
|
248
|
+
assert "probably" in parsed_response["reason"]
|
|
249
|
+
assert "label that claim unverified" in parsed_response["reason"]
|
|
250
|
+
assert "AskUserQuestion" in parsed_response["reason"]
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def test_find_blocking_hedging_terms_is_sentence_scoped() -> None:
|
|
254
|
+
bare = hedging_language_blocker.find_blocking_hedging_terms(
|
|
255
|
+
"This claim is unverified. The deploy is probably blocked."
|
|
256
|
+
)
|
|
257
|
+
labeled = hedging_language_blocker.find_blocking_hedging_terms(
|
|
258
|
+
"This claim is unverified; the deploy is probably blocked."
|
|
259
|
+
)
|
|
260
|
+
assert bare == ["probably"]
|
|
261
|
+
assert labeled == []
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import importlib.util
|
|
4
4
|
import io
|
|
5
5
|
import json
|
|
6
|
+
import os
|
|
6
7
|
import pathlib
|
|
7
8
|
import sys
|
|
8
9
|
from unittest import mock
|
|
@@ -192,14 +193,33 @@ def test_written_content_reads_edit_new_string() -> None:
|
|
|
192
193
|
assert written_content("Edit", edit_input) == "edited body"
|
|
193
194
|
|
|
194
195
|
|
|
195
|
-
def _run_main_with_io(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
196
|
+
def _run_main_with_io(
|
|
197
|
+
input_text: str, *, is_prose_style_enabled: bool = True
|
|
198
|
+
) -> str:
|
|
199
|
+
environment_by_key = os.environ.copy()
|
|
200
|
+
if is_prose_style_enabled:
|
|
201
|
+
environment_by_key["CLAUDE_PROSE_STYLE_ENFORCEMENT"] = "1"
|
|
202
|
+
else:
|
|
203
|
+
environment_by_key.pop("CLAUDE_PROSE_STYLE_ENFORCEMENT", None)
|
|
204
|
+
with mock.patch.dict(os.environ, environment_by_key, clear=True):
|
|
205
|
+
with mock.patch("sys.stdin", io.StringIO(input_text)):
|
|
206
|
+
with mock.patch("sys.stdout", new_callable=io.StringIO) as mock_stdout:
|
|
207
|
+
try:
|
|
208
|
+
hook_module.main()
|
|
209
|
+
except SystemExit:
|
|
210
|
+
pass
|
|
211
|
+
return mock_stdout.getvalue()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_main_is_default_off_for_overstated_hook_module_write() -> None:
|
|
215
|
+
hook_input = {
|
|
216
|
+
"tool_name": "Write",
|
|
217
|
+
"tool_input": {
|
|
218
|
+
"file_path": "/repo/hooks/hooks_constants/some_blocker_constants.py",
|
|
219
|
+
"content": _OVERSTATED_MESSAGE_MODULE,
|
|
220
|
+
},
|
|
221
|
+
}
|
|
222
|
+
assert _run_main_with_io(json.dumps(hook_input), is_prose_style_enabled=False) == ""
|
|
203
223
|
|
|
204
224
|
|
|
205
225
|
def test_main_blocks_overstated_hook_module_write() -> None:
|
|
@@ -42,24 +42,40 @@ USER_DIRECTED_NEXT_STEPS_MESSAGE = (
|
|
|
42
42
|
EMPTY_MESSAGE = ""
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
def run_hook_with_message(
|
|
45
|
+
def run_hook_with_message(
|
|
46
|
+
assistant_message: str, *, is_prose_style_enabled: bool = True
|
|
47
|
+
) -> subprocess.CompletedProcess:
|
|
46
48
|
hook_input_payload = json.dumps({"last_assistant_message": assistant_message})
|
|
49
|
+
environment_by_key = os.environ.copy()
|
|
50
|
+
if is_prose_style_enabled:
|
|
51
|
+
environment_by_key["CLAUDE_PROSE_STYLE_ENFORCEMENT"] = "1"
|
|
52
|
+
else:
|
|
53
|
+
environment_by_key.pop("CLAUDE_PROSE_STYLE_ENFORCEMENT", None)
|
|
47
54
|
return subprocess.run(
|
|
48
55
|
[sys.executable, HOOK_SCRIPT_PATH],
|
|
49
56
|
input=hook_input_payload,
|
|
50
57
|
capture_output=True,
|
|
51
58
|
text=True,
|
|
52
59
|
check=False,
|
|
60
|
+
env=environment_by_key,
|
|
53
61
|
)
|
|
54
62
|
|
|
55
63
|
|
|
56
|
-
def run_hook_with_payload(
|
|
64
|
+
def run_hook_with_payload(
|
|
65
|
+
hook_input_payload: dict, *, is_prose_style_enabled: bool = True
|
|
66
|
+
) -> subprocess.CompletedProcess:
|
|
67
|
+
environment_by_key = os.environ.copy()
|
|
68
|
+
if is_prose_style_enabled:
|
|
69
|
+
environment_by_key["CLAUDE_PROSE_STYLE_ENFORCEMENT"] = "1"
|
|
70
|
+
else:
|
|
71
|
+
environment_by_key.pop("CLAUDE_PROSE_STYLE_ENFORCEMENT", None)
|
|
57
72
|
return subprocess.run(
|
|
58
73
|
[sys.executable, HOOK_SCRIPT_PATH],
|
|
59
74
|
input=json.dumps(hook_input_payload),
|
|
60
75
|
capture_output=True,
|
|
61
76
|
text=True,
|
|
62
77
|
check=False,
|
|
78
|
+
env=environment_by_key,
|
|
63
79
|
)
|
|
64
80
|
|
|
65
81
|
|
|
@@ -76,6 +92,15 @@ def test_user_facing_notice_matches_config_messages_module():
|
|
|
76
92
|
)
|
|
77
93
|
|
|
78
94
|
|
|
95
|
+
def test_intent_scan_is_default_off() -> None:
|
|
96
|
+
completed_process = run_hook_with_message(
|
|
97
|
+
INTENT_ENDING_MESSAGE, is_prose_style_enabled=False
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
assert completed_process.returncode == 0
|
|
101
|
+
assert completed_process.stdout == ""
|
|
102
|
+
|
|
103
|
+
|
|
79
104
|
def test_intent_ending_message_emits_block_with_short_user_notice():
|
|
80
105
|
completed_process = run_hook_with_message(INTENT_ENDING_MESSAGE)
|
|
81
106
|
|
|
@@ -367,9 +367,16 @@ def test_find_stale_inventory_returns_survey_for_omission(tmp_path: Path):
|
|
|
367
367
|
assert survey.named_basenames == {"dialer_compose.py", "compose_dialer_cli.py"}
|
|
368
368
|
|
|
369
369
|
|
|
370
|
-
def
|
|
371
|
-
|
|
372
|
-
|
|
370
|
+
def test_find_stale_inventory_skips_directory_below_minimum_named_entries(
|
|
371
|
+
tmp_path: Path,
|
|
372
|
+
) -> None:
|
|
373
|
+
"""Skip a package inventory that names fewer than the minimum on-disk siblings."""
|
|
374
|
+
package_directory = tmp_path / "prose_package"
|
|
375
|
+
package_directory.mkdir()
|
|
376
|
+
(package_directory / "CLAUDE.md").write_text(
|
|
377
|
+
"| File | Role |\n|---|---|\n| `only_one.py` | sole entry |\n",
|
|
378
|
+
encoding="utf-8",
|
|
373
379
|
)
|
|
374
|
-
|
|
380
|
+
(package_directory / "only_one.py").write_text("FLAG = 1\n", encoding="utf-8")
|
|
381
|
+
new_file_path = package_directory / "new_helper.py"
|
|
375
382
|
assert find_stale_inventory(str(new_file_path)) is None
|
|
@@ -42,18 +42,31 @@ strip_non_prose_regions = hook_module.strip_non_prose_regions
|
|
|
42
42
|
build_block_reason = hook_module.build_block_reason
|
|
43
43
|
find_question_block_violations = hook_module.find_question_block_violations
|
|
44
44
|
build_lean_block_reason = hook_module.build_lean_block_reason
|
|
45
|
-
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def evaluate(payload_by_key: dict[str, object]) -> str | None:
|
|
48
|
+
"""Call evaluate with opinionated prose enforcement enabled."""
|
|
49
|
+
with patch.dict(os.environ, {"CLAUDE_PROSE_STYLE_ENFORCEMENT": "1"}):
|
|
50
|
+
return hook_module.evaluate(payload_by_key)
|
|
46
51
|
|
|
47
52
|
from pre_tool_use_dispatcher import NativeHook, run_native_hook # noqa: E402
|
|
48
53
|
|
|
49
54
|
|
|
50
|
-
def _run_hook_with_payload(
|
|
55
|
+
def _run_hook_with_payload(
|
|
56
|
+
payload: dict, *, is_prose_style_enabled: bool = True
|
|
57
|
+
) -> subprocess.CompletedProcess[str]:
|
|
58
|
+
environment_by_key = os.environ.copy()
|
|
59
|
+
if is_prose_style_enabled:
|
|
60
|
+
environment_by_key["CLAUDE_PROSE_STYLE_ENFORCEMENT"] = "1"
|
|
61
|
+
else:
|
|
62
|
+
environment_by_key.pop("CLAUDE_PROSE_STYLE_ENFORCEMENT", None)
|
|
51
63
|
return subprocess.run(
|
|
52
64
|
[sys.executable, str(HOOK_SCRIPT_PATH)],
|
|
53
65
|
input=json.dumps(payload),
|
|
54
66
|
capture_output=True,
|
|
55
67
|
text=True,
|
|
56
68
|
check=False,
|
|
69
|
+
env=environment_by_key,
|
|
57
70
|
)
|
|
58
71
|
|
|
59
72
|
|
|
@@ -64,10 +77,81 @@ def _decision_from(completed: subprocess.CompletedProcess[str]) -> str | None:
|
|
|
64
77
|
return parsed.get("hookSpecificOutput", {}).get("permissionDecision")
|
|
65
78
|
|
|
66
79
|
|
|
80
|
+
def _system_message_from(completed: subprocess.CompletedProcess[str]) -> str | None:
|
|
81
|
+
if not completed.stdout:
|
|
82
|
+
return None
|
|
83
|
+
parsed = json.loads(completed.stdout)
|
|
84
|
+
system_message = parsed.get("systemMessage")
|
|
85
|
+
return system_message if isinstance(system_message, str) else None
|
|
86
|
+
|
|
87
|
+
|
|
67
88
|
def test_canonical_hook_script_exists_at_expected_path() -> None:
|
|
68
89
|
assert HOOK_SCRIPT_PATH.is_file()
|
|
69
90
|
|
|
70
91
|
|
|
92
|
+
def test_heavy_word_scan_is_default_off(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
93
|
+
monkeypatch.delenv("CLAUDE_PROSE_STYLE_ENFORCEMENT", raising=False)
|
|
94
|
+
payload = {
|
|
95
|
+
"tool_name": "Write",
|
|
96
|
+
"tool_input": {
|
|
97
|
+
"file_path": "notes.md",
|
|
98
|
+
"content": "We initiate the worker pool at boot.",
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
assert hook_module.evaluate(payload) is None
|
|
102
|
+
completed = _run_hook_with_payload(payload, is_prose_style_enabled=False)
|
|
103
|
+
assert _decision_from(completed) is None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_default_off_emits_privacy_safe_advisory_candidates(
|
|
107
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
108
|
+
) -> None:
|
|
109
|
+
monkeypatch.delenv("CLAUDE_PROSE_STYLE_ENFORCEMENT", raising=False)
|
|
110
|
+
all_emitted: list[tuple[str, str, str]] = []
|
|
111
|
+
|
|
112
|
+
def _record_advisory(
|
|
113
|
+
matcher_id: str, surface: str, context_text: str, **_kwargs: object
|
|
114
|
+
) -> dict[str, object]:
|
|
115
|
+
all_emitted.append((matcher_id, surface, context_text))
|
|
116
|
+
return {}
|
|
117
|
+
|
|
118
|
+
monkeypatch.setattr(hook_module, "emit_advisory_candidate", _record_advisory)
|
|
119
|
+
payload = {
|
|
120
|
+
"tool_name": "Write",
|
|
121
|
+
"tool_input": {
|
|
122
|
+
"file_path": "notes.md",
|
|
123
|
+
"content": "We initiate the worker pool at boot.",
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
assert hook_module.evaluate(payload) is None
|
|
127
|
+
assert all_emitted
|
|
128
|
+
matcher_id, surface, context_text = all_emitted[0]
|
|
129
|
+
assert matcher_id == "plain_language_heavy_word"
|
|
130
|
+
assert surface == "Write"
|
|
131
|
+
assert "initiate" in context_text
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_heavy_word_scan_opt_in_allows_with_system_message_swaps() -> None:
|
|
135
|
+
payload = {
|
|
136
|
+
"tool_name": "Write",
|
|
137
|
+
"tool_input": {
|
|
138
|
+
"file_path": "notes.md",
|
|
139
|
+
"content": "We initiate the worker pool at boot.",
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
assert evaluate(payload) is None
|
|
143
|
+
with patch.dict(os.environ, {"CLAUDE_PROSE_STYLE_ENFORCEMENT": "1"}):
|
|
144
|
+
deny_reason, advisory_message = hook_module.evaluate_with_advisory(payload)
|
|
145
|
+
assert deny_reason is None
|
|
146
|
+
assert advisory_message is not None
|
|
147
|
+
assert 'use "start" instead of "initiate"' in advisory_message
|
|
148
|
+
completed = _run_hook_with_payload(payload, is_prose_style_enabled=True)
|
|
149
|
+
assert _decision_from(completed) == "allow"
|
|
150
|
+
system_message = _system_message_from(completed)
|
|
151
|
+
assert system_message is not None
|
|
152
|
+
assert 'use "start" instead of "initiate"' in system_message
|
|
153
|
+
|
|
154
|
+
|
|
71
155
|
def test_bare_prose_banned_term_is_detected() -> None:
|
|
72
156
|
matched = find_banned_terms("We initiate the worker pool at boot.")
|
|
73
157
|
assert any(each_term == "initiate" for each_term, _replacement in matched)
|
|
@@ -127,7 +211,7 @@ def test_block_reason_names_term_and_replacement() -> None:
|
|
|
127
211
|
assert "start" in reason
|
|
128
212
|
|
|
129
213
|
|
|
130
|
-
def
|
|
214
|
+
def test_ask_user_question_with_banned_term_is_advisory_allow() -> None:
|
|
131
215
|
payload = {
|
|
132
216
|
"tool_name": "AskUserQuestion",
|
|
133
217
|
"tool_input": {
|
|
@@ -141,10 +225,13 @@ def test_ask_user_question_with_banned_term_is_denied() -> None:
|
|
|
141
225
|
},
|
|
142
226
|
}
|
|
143
227
|
completed = _run_hook_with_payload(payload)
|
|
144
|
-
assert _decision_from(completed) == "
|
|
228
|
+
assert _decision_from(completed) == "allow"
|
|
229
|
+
system_message = _system_message_from(completed)
|
|
230
|
+
assert system_message is not None
|
|
231
|
+
assert "utilize" in system_message
|
|
145
232
|
|
|
146
233
|
|
|
147
|
-
def
|
|
234
|
+
def test_ask_user_question_banned_term_in_option_label_is_advisory_allow() -> None:
|
|
148
235
|
payload = {
|
|
149
236
|
"tool_name": "AskUserQuestion",
|
|
150
237
|
"tool_input": {
|
|
@@ -158,7 +245,10 @@ def test_ask_user_question_banned_term_in_option_label_is_denied() -> None:
|
|
|
158
245
|
},
|
|
159
246
|
}
|
|
160
247
|
completed = _run_hook_with_payload(payload)
|
|
161
|
-
assert _decision_from(completed) == "
|
|
248
|
+
assert _decision_from(completed) == "allow"
|
|
249
|
+
system_message = _system_message_from(completed)
|
|
250
|
+
assert system_message is not None
|
|
251
|
+
assert "utilize" in system_message
|
|
162
252
|
|
|
163
253
|
|
|
164
254
|
def test_clean_ask_user_question_passes_through() -> None:
|
|
@@ -178,7 +268,7 @@ def test_clean_ask_user_question_passes_through() -> None:
|
|
|
178
268
|
assert _decision_from(completed) is None
|
|
179
269
|
|
|
180
270
|
|
|
181
|
-
def
|
|
271
|
+
def test_write_markdown_with_banned_term_is_advisory_allow(tmp_path: Path) -> None:
|
|
182
272
|
target = tmp_path / "notes.md"
|
|
183
273
|
payload = {
|
|
184
274
|
"tool_name": "Write",
|
|
@@ -188,7 +278,10 @@ def test_write_markdown_with_banned_term_is_denied(tmp_path: Path) -> None:
|
|
|
188
278
|
},
|
|
189
279
|
}
|
|
190
280
|
completed = _run_hook_with_payload(payload)
|
|
191
|
-
assert _decision_from(completed) == "
|
|
281
|
+
assert _decision_from(completed) == "allow"
|
|
282
|
+
system_message = _system_message_from(completed)
|
|
283
|
+
assert system_message is not None
|
|
284
|
+
assert "utilize" in system_message
|
|
192
285
|
|
|
193
286
|
|
|
194
287
|
def test_write_markdown_at_ephemeral_path_is_allowed(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
@@ -230,7 +323,7 @@ def test_edit_markdown_clean_content_passes_through(tmp_path: Path) -> None:
|
|
|
230
323
|
assert _decision_from(completed) is None
|
|
231
324
|
|
|
232
325
|
|
|
233
|
-
def
|
|
326
|
+
def test_multiedit_markdown_with_banned_term_is_advisory_allow(tmp_path: Path) -> None:
|
|
234
327
|
target = tmp_path / "notes.md"
|
|
235
328
|
payload = {
|
|
236
329
|
"tool_name": "MultiEdit",
|
|
@@ -243,7 +336,10 @@ def test_multiedit_markdown_with_banned_term_is_denied(tmp_path: Path) -> None:
|
|
|
243
336
|
},
|
|
244
337
|
}
|
|
245
338
|
completed = _run_hook_with_payload(payload)
|
|
246
|
-
assert _decision_from(completed) == "
|
|
339
|
+
assert _decision_from(completed) == "allow"
|
|
340
|
+
system_message = _system_message_from(completed)
|
|
341
|
+
assert system_message is not None
|
|
342
|
+
assert "utilize" in system_message
|
|
247
343
|
|
|
248
344
|
|
|
249
345
|
def test_other_tool_is_ignored() -> None:
|
|
@@ -270,19 +366,24 @@ def test_real_file_path_is_still_stripped() -> None:
|
|
|
270
366
|
|
|
271
367
|
|
|
272
368
|
def test_native_dispatch_path_logs_the_block(tmp_path: Path) -> None:
|
|
273
|
-
"""A deny routed through the dispatcher's native path logs
|
|
369
|
+
"""A lean-block deny routed through the dispatcher's native path logs once.
|
|
274
370
|
|
|
275
371
|
On the Write|Edit|MultiEdit surface this hook runs only through
|
|
276
372
|
pre_tool_use_dispatcher's native path, which calls evaluate() and
|
|
277
|
-
build_deny_payload() — never _emit_deny() or main().
|
|
278
|
-
|
|
279
|
-
|
|
373
|
+
build_deny_payload() — never _emit_deny() or main(). Heavy words no longer
|
|
374
|
+
deny (OP-07D), so this probe uses AskUserQuestion lean-block chat detail.
|
|
375
|
+
The log call still lives on build_deny_payload.
|
|
280
376
|
"""
|
|
281
377
|
deny_payload = {
|
|
282
|
-
"tool_name": "
|
|
378
|
+
"tool_name": "AskUserQuestion",
|
|
283
379
|
"tool_input": {
|
|
284
|
-
"
|
|
285
|
-
|
|
380
|
+
"questions": [
|
|
381
|
+
{
|
|
382
|
+
"question": "Which gate should run first?\n\nPlan the whole stack.",
|
|
383
|
+
"header": "Gate",
|
|
384
|
+
"options": [{"label": "Write", "description": "Runs on write."}],
|
|
385
|
+
}
|
|
386
|
+
]
|
|
286
387
|
},
|
|
287
388
|
}
|
|
288
389
|
native_hook = NativeHook(
|
|
@@ -291,7 +392,9 @@ def test_native_dispatch_path_logs_the_block(tmp_path: Path) -> None:
|
|
|
291
392
|
)
|
|
292
393
|
|
|
293
394
|
with patch.object(Path, "home", return_value=tmp_path):
|
|
294
|
-
hosted_result = run_native_hook(
|
|
395
|
+
hosted_result = run_native_hook(
|
|
396
|
+
native_hook, deny_payload, is_blocking=True
|
|
397
|
+
)
|
|
295
398
|
|
|
296
399
|
assert hosted_result.captured_stdout
|
|
297
400
|
log_path = tmp_path / ".claude" / "logs" / "hook-blocks.log"
|
|
@@ -527,7 +630,14 @@ def test_markdown_write_keeps_its_bullets_and_tables(tmp_path: Path) -> None:
|
|
|
527
630
|
"tool_input": {"file_path": target_path, "content": bulleted_prose},
|
|
528
631
|
}
|
|
529
632
|
|
|
530
|
-
assert evaluate(heavy_payload) is
|
|
633
|
+
assert evaluate(heavy_payload) is None
|
|
634
|
+
with patch.dict(os.environ, {"CLAUDE_PROSE_STYLE_ENFORCEMENT": "1"}):
|
|
635
|
+
_deny_reason, advisory_message = hook_module.evaluate_with_advisory(
|
|
636
|
+
heavy_payload
|
|
637
|
+
)
|
|
638
|
+
assert _deny_reason is None
|
|
639
|
+
assert advisory_message is not None
|
|
640
|
+
assert "utilize" in advisory_message
|
|
531
641
|
assert evaluate(clean_payload) is None
|
|
532
642
|
|
|
533
643
|
|