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
|
@@ -13,14 +13,22 @@ equivalence holds across both outcomes.
|
|
|
13
13
|
from __future__ import annotations
|
|
14
14
|
|
|
15
15
|
import json
|
|
16
|
+
import os
|
|
16
17
|
import subprocess
|
|
17
18
|
import sys
|
|
18
19
|
from pathlib import Path
|
|
20
|
+
from unittest.mock import patch
|
|
21
|
+
|
|
22
|
+
import pytest
|
|
19
23
|
|
|
20
24
|
_HOOKS_DIR = str(Path(__file__).resolve().parent.parent)
|
|
21
25
|
if _HOOKS_DIR not in sys.path:
|
|
22
26
|
sys.path.insert(0, _HOOKS_DIR)
|
|
23
27
|
|
|
28
|
+
_PROSE_STYLE_ENFORCEMENT_ENVIRONMENT = {
|
|
29
|
+
"CLAUDE_PROSE_STYLE_ENFORCEMENT": "1",
|
|
30
|
+
}
|
|
31
|
+
|
|
24
32
|
from hooks_constants.pre_tool_use_dispatcher_constants import ( # noqa: E402, I001
|
|
25
33
|
DENY_DECISION,
|
|
26
34
|
EDIT_TOOL_NAME,
|
|
@@ -43,6 +51,19 @@ _STATE_DESCRIPTION_DENY_CONTENT = "# Guide\n\nPreviously the system used port 80
|
|
|
43
51
|
_PLAIN_LANGUAGE_ALLOW_CONTENT = "# Guide\n\nStart the build to make the report.\n"
|
|
44
52
|
_PLAIN_LANGUAGE_DENY_CONTENT = "# Guide\n\nUtilize this to commence the process.\n"
|
|
45
53
|
|
|
54
|
+
_PROSE_STYLE_ENV_VAR = "CLAUDE_PROSE_STYLE_ENFORCEMENT"
|
|
55
|
+
_PROSE_STYLE_ENV_VALUE = "1"
|
|
56
|
+
|
|
57
|
+
@pytest.fixture(autouse=True)
|
|
58
|
+
def enable_prose_style_enforcement(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
59
|
+
"""Opinionated prose gates are default-off; native-equivalence tests arm them."""
|
|
60
|
+
monkeypatch.setenv(_PROSE_STYLE_ENV_VAR, _PROSE_STYLE_ENV_VALUE)
|
|
61
|
+
|
|
62
|
+
def _subprocess_environment() -> dict[str, str]:
|
|
63
|
+
"""Return process env with opinionated prose gates enabled."""
|
|
64
|
+
environment_by_key = os.environ.copy()
|
|
65
|
+
environment_by_key[_PROSE_STYLE_ENV_VAR] = _PROSE_STYLE_ENV_VALUE
|
|
66
|
+
return environment_by_key
|
|
46
67
|
|
|
47
68
|
def _write_payload_dictionary(file_path: str, content: str) -> dict[str, object]:
|
|
48
69
|
"""Build a Write tool payload dict.
|
|
@@ -59,7 +80,6 @@ def _write_payload_dictionary(file_path: str, content: str) -> dict[str, object]
|
|
|
59
80
|
"tool_input": {"file_path": file_path, "content": content},
|
|
60
81
|
}
|
|
61
82
|
|
|
62
|
-
|
|
63
83
|
def _edit_payload_dictionary(file_path: str, new_string: str) -> dict[str, object]:
|
|
64
84
|
"""Build an Edit tool payload dict.
|
|
65
85
|
|
|
@@ -79,7 +99,6 @@ def _edit_payload_dictionary(file_path: str, new_string: str) -> dict[str, objec
|
|
|
79
99
|
},
|
|
80
100
|
}
|
|
81
101
|
|
|
82
|
-
|
|
83
102
|
def _multi_edit_payload_dictionary(file_path: str, new_string: str) -> dict[str, object]:
|
|
84
103
|
"""Build a MultiEdit tool payload dict.
|
|
85
104
|
|
|
@@ -99,6 +118,14 @@ def _multi_edit_payload_dictionary(file_path: str, new_string: str) -> dict[str,
|
|
|
99
118
|
}
|
|
100
119
|
|
|
101
120
|
|
|
121
|
+
def _evaluate_with_prose_style_opt_in(
|
|
122
|
+
evaluate_callable: object, payload_dictionary: dict[str, object]
|
|
123
|
+
) -> str | None:
|
|
124
|
+
"""Call a native evaluate with opinionated prose enforcement enabled."""
|
|
125
|
+
with patch.dict(os.environ, _PROSE_STYLE_ENFORCEMENT_ENVIRONMENT):
|
|
126
|
+
return evaluate_callable(payload_dictionary) # type: ignore[operator]
|
|
127
|
+
|
|
128
|
+
|
|
102
129
|
def _run_script_subprocess(script_path: str, payload_dictionary: dict[str, object]) -> str:
|
|
103
130
|
"""Run a hook script as a subprocess and return its stripped stdout.
|
|
104
131
|
|
|
@@ -116,10 +143,10 @@ def _run_script_subprocess(script_path: str, payload_dictionary: dict[str, objec
|
|
|
116
143
|
capture_output=True,
|
|
117
144
|
text=True,
|
|
118
145
|
encoding="utf-8",
|
|
146
|
+
env=_subprocess_environment(),
|
|
119
147
|
)
|
|
120
148
|
return completed_process.stdout.strip()
|
|
121
149
|
|
|
122
|
-
|
|
123
150
|
def _deny_reason_from_script_stdout(stdout_text: str) -> str | None:
|
|
124
151
|
"""Parse a script's stdout into its deny-reason text, or None for allow.
|
|
125
152
|
|
|
@@ -139,7 +166,6 @@ def _deny_reason_from_script_stdout(stdout_text: str) -> str | None:
|
|
|
139
166
|
reason_text = hook_specific.get("permissionDecisionReason", "")
|
|
140
167
|
return reason_text if isinstance(reason_text, str) else None
|
|
141
168
|
|
|
142
|
-
|
|
143
169
|
def _deny_reason_from_dispatcher(payload_dictionary: dict[str, object]) -> str | None:
|
|
144
170
|
"""Run the dispatcher as a subprocess and return its deny-reason text.
|
|
145
171
|
|
|
@@ -157,10 +183,10 @@ def _deny_reason_from_dispatcher(payload_dictionary: dict[str, object]) -> str |
|
|
|
157
183
|
capture_output=True,
|
|
158
184
|
text=True,
|
|
159
185
|
encoding="utf-8",
|
|
186
|
+
env=_subprocess_environment(),
|
|
160
187
|
)
|
|
161
188
|
return _deny_reason_from_script_stdout(completed_process.stdout.strip())
|
|
162
189
|
|
|
163
|
-
|
|
164
190
|
def _deny_payload_from_dispatcher(payload_dictionary: dict[str, object]) -> dict[str, object]:
|
|
165
191
|
"""Run the dispatcher as a subprocess and return its parsed deny payload.
|
|
166
192
|
|
|
@@ -177,145 +203,157 @@ def _deny_payload_from_dispatcher(payload_dictionary: dict[str, object]) -> dict
|
|
|
177
203
|
capture_output=True,
|
|
178
204
|
text=True,
|
|
179
205
|
encoding="utf-8",
|
|
206
|
+
env=_subprocess_environment(),
|
|
180
207
|
)
|
|
181
208
|
parsed_payload = json.loads(completed_process.stdout.strip())
|
|
182
209
|
assert isinstance(parsed_payload, dict)
|
|
183
210
|
return parsed_payload
|
|
184
211
|
|
|
185
|
-
|
|
186
212
|
def test_state_description_native_allows_match_script() -> None:
|
|
187
213
|
"""state_description_blocker native allow matches the script's allow."""
|
|
188
214
|
payload_dictionary = _write_payload_dictionary(_MARKDOWN_PATH, _STATE_DESCRIPTION_ALLOW_CONTENT)
|
|
189
|
-
native_reason = state_description_blocker.evaluate
|
|
215
|
+
native_reason = _evaluate_with_prose_style_opt_in(state_description_blocker.evaluate, payload_dictionary)
|
|
190
216
|
script_stdout = _run_script_subprocess(_STATE_DESCRIPTION_SCRIPT, payload_dictionary)
|
|
191
217
|
script_reason = _deny_reason_from_script_stdout(script_stdout)
|
|
192
218
|
assert native_reason is None
|
|
193
219
|
assert script_reason is None
|
|
194
220
|
|
|
195
|
-
|
|
196
221
|
def test_state_description_native_deny_matches_script_reason() -> None:
|
|
197
222
|
"""state_description_blocker native deny reason matches the script's reason."""
|
|
198
223
|
payload_dictionary = _write_payload_dictionary(_MARKDOWN_PATH, _STATE_DESCRIPTION_DENY_CONTENT)
|
|
199
|
-
native_reason = state_description_blocker.evaluate
|
|
224
|
+
native_reason = _evaluate_with_prose_style_opt_in(state_description_blocker.evaluate, payload_dictionary)
|
|
200
225
|
script_stdout = _run_script_subprocess(_STATE_DESCRIPTION_SCRIPT, payload_dictionary)
|
|
201
226
|
script_reason = _deny_reason_from_script_stdout(script_stdout)
|
|
202
227
|
assert native_reason is not None
|
|
203
228
|
assert native_reason == script_reason
|
|
204
229
|
|
|
205
|
-
|
|
206
230
|
def test_state_description_native_edit_deny_matches_script_reason() -> None:
|
|
207
231
|
"""state_description_blocker native and script agree on an Edit denial."""
|
|
208
232
|
payload_dictionary = _edit_payload_dictionary(
|
|
209
233
|
_MARKDOWN_PATH, "Previously this used the old client.\n"
|
|
210
234
|
)
|
|
211
|
-
native_reason = state_description_blocker.evaluate
|
|
235
|
+
native_reason = _evaluate_with_prose_style_opt_in(state_description_blocker.evaluate, payload_dictionary)
|
|
212
236
|
script_stdout = _run_script_subprocess(_STATE_DESCRIPTION_SCRIPT, payload_dictionary)
|
|
213
237
|
script_reason = _deny_reason_from_script_stdout(script_stdout)
|
|
214
238
|
assert native_reason is not None
|
|
215
239
|
assert native_reason == script_reason
|
|
216
240
|
|
|
217
|
-
|
|
218
241
|
def test_state_description_native_non_target_tool_allows_match_script() -> None:
|
|
219
242
|
"""state_description_blocker native allows MultiEdit, matching the script."""
|
|
220
243
|
payload_dictionary = _multi_edit_payload_dictionary(
|
|
221
244
|
_MARKDOWN_PATH, _STATE_DESCRIPTION_DENY_CONTENT
|
|
222
245
|
)
|
|
223
|
-
native_reason = state_description_blocker.evaluate
|
|
246
|
+
native_reason = _evaluate_with_prose_style_opt_in(state_description_blocker.evaluate, payload_dictionary)
|
|
224
247
|
script_stdout = _run_script_subprocess(_STATE_DESCRIPTION_SCRIPT, payload_dictionary)
|
|
225
248
|
script_reason = _deny_reason_from_script_stdout(script_stdout)
|
|
226
249
|
assert native_reason is None
|
|
227
250
|
assert script_reason is None
|
|
228
251
|
|
|
229
|
-
|
|
230
252
|
def test_plain_language_native_allows_match_script() -> None:
|
|
231
253
|
"""plain_language_blocker native allow matches the script's allow."""
|
|
232
254
|
payload_dictionary = _write_payload_dictionary(_MARKDOWN_PATH, _PLAIN_LANGUAGE_ALLOW_CONTENT)
|
|
233
|
-
native_reason = plain_language_blocker.evaluate
|
|
255
|
+
native_reason = _evaluate_with_prose_style_opt_in(plain_language_blocker.evaluate, payload_dictionary)
|
|
234
256
|
script_stdout = _run_script_subprocess(_PLAIN_LANGUAGE_SCRIPT, payload_dictionary)
|
|
235
257
|
script_reason = _deny_reason_from_script_stdout(script_stdout)
|
|
236
258
|
assert native_reason is None
|
|
237
259
|
assert script_reason is None
|
|
238
260
|
|
|
239
261
|
|
|
240
|
-
def
|
|
241
|
-
"""plain_language_blocker native
|
|
262
|
+
def test_plain_language_native_heavy_word_advisory_matches_script() -> None:
|
|
263
|
+
"""plain_language_blocker native and script agree: no deny, allow advisory."""
|
|
242
264
|
payload_dictionary = _write_payload_dictionary(_MARKDOWN_PATH, _PLAIN_LANGUAGE_DENY_CONTENT)
|
|
243
|
-
|
|
265
|
+
with patch.dict(os.environ, _PROSE_STYLE_ENFORCEMENT_ENVIRONMENT):
|
|
266
|
+
native_deny, native_advisory = plain_language_blocker.evaluate_with_advisory(
|
|
267
|
+
payload_dictionary
|
|
268
|
+
)
|
|
244
269
|
script_stdout = _run_script_subprocess(_PLAIN_LANGUAGE_SCRIPT, payload_dictionary)
|
|
245
270
|
script_reason = _deny_reason_from_script_stdout(script_stdout)
|
|
246
|
-
assert
|
|
247
|
-
assert
|
|
271
|
+
assert native_deny is None
|
|
272
|
+
assert script_reason is None
|
|
273
|
+
assert native_advisory is not None
|
|
274
|
+
assert "utilize" in native_advisory.lower() or "commence" in native_advisory.lower()
|
|
275
|
+
script_payload = json.loads(script_stdout)
|
|
276
|
+
assert script_payload["hookSpecificOutput"]["permissionDecision"] == "allow"
|
|
277
|
+
assert native_advisory in script_payload["systemMessage"]
|
|
248
278
|
|
|
249
279
|
|
|
250
|
-
def
|
|
251
|
-
"""plain_language_blocker native and script agree on
|
|
280
|
+
def test_plain_language_native_multi_edit_heavy_word_advisory_matches_script() -> None:
|
|
281
|
+
"""plain_language_blocker native and script agree on MultiEdit heavy-word allow."""
|
|
252
282
|
payload_dictionary = _multi_edit_payload_dictionary(
|
|
253
283
|
_MARKDOWN_PATH, "Utilize this to commence the process.\n"
|
|
254
284
|
)
|
|
255
|
-
|
|
285
|
+
with patch.dict(os.environ, _PROSE_STYLE_ENFORCEMENT_ENVIRONMENT):
|
|
286
|
+
native_deny, native_advisory = plain_language_blocker.evaluate_with_advisory(
|
|
287
|
+
payload_dictionary
|
|
288
|
+
)
|
|
256
289
|
script_stdout = _run_script_subprocess(_PLAIN_LANGUAGE_SCRIPT, payload_dictionary)
|
|
257
290
|
script_reason = _deny_reason_from_script_stdout(script_stdout)
|
|
258
|
-
assert
|
|
259
|
-
assert
|
|
260
|
-
|
|
291
|
+
assert native_deny is None
|
|
292
|
+
assert script_reason is None
|
|
293
|
+
assert native_advisory is not None
|
|
294
|
+
script_payload = json.loads(script_stdout)
|
|
295
|
+
assert script_payload["hookSpecificOutput"]["permissionDecision"] == "allow"
|
|
296
|
+
assert native_advisory in script_payload["systemMessage"]
|
|
261
297
|
|
|
262
298
|
def test_plain_language_native_non_markdown_allows_match_script() -> None:
|
|
263
299
|
"""plain_language_blocker native allows a non-markdown Write, matching the script."""
|
|
264
300
|
payload_dictionary = _write_payload_dictionary(_PYTHON_PATH, _PLAIN_LANGUAGE_DENY_CONTENT)
|
|
265
|
-
native_reason = plain_language_blocker.evaluate
|
|
301
|
+
native_reason = _evaluate_with_prose_style_opt_in(plain_language_blocker.evaluate, payload_dictionary)
|
|
266
302
|
script_stdout = _run_script_subprocess(_PLAIN_LANGUAGE_SCRIPT, payload_dictionary)
|
|
267
303
|
script_reason = _deny_reason_from_script_stdout(script_stdout)
|
|
268
304
|
assert native_reason is None
|
|
269
305
|
assert script_reason is None
|
|
270
306
|
|
|
271
|
-
|
|
272
307
|
def test_dispatcher_native_path_denies_state_description() -> None:
|
|
273
308
|
"""The dispatcher's native path denies a state_description_blocker violation."""
|
|
274
309
|
payload_dictionary = _write_payload_dictionary(_MARKDOWN_PATH, _STATE_DESCRIPTION_DENY_CONTENT)
|
|
275
|
-
native_reason = state_description_blocker.evaluate
|
|
310
|
+
native_reason = _evaluate_with_prose_style_opt_in(state_description_blocker.evaluate, payload_dictionary)
|
|
276
311
|
dispatcher_reason = _deny_reason_from_dispatcher(payload_dictionary)
|
|
277
312
|
assert native_reason is not None
|
|
278
313
|
assert dispatcher_reason is not None
|
|
279
314
|
assert native_reason in dispatcher_reason
|
|
280
315
|
|
|
281
316
|
|
|
282
|
-
def
|
|
283
|
-
"""The dispatcher's native path
|
|
317
|
+
def test_dispatcher_native_path_allows_plain_language_heavy_word() -> None:
|
|
318
|
+
"""The dispatcher's native path allows heavy words and does not deny."""
|
|
284
319
|
payload_dictionary = _multi_edit_payload_dictionary(
|
|
285
320
|
_MARKDOWN_PATH, "Utilize this to commence the process.\n"
|
|
286
321
|
)
|
|
287
|
-
native_reason =
|
|
322
|
+
native_reason = _evaluate_with_prose_style_opt_in(
|
|
323
|
+
plain_language_blocker.evaluate, payload_dictionary
|
|
324
|
+
)
|
|
288
325
|
dispatcher_reason = _deny_reason_from_dispatcher(payload_dictionary)
|
|
289
|
-
assert native_reason is
|
|
290
|
-
assert dispatcher_reason is
|
|
291
|
-
assert native_reason in dispatcher_reason
|
|
326
|
+
assert native_reason is None
|
|
327
|
+
assert dispatcher_reason is None
|
|
292
328
|
|
|
293
329
|
|
|
294
|
-
def
|
|
295
|
-
"""The dispatcher's plain-language
|
|
330
|
+
def test_dispatcher_native_plain_language_carries_advisory_system_message() -> None:
|
|
331
|
+
"""The dispatcher's plain-language allow carries the advisory systemMessage."""
|
|
296
332
|
payload_dictionary = _write_payload_dictionary(_MARKDOWN_PATH, _PLAIN_LANGUAGE_DENY_CONTENT)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
assert
|
|
333
|
+
with patch.dict(os.environ, _PROSE_STYLE_ENFORCEMENT_ENVIRONMENT):
|
|
334
|
+
deny_reason, advisory_message = plain_language_blocker.evaluate_with_advisory(
|
|
335
|
+
payload_dictionary
|
|
336
|
+
)
|
|
337
|
+
assert deny_reason is None
|
|
338
|
+
assert advisory_message is not None
|
|
302
339
|
dispatcher_payload = _deny_payload_from_dispatcher(payload_dictionary)
|
|
340
|
+
assert dispatcher_payload["hookSpecificOutput"]["permissionDecision"] == "allow"
|
|
303
341
|
dispatcher_system_message = dispatcher_payload.get("systemMessage")
|
|
304
342
|
assert isinstance(dispatcher_system_message, str)
|
|
305
|
-
assert
|
|
343
|
+
assert advisory_message in dispatcher_system_message
|
|
306
344
|
|
|
307
345
|
|
|
308
|
-
def
|
|
309
|
-
"""The dispatcher's plain-language
|
|
346
|
+
def test_dispatcher_native_plain_language_allow_does_not_suppress_output() -> None:
|
|
347
|
+
"""The dispatcher's plain-language advisory allow leaves suppressOutput off."""
|
|
310
348
|
payload_dictionary = _write_payload_dictionary(_MARKDOWN_PATH, _PLAIN_LANGUAGE_DENY_CONTENT)
|
|
311
349
|
dispatcher_payload = _deny_payload_from_dispatcher(payload_dictionary)
|
|
312
|
-
assert dispatcher_payload.get("suppressOutput") is True
|
|
313
|
-
|
|
350
|
+
assert dispatcher_payload.get("suppressOutput") is not True
|
|
351
|
+
assert dispatcher_payload["hookSpecificOutput"]["permissionDecision"] == "allow"
|
|
314
352
|
|
|
315
353
|
def test_dispatcher_native_state_description_carries_additional_context() -> None:
|
|
316
354
|
"""The dispatcher's state-description deny carries the standalone additionalContext."""
|
|
317
355
|
payload_dictionary = _write_payload_dictionary(_MARKDOWN_PATH, _STATE_DESCRIPTION_DENY_CONTENT)
|
|
318
|
-
deny_reason = state_description_blocker.evaluate
|
|
356
|
+
deny_reason = _evaluate_with_prose_style_opt_in(state_description_blocker.evaluate, payload_dictionary)
|
|
319
357
|
assert deny_reason is not None
|
|
320
358
|
standalone_payload = state_description_blocker.build_deny_payload(deny_reason)
|
|
321
359
|
standalone_hook_specific = standalone_payload["hookSpecificOutput"]
|
|
@@ -326,11 +364,10 @@ def test_dispatcher_native_state_description_carries_additional_context() -> Non
|
|
|
326
364
|
assert isinstance(dispatcher_hook_specific, dict)
|
|
327
365
|
assert dispatcher_hook_specific.get("additionalContext") == expected_additional_context
|
|
328
366
|
|
|
329
|
-
|
|
330
367
|
def test_dispatcher_native_state_description_carries_system_message() -> None:
|
|
331
368
|
"""The dispatcher's state-description deny carries the standalone systemMessage."""
|
|
332
369
|
payload_dictionary = _write_payload_dictionary(_MARKDOWN_PATH, _STATE_DESCRIPTION_DENY_CONTENT)
|
|
333
|
-
deny_reason = state_description_blocker.evaluate
|
|
370
|
+
deny_reason = _evaluate_with_prose_style_opt_in(state_description_blocker.evaluate, payload_dictionary)
|
|
334
371
|
assert deny_reason is not None
|
|
335
372
|
standalone_payload = state_description_blocker.build_deny_payload(deny_reason)
|
|
336
373
|
expected_system_message = standalone_payload["systemMessage"]
|
|
@@ -19,8 +19,8 @@ if _HOOKS_ROOT not in sys.path:
|
|
|
19
19
|
from pre_tool_use_dispatcher import NativeHook, run_native_hook # noqa: E402
|
|
20
20
|
from state_description_blocker import ( # noqa: E402
|
|
21
21
|
build_deny_payload,
|
|
22
|
-
evaluate,
|
|
23
22
|
)
|
|
23
|
+
from state_description_blocker import evaluate as _evaluate_without_opt_in # noqa: E402
|
|
24
24
|
|
|
25
25
|
HOOK_SCRIPT_PATH = os.path.join(
|
|
26
26
|
os.path.dirname(__file__), "state_description_blocker.py"
|
|
@@ -39,23 +39,66 @@ VIOLATION_MD_PREVIOUSLY = "# Config\n\nPreviously set via env var."
|
|
|
39
39
|
VIOLATION_MD_NOW_USES = "# Auth\n\nNow uses OAuth2."
|
|
40
40
|
|
|
41
41
|
|
|
42
|
+
def evaluate(payload_by_key: dict[str, object]) -> str | None:
|
|
43
|
+
"""Call evaluate with opinionated prose enforcement enabled."""
|
|
44
|
+
with patch.dict(os.environ, {"CLAUDE_PROSE_STYLE_ENFORCEMENT": "1"}):
|
|
45
|
+
return _evaluate_without_opt_in(payload_by_key)
|
|
46
|
+
|
|
47
|
+
|
|
42
48
|
class _RunHook:
|
|
43
49
|
"""Helper to test the hook via subprocess."""
|
|
44
50
|
|
|
45
|
-
def __call__(
|
|
51
|
+
def __call__(
|
|
52
|
+
self,
|
|
53
|
+
tool_name: str,
|
|
54
|
+
tool_input: dict,
|
|
55
|
+
*,
|
|
56
|
+
is_prose_style_enabled: bool = True,
|
|
57
|
+
) -> subprocess.CompletedProcess:
|
|
46
58
|
payload = json.dumps({"tool_name": tool_name, "tool_input": tool_input})
|
|
59
|
+
environment_by_key = os.environ.copy()
|
|
60
|
+
if is_prose_style_enabled:
|
|
61
|
+
environment_by_key["CLAUDE_PROSE_STYLE_ENFORCEMENT"] = "1"
|
|
62
|
+
else:
|
|
63
|
+
environment_by_key.pop("CLAUDE_PROSE_STYLE_ENFORCEMENT", None)
|
|
47
64
|
return subprocess.run(
|
|
48
65
|
[sys.executable, HOOK_SCRIPT_PATH],
|
|
49
66
|
input=payload,
|
|
50
67
|
capture_output=True,
|
|
51
68
|
text=True,
|
|
52
69
|
check=False,
|
|
70
|
+
env=environment_by_key,
|
|
53
71
|
)
|
|
54
72
|
|
|
55
73
|
|
|
56
74
|
_run_hook = _RunHook()
|
|
57
75
|
|
|
58
76
|
|
|
77
|
+
def test_historical_phrase_scan_is_default_off(
|
|
78
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
79
|
+
) -> None:
|
|
80
|
+
monkeypatch.delenv("CLAUDE_PROSE_STYLE_ENFORCEMENT", raising=False)
|
|
81
|
+
result = _run_hook(
|
|
82
|
+
"Write",
|
|
83
|
+
{
|
|
84
|
+
"file_path": "src/main.py",
|
|
85
|
+
"content": VIOLATION_INSTEAD_OF_COMMENT,
|
|
86
|
+
},
|
|
87
|
+
is_prose_style_enabled=False,
|
|
88
|
+
)
|
|
89
|
+
assert result.returncode == 0
|
|
90
|
+
assert result.stdout == ""
|
|
91
|
+
assert _evaluate_without_opt_in(
|
|
92
|
+
{
|
|
93
|
+
"tool_name": "Write",
|
|
94
|
+
"tool_input": {
|
|
95
|
+
"file_path": "src/main.py",
|
|
96
|
+
"content": VIOLATION_INSTEAD_OF_COMMENT,
|
|
97
|
+
},
|
|
98
|
+
}
|
|
99
|
+
) is None
|
|
100
|
+
|
|
101
|
+
|
|
59
102
|
def test_block_clean_python_comment_passes():
|
|
60
103
|
result = _run_hook(
|
|
61
104
|
"Write",
|
|
@@ -156,9 +156,10 @@ def test_dispatcher_blocks_hedging_message_matching_standalone() -> None:
|
|
|
156
156
|
payload_text = json.dumps(
|
|
157
157
|
{
|
|
158
158
|
"stop_hook_active": False,
|
|
159
|
-
"last_assistant_message": "This is probably correct
|
|
159
|
+
"last_assistant_message": "This is probably correct.",
|
|
160
160
|
}
|
|
161
161
|
)
|
|
162
|
+
environment_by_key = {**os.environ, "CLAUDE_PROSE_STYLE_ENFORCEMENT": "1"}
|
|
162
163
|
completed = subprocess.run(
|
|
163
164
|
[sys.executable, _DISPATCHER_SCRIPT],
|
|
164
165
|
check=False,
|
|
@@ -166,6 +167,7 @@ def test_dispatcher_blocks_hedging_message_matching_standalone() -> None:
|
|
|
166
167
|
capture_output=True,
|
|
167
168
|
text=True,
|
|
168
169
|
encoding="utf-8",
|
|
170
|
+
env=environment_by_key,
|
|
169
171
|
)
|
|
170
172
|
assert completed.returncode == 0
|
|
171
173
|
parsed = json.loads(completed.stdout)
|
|
@@ -173,14 +175,16 @@ def test_dispatcher_blocks_hedging_message_matching_standalone() -> None:
|
|
|
173
175
|
assert "probably" in parsed["reason"].lower() or "hedging" in parsed["reason"].lower()
|
|
174
176
|
|
|
175
177
|
|
|
176
|
-
def
|
|
177
|
-
"""An
|
|
178
|
-
|
|
179
|
-
"
|
|
180
|
-
for _ in range(30)
|
|
178
|
+
def test_dispatcher_blocks_eli11_shape_violation_matching_standalone() -> None:
|
|
179
|
+
"""An ELI11 shape violation (too many bullets) blocks through the dispatcher."""
|
|
180
|
+
too_many_bullets_message = "\n".join(
|
|
181
|
+
f"- finding{each_index} detail for this line" for each_index in range(7)
|
|
181
182
|
)
|
|
182
183
|
payload_text = json.dumps(
|
|
183
|
-
{
|
|
184
|
+
{
|
|
185
|
+
"stop_hook_active": False,
|
|
186
|
+
"last_assistant_message": too_many_bullets_message,
|
|
187
|
+
}
|
|
184
188
|
)
|
|
185
189
|
completed = subprocess.run(
|
|
186
190
|
[sys.executable, _DISPATCHER_SCRIPT],
|
|
@@ -31,13 +31,13 @@ def _body_names_volatile_path(tool_name: str, tool_input: dict[str, object]) ->
|
|
|
31
31
|
|
|
32
32
|
|
|
33
33
|
def test_scan_detects_job_scratch_path_backslash() -> None:
|
|
34
|
-
text = r"See C:\Users\example\.claude-
|
|
35
|
-
assert scan_text_for_volatile_marker(text) == ".claude-
|
|
34
|
+
text = r"See C:\Users\example\.claude-profile-a\jobs\95762cea\tmp\staging\contact_sheet.png"
|
|
35
|
+
assert scan_text_for_volatile_marker(text) == ".claude-profile-a/jobs/"
|
|
36
36
|
|
|
37
37
|
|
|
38
38
|
def test_scan_detects_job_scratch_path_forward_slash() -> None:
|
|
39
|
-
text = "artifact at /home/user/.claude-
|
|
40
|
-
assert scan_text_for_volatile_marker(text) == ".claude-
|
|
39
|
+
text = "artifact at /home/user/.claude-profile-a/jobs/abc/tmp/out.png"
|
|
40
|
+
assert scan_text_for_volatile_marker(text) == ".claude-profile-a/jobs/"
|
|
41
41
|
|
|
42
42
|
|
|
43
43
|
def test_scan_detects_worktree_path() -> None:
|
|
@@ -92,12 +92,12 @@ def test_scan_home_anchored_worktree_path_is_detected() -> None:
|
|
|
92
92
|
|
|
93
93
|
|
|
94
94
|
def test_scan_posix_absolute_job_scratch_path_is_detected() -> None:
|
|
95
|
-
text = "/home/me/.claude-
|
|
96
|
-
assert scan_text_for_volatile_marker(text) == ".claude-
|
|
95
|
+
text = "/home/me/.claude-profile-a/jobs/j1/log.txt"
|
|
96
|
+
assert scan_text_for_volatile_marker(text) == ".claude-profile-a/jobs/"
|
|
97
97
|
|
|
98
98
|
|
|
99
99
|
def test_scan_bare_parenthesized_job_scratch_mention_is_allowed() -> None:
|
|
100
|
-
text = "(.claude-
|
|
100
|
+
text = "(.claude-profile-a/jobs/) as a directory name in prose"
|
|
101
101
|
assert scan_text_for_volatile_marker(text) is None
|
|
102
102
|
|
|
103
103
|
|
|
@@ -139,8 +139,8 @@ def test_scan_markdown_link_relative_worktree_target_is_detected() -> None:
|
|
|
139
139
|
|
|
140
140
|
|
|
141
141
|
def test_scan_space_prefixed_relative_job_scratch_path_is_detected() -> None:
|
|
142
|
-
text = "see .claude-
|
|
143
|
-
assert scan_text_for_volatile_marker(text) == ".claude-
|
|
142
|
+
text = "see .claude-profile-a/jobs/j1/log.txt"
|
|
143
|
+
assert scan_text_for_volatile_marker(text) == ".claude-profile-a/jobs/"
|
|
144
144
|
|
|
145
145
|
|
|
146
146
|
def test_scan_backslash_relative_worktree_path_is_detected() -> None:
|
|
@@ -171,7 +171,7 @@ def test_scan_sentence_final_marker_period_is_allowed() -> None:
|
|
|
171
171
|
def test_gh_comment_with_job_scratch_path_is_blocked() -> None:
|
|
172
172
|
command = (
|
|
173
173
|
'gh pr comment 669 --body "Contact sheet at '
|
|
174
|
-
r'C:\Users\example\.claude-
|
|
174
|
+
r'C:\Users\example\.claude-profile-a\jobs\95762cea\tmp\staging\contact_sheet.png"'
|
|
175
175
|
)
|
|
176
176
|
assert _body_names_volatile_path("Bash", {"command": command})
|
|
177
177
|
|
|
@@ -223,7 +223,7 @@ def test_substring_mention_of_gh_post_does_not_classify() -> None:
|
|
|
223
223
|
|
|
224
224
|
|
|
225
225
|
def test_gh_word_not_first_token_does_not_classify() -> None:
|
|
226
|
-
command = r'echo gh pr comment 42 --body "see C:\.claude-
|
|
226
|
+
command = r'echo gh pr comment 42 --body "see C:\.claude-profile-a\jobs\x"'
|
|
227
227
|
assert not _body_names_volatile_path("Bash", {"command": command})
|
|
228
228
|
|
|
229
229
|
|
|
@@ -279,7 +279,7 @@ def test_gh_issue_edit_quoting_bare_constant_is_allowed(tmp_path: pathlib.Path)
|
|
|
279
279
|
body_file.write_text(
|
|
280
280
|
"The manifest skips these tooling-state prefixes:\n"
|
|
281
281
|
' ".claude/worktrees/",\n'
|
|
282
|
-
' ".claude-
|
|
282
|
+
' ".claude-profile-a/jobs/",\n'
|
|
283
283
|
"Neither is a machine-local path.",
|
|
284
284
|
encoding="utf-8",
|
|
285
285
|
)
|
|
@@ -21,7 +21,7 @@ another argument never classifies.
|
|
|
21
21
|
|
|
22
22
|
The marker scan runs in two tiers over the normalized body. Five bare markers
|
|
23
23
|
— the system temp locations and env tokens — match as a plain substring
|
|
24
|
-
anywhere. Two dot-relative directory markers, ``.claude-
|
|
24
|
+
anywhere. Two dot-relative directory markers, ``.claude-profile-a/jobs/`` and
|
|
25
25
|
``.claude/worktrees/``, match when either a slash sits immediately before the
|
|
26
26
|
marker or a path segment follows it. A machine-local absolute path
|
|
27
27
|
(``~/.claude/worktrees/wt/f.py``) and a relative path that names a child
|
|
@@ -117,7 +117,7 @@ def scan_text_for_volatile_marker(text: str) -> str | None:
|
|
|
117
117
|
markers match anywhere::
|
|
118
118
|
|
|
119
119
|
ok: "the `.claude/worktrees/` manifest key" -> None
|
|
120
|
-
flag: r"C:\\Users\\me\\.claude-
|
|
120
|
+
flag: r"C:\\Users\\me\\.claude-profile-a\\jobs\\x" -> ".claude-profile-a/jobs/"
|
|
121
121
|
flag: "see .claude/worktrees/wt-1/notes.md" -> ".claude/worktrees/"
|
|
122
122
|
flag: "saved to %TEMP%\\out.txt" -> "%temp%"
|
|
123
123
|
|
package/hooks/hooks.json
CHANGED
|
@@ -176,6 +176,21 @@
|
|
|
176
176
|
"type": "command",
|
|
177
177
|
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/task_list_loop_starter.py",
|
|
178
178
|
"timeout": 10
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
"type": "command",
|
|
182
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/working_style_prompt.py",
|
|
183
|
+
"timeout": 10
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"type": "command",
|
|
187
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/orchestrator_auto_starter.py",
|
|
188
|
+
"timeout": 10
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"type": "command",
|
|
192
|
+
"command": "python3 ${CLAUDE_PLUGIN_ROOT}/hooks/session/issue_tracker_session_starter.py",
|
|
193
|
+
"timeout": 10
|
|
179
194
|
}
|
|
180
195
|
]
|
|
181
196
|
}
|
|
@@ -8,6 +8,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
8
8
|
|---|---|
|
|
9
9
|
| `__init__.py` | Package marker (`# pragma: no-tdd-gate`) |
|
|
10
10
|
| `any_type_config.py` | Config for the `Any`-type escape-hatch check |
|
|
11
|
+
| `ask_user_question_shape.py` | Pure AskUserQuestion lean-block shape analyzer: grades question and option-description prose against shared chat-detail markers and sentence/word caps with no hook I/O |
|
|
11
12
|
| `banned_identifiers_constants.py` | The set of banned short identifiers and banned function-name prefixes |
|
|
12
13
|
| `bash_pre_tool_use_dispatcher_constants.py` | Permission outcomes, tool-name sets, and the ordered hosted-hook roster for the Bash PreToolUse dispatcher (also covers PowerShell-shared gates) |
|
|
13
14
|
| `blocking_check_limits.py` | Max issue counts and preview lengths for blocking hooks |
|
|
@@ -31,11 +32,12 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
31
32
|
| `duplicate_function_body_constants.py` | Hashing and comparison config for the duplicate-body check |
|
|
32
33
|
| `duplicate_rmtree_helper_blocker_constants.py` | Sanctioned Windows-safe rmtree helper names, the definition pattern, and the exempt-path set for the duplicate-rmtree-helper blocker |
|
|
33
34
|
| `dynamic_stderr_handler.py` | `DynamicStderrHandler` — a logging handler that resolves `sys.stderr` at emit time (for testability) |
|
|
34
|
-
| `eli11_reply_enforcer_constants.py` | Reply-shape thresholds (
|
|
35
|
+
| `eli11_reply_enforcer_constants.py` | Reply-shape thresholds (bullet cap, lead-line window, per-list-line word cap, overpacked list-line cap), the bullet / numbered-step / table-row / link-target / word patterns, the imperative instruction verbs, and the user-facing notice for the ELI11 reply-shape Stop hook |
|
|
35
36
|
| `fable_spawn_gate_constants.py` | The spawn tool-name set, the fable model alias and the model-id segment split pattern, the `FABLE-SPAWN-AUTHORIZED` authorization token, the advisor-protocol document path, payload field names, the deny message, and the deny `additionalContext` recovery text for the fable-tier spawn gate |
|
|
36
37
|
| `gh_pr_author_swap_constants.py` | Constants for the PR-author swap enforcement hooks |
|
|
37
38
|
| `hardcoded_user_path_constants.py` | Patterns for detecting hardcoded home-directory paths |
|
|
38
39
|
| `harness_scratchpad_constants.py` | Fixed path components (`claude` / `claude-` user-directory name and prefix, `scratchpad` leaf name), the PreToolUse session-id payload key, and the `CLAUDE_CODE_SESSION_ID` environment variable name that the code-rules and TDD gates use to recognize an existing harness session scratchpad directory |
|
|
40
|
+
| `hedging_uncertainty_constants.py` | Sentence-boundary pattern, explicit uncertainty label patterns, hedge-term list separator, and positive corrective block text for OP-07C sentence-scoped hedging |
|
|
39
41
|
| `hook_block_logger.py` | `log_hook_block()` — shared fail-safe logger every blocking hook calls to append a JSON record of each block decision to `~/.claude/logs/hook-blocks.log` |
|
|
40
42
|
| `hook_log_extractor_constants.py` | Neon table name, offset state file path, timeouts, and outcome-type mapping for the hook-log extractor |
|
|
41
43
|
| `hook_prose_detector_consistency_constants.py` | Trigger patterns and corrective messages for the hook-prose consistency checker |
|
|
@@ -54,15 +56,17 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
54
56
|
| `paired_test_coverage_constants.py` | Test-directory name, stem-test filename affixes, test-file globs, exempt public-function names, scan budget, coverage threshold, and guidance text for the public-function paired-test coverage check |
|
|
55
57
|
| `path_rewriter_constants.py` | Path rewriting patterns for the Everything-search path rewriter |
|
|
56
58
|
| `pii_prevention_constants.py` | Patterns, allowlists, path exemptions, and deny-message text for `pii_prevention_blocker` |
|
|
57
|
-
| `piped_pytest_blocker_constants.py` | Bash tool name, pytest program basenames and the python-interpreter basename pattern, the pipe and segment-reset operator token sets, and the deny message for the piped-pytest blocker |
|
|
59
|
+
| `piped_pytest_blocker_constants.py` | Bash tool name, pytest program basenames and the python-interpreter basename pattern, the pipe and segment-reset operator token sets, and the deny message for the piped-pytest blocker; classification that consumes those program and wrapper constants lives in `pytest_invocation.py` |
|
|
60
|
+
| `pytest_invocation.py` | Pure parsed-segment classifier for pytest runs through interpreters, shims, wrappers, and shell string-exec forms; no pipeline deny decision |
|
|
58
61
|
| `plain_language_blocker_constants.py` | The list of heavy words and their everyday replacements |
|
|
59
62
|
| `pr_converge_bugteam_enforcer_constants.py` | State keys and timing config for the bugteam-parallel enforcer |
|
|
60
63
|
| `pr_converge_bugteam_enforcer_state.py` | State-file helpers for the bugteam enforcer |
|
|
61
64
|
| `pr_description_enforcer_constants.py` | PR-description shape rules and command patterns |
|
|
62
65
|
| `pr_description_proof_of_work_constants.py` | Proof-part detection patterns, gh command tokens, and corrective messages for the proof-of-work comment audit |
|
|
66
|
+
| `prose_matcher_precision_constants.py` | Sample floor, keep/narrow precision floors, matcher ids, label decisions, advisory log path, emit caps, and context fingerprint length for OP-07B prose-matcher advisory telemetry |
|
|
63
67
|
| `pre_tool_use_stdin.py` | `read_hook_input_dictionary_from_stdin()` — shared stdin parser for PreToolUse hooks |
|
|
64
68
|
| `precommit_code_rules_gate_constants.py` | Scope argument and exit-code constants for the precommit gate |
|
|
65
|
-
|
|
|
69
|
+
| project_paths_reader.py | Loads ~/.claude/project-paths.json and finds a home-bounded git root for registry checks |
|
|
66
70
|
| `pyproject_config_discovery_constants.py` | Table names (``tool`` key, ``mypy``, ``ruff``) for resolving a validator's config from a pyproject.toml `[tool.<name>]` table |
|
|
67
71
|
| `pytest_testpaths_orphan_blocker_constants.py` | Marker filename, section and key names, test-file pattern, search budget, and block-message text for the pytest unregistered-test-directory blocker |
|
|
68
72
|
| `python_style_checks_constants.py` | Command-line argument count and blank-line count between top-level functions for the style validator |
|
|
@@ -72,8 +76,14 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
72
76
|
| `sensitive_file_protector_constants.py` | Sensitive filename patterns, the committed-template suffixes that earn an exemption, the write/edit tool names, and the deny decision and message template for `sensitive_file_protector` |
|
|
73
77
|
| `session_edit_stage_gate_constants.py` | Tracker filename prefix/suffix, JSON payload key, edit tool name set, session-id sanitize pattern, lock filename suffix and lock-acquire timing, git diff command, commit flag escapes, and deny-message template shared by the session edit stage gate trio |
|
|
74
78
|
| `session_env_cleanup_constants.py` | Stale-age threshold and directory names for the session-env cleanup hook |
|
|
79
|
+
| `session_start_injector.py` | Pure SessionStart context injector: normalize `source` (`startup`/`resume`/`clear`/`compact`), honor enable/timeout, return structured injection results for starter hooks |
|
|
80
|
+
| `session_start_injector_constants.py` | Known SessionStart source values, injection status codes, enable env var, default timeout for the shared injector |
|
|
81
|
+
| `test_session_start_injector.py` | Behavioral tests for every known source, disabled, timeout, and unknown-source paths |
|
|
82
|
+
| `orchestrator_auto_starter_constants.py` | Opt-in env var, enable values, timeout, and SessionStart directive text for the orchestrator auto-starter |
|
|
83
|
+
| `issue_tracker_session_starter_constants.py` | Opt-in env var, enable values, timeout, and SessionStart directive text for the repository-gated issue-tracker starter |
|
|
75
84
|
| `session_handoff_blocker_constants.py` | Trigger phrases for the session-handoff blocker |
|
|
76
85
|
| `setup_project_paths_constants.py` | Encoding policy, BOM marker, and registry meta-key used across multiple hooks |
|
|
86
|
+
| `shell_command_pipeline.py` | Quote-aware shell pipeline parsing: operator-paired segments after line-continuation join, comment strip, heredoc body drop, and parenthesis-group join |
|
|
77
87
|
| `shell_command_segments.py` | Shell tokenization helpers shared by the segment-walking blockers: segment splitting on control operators, a segment's effective leading program, and a token's basename |
|
|
78
88
|
| `shell_substitution_blocker_constants.py` | Tool name, payload keys, the four substitution-detection patterns, the deny decision shape, and the corrective message for the shell-substitution blocker |
|
|
79
89
|
| `stale_comment_reference_blocker_constants.py` | Identifier pattern, comment stopwords, and denial text for the stale-comment-reference blocker |
|
|
@@ -91,6 +101,7 @@ Shared constant modules imported by hooks throughout the `hooks/` tree. Each fil
|
|
|
91
101
|
| `volatile_path_in_post_blocker_constants.py` | Volatile path markers, affected `gh` post subcommands, MCP body param names, and the corrective message for the volatile-path post blocker |
|
|
92
102
|
| `windows_rmtree_blocker_constants.py` | The unsafe `shutil.rmtree` pattern and the safe replacement pattern |
|
|
93
103
|
| `workflow_substitution_slot_blocker_constants.py` | Per-iteration token patterns for the workflow-slot blocker |
|
|
104
|
+
| `working_style_prompt_constants.py` | The fixed multi-paragraph working-style prompt text for the SessionStart working_style_prompt hook |
|
|
94
105
|
|
|
95
106
|
## Conventions
|
|
96
107
|
|