claude-dev-env 2.9.0 → 2.10.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 +1 -1
- 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 +1 -0
- package/docs/references/advisor-tool.md +1 -1
- package/docs/references/prose-style-enforcement.md +25 -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_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/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/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/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
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Tests for the prose-style enforcement opt-in flag (default off)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.util
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
|
|
10
|
+
_CONSTANTS_PATH = Path(__file__).resolve().parent / "prose_style_enforcement_constants.py"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def _load_constants_module(monkeypatch: pytest.MonkeyPatch, env_value: str | None):
|
|
14
|
+
module_name = "prose_style_enforcement_constants_under_test"
|
|
15
|
+
if env_value is None:
|
|
16
|
+
monkeypatch.delenv("CLAUDE_PROSE_STYLE_ENFORCEMENT", raising=False)
|
|
17
|
+
else:
|
|
18
|
+
monkeypatch.setenv("CLAUDE_PROSE_STYLE_ENFORCEMENT", env_value)
|
|
19
|
+
module_spec = importlib.util.spec_from_file_location(module_name, _CONSTANTS_PATH)
|
|
20
|
+
assert module_spec is not None and module_spec.loader is not None
|
|
21
|
+
module = importlib.util.module_from_spec(module_spec)
|
|
22
|
+
module_spec.loader.exec_module(module)
|
|
23
|
+
return module
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_absent_env_is_default_off(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
27
|
+
module = _load_constants_module(monkeypatch, None)
|
|
28
|
+
assert module.PROSE_STYLE_ENFORCEMENT_ENABLED is False
|
|
29
|
+
assert module.prose_style_enforcement_enabled_in_environment() is False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@pytest.mark.parametrize("enabled_value", ["1", "true", "yes", "on", " True "])
|
|
33
|
+
def test_truthy_env_enables(
|
|
34
|
+
monkeypatch: pytest.MonkeyPatch, enabled_value: str
|
|
35
|
+
) -> None:
|
|
36
|
+
module = _load_constants_module(monkeypatch, enabled_value)
|
|
37
|
+
assert module.PROSE_STYLE_ENFORCEMENT_ENABLED is True
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.parametrize("disabled_value", ["0", "false", "no", "off", ""])
|
|
41
|
+
def test_falsy_env_stays_off(
|
|
42
|
+
monkeypatch: pytest.MonkeyPatch, disabled_value: str
|
|
43
|
+
) -> None:
|
|
44
|
+
module = _load_constants_module(monkeypatch, disabled_value)
|
|
45
|
+
assert module.PROSE_STYLE_ENFORCEMENT_ENABLED is False
|
|
@@ -1,15 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
Stop hook that blocks
|
|
4
|
-
|
|
5
|
-
The `eli11-replies`
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
A reply opening with "Long form:" opts out, so a user-requested full report
|
|
12
|
-
still goes through.
|
|
3
|
+
Stop hook that blocks egregious reply-shape failures (action-first, bullets).
|
|
4
|
+
|
|
5
|
+
The `eli11-replies` and `opus5-communication-contract` rules ask every chat
|
|
6
|
+
reply to lead with the action when the user must act, keep findings to a few
|
|
7
|
+
bullets, and stay scannable. This hook reads the final assistant message,
|
|
8
|
+
strips code fences, inline code, blockquotes, table rows, and link targets,
|
|
9
|
+
then judges shape only: multi-line instructions need numbered steps first,
|
|
10
|
+
bullet count stays bounded, and list lines stay one-idea short.
|
|
13
11
|
"""
|
|
14
12
|
|
|
15
13
|
import json
|
|
@@ -30,13 +28,10 @@ from hooks_constants.eli11_reply_enforcer_constants import ( # noqa: E402
|
|
|
30
28
|
IMPERATIVE_OBJECT_TOKEN_PATTERN,
|
|
31
29
|
LINK_TARGET_PATTERN,
|
|
32
30
|
LIST_MARKER_PREFIX_PATTERN,
|
|
33
|
-
LONG_FORM_ESCAPE_PREFIX,
|
|
34
31
|
MARKDOWN_LEAD_MARKER_PATTERN,
|
|
35
32
|
MAXIMUM_BULLET_LINE_COUNT,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
MAXIMUM_WORDS_PER_LINE,
|
|
39
|
-
MINIMUM_ENFORCED_WORD_COUNT,
|
|
33
|
+
MAXIMUM_OVERPACKED_LIST_LINE_COUNT,
|
|
34
|
+
MAXIMUM_WORDS_PER_LIST_LINE,
|
|
40
35
|
NUMBERED_STEP_PATTERN,
|
|
41
36
|
TABLE_ROW_PATTERN,
|
|
42
37
|
TARGET_BULLET_LINE_COUNT,
|
|
@@ -69,18 +64,6 @@ def extract_reply_prose(assistant_message: str) -> str:
|
|
|
69
64
|
return LINK_TARGET_PATTERN.sub("", prose_text)
|
|
70
65
|
|
|
71
66
|
|
|
72
|
-
def count_reply_words(prose_text: str) -> int:
|
|
73
|
-
"""Return the number of reader-visible words in the stripped prose.
|
|
74
|
-
|
|
75
|
-
Args:
|
|
76
|
-
prose_text: Prose already cleaned of code, quotes, tables, and links.
|
|
77
|
-
|
|
78
|
-
Returns:
|
|
79
|
-
The count of whitespace-separated tokens carrying a letter or digit.
|
|
80
|
-
"""
|
|
81
|
-
return len(COUNTABLE_WORD_PATTERN.findall(prose_text))
|
|
82
|
-
|
|
83
|
-
|
|
84
67
|
def collect_non_empty_lines(prose_text: str) -> list[str]:
|
|
85
68
|
"""Return every line of the stripped prose that carries visible text.
|
|
86
69
|
|
|
@@ -249,29 +232,13 @@ def count_words_in_line(prose_line: str) -> int:
|
|
|
249
232
|
return len(COUNTABLE_WORD_PATTERN.findall(unmarked_line))
|
|
250
233
|
|
|
251
234
|
|
|
252
|
-
def count_overpacked_lines(all_prose_lines: list[str]) -> int:
|
|
253
|
-
"""Return how many lines carry more words than one idea needs.
|
|
254
|
-
|
|
255
|
-
Args:
|
|
256
|
-
all_prose_lines: The non-empty lines of reply prose.
|
|
257
|
-
|
|
258
|
-
Returns:
|
|
259
|
-
The count of lines over the per-line word cap.
|
|
260
|
-
"""
|
|
261
|
-
return sum(
|
|
262
|
-
1
|
|
263
|
-
for each_line in all_prose_lines
|
|
264
|
-
if count_words_in_line(each_line) > MAXIMUM_WORDS_PER_LINE
|
|
265
|
-
)
|
|
266
|
-
|
|
267
|
-
|
|
268
235
|
def strip_markdown_lead_markers(prose_line: str) -> str:
|
|
269
236
|
"""Return a line with its opening blockquote, heading, and bold markers off.
|
|
270
237
|
|
|
271
238
|
::
|
|
272
239
|
|
|
273
|
-
in: "> **
|
|
274
|
-
out: "
|
|
240
|
+
in: "> **Bold lead:** the report follows"
|
|
241
|
+
out: "Bold lead:** the report follows"
|
|
275
242
|
|
|
276
243
|
Args:
|
|
277
244
|
prose_line: One line of raw reply text.
|
|
@@ -282,34 +249,37 @@ def strip_markdown_lead_markers(prose_line: str) -> str:
|
|
|
282
249
|
return MARKDOWN_LEAD_MARKER_PATTERN.sub("", prose_line.strip(), count=1)
|
|
283
250
|
|
|
284
251
|
|
|
285
|
-
def
|
|
286
|
-
"""Return True when
|
|
252
|
+
def is_list_item_line(prose_line: str) -> bool:
|
|
253
|
+
"""Return True when a line is a bullet or numbered list item.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
prose_line: One non-empty line of reply prose.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
True when the line opens with a bullet or numbered-step marker.
|
|
260
|
+
"""
|
|
261
|
+
if BULLET_LINE_PATTERN.match(prose_line):
|
|
262
|
+
return True
|
|
263
|
+
return NUMBERED_STEP_PATTERN.match(prose_line) is not None
|
|
287
264
|
|
|
288
|
-
::
|
|
289
265
|
|
|
290
|
-
|
|
291
|
-
|
|
266
|
+
def count_overpacked_list_lines(all_prose_lines: list[str]) -> int:
|
|
267
|
+
"""Return how many list lines carry more words than one idea needs.
|
|
292
268
|
|
|
293
|
-
|
|
269
|
+
Plain paragraphs are not counted so a requested full report can use long
|
|
270
|
+
prose lines without a word ceiling.
|
|
294
271
|
|
|
295
272
|
Args:
|
|
296
|
-
|
|
273
|
+
all_prose_lines: The non-empty lines of reply prose.
|
|
297
274
|
|
|
298
275
|
Returns:
|
|
299
|
-
|
|
276
|
+
The count of bullet/numbered lines over the per-list-line word cap.
|
|
300
277
|
"""
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
return False
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
def describe_word_cap_violation(reply_word_count: int) -> str:
|
|
310
|
-
"""Return the violation text naming the reply length and the cap."""
|
|
311
|
-
return (
|
|
312
|
-
f"{reply_word_count} words, over the {MAXIMUM_REPLY_WORD_COUNT}-word cap"
|
|
278
|
+
return sum(
|
|
279
|
+
1
|
|
280
|
+
for each_line in all_prose_lines
|
|
281
|
+
if is_list_item_line(each_line)
|
|
282
|
+
and count_words_in_line(each_line) > MAXIMUM_WORDS_PER_LIST_LINE
|
|
313
283
|
)
|
|
314
284
|
|
|
315
285
|
|
|
@@ -317,70 +287,68 @@ def describe_bullet_cap_violation(bullet_line_count: int) -> str:
|
|
|
317
287
|
"""Return the violation text naming the bullet count and the target."""
|
|
318
288
|
return (
|
|
319
289
|
f"{bullet_line_count} bullets, over the {MAXIMUM_BULLET_LINE_COUNT}-bullet "
|
|
320
|
-
f"cap -
|
|
290
|
+
f"cap - put findings in at most {TARGET_BULLET_LINE_COUNT} bullets"
|
|
321
291
|
)
|
|
322
292
|
|
|
323
293
|
|
|
324
|
-
def
|
|
325
|
-
"""Return the violation text
|
|
294
|
+
def describe_overpacked_list_line_violation(overpacked_line_count: int) -> str:
|
|
295
|
+
"""Return the violation text for overpacked list lines."""
|
|
326
296
|
return (
|
|
327
|
-
f"{overpacked_line_count} lines carry too many words - one idea per
|
|
328
|
-
f"at most {
|
|
297
|
+
f"{overpacked_line_count} list lines carry too many words - one idea per "
|
|
298
|
+
f"bullet or step, at most {MAXIMUM_WORDS_PER_LIST_LINE} words each"
|
|
329
299
|
)
|
|
330
300
|
|
|
331
301
|
|
|
332
302
|
def describe_action_first_violation() -> str:
|
|
333
303
|
"""Return the violation text for instructions buried under prose."""
|
|
334
304
|
return (
|
|
335
|
-
"the reply tells the user to act but leads with prose -
|
|
336
|
-
"
|
|
305
|
+
"the reply tells the user to act but leads with prose - open with "
|
|
306
|
+
"numbered steps, one short line each"
|
|
337
307
|
)
|
|
338
308
|
|
|
339
309
|
|
|
340
|
-
def is_action_first_violation(
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
310
|
+
def is_action_first_violation(all_prose_lines: list[str]) -> bool:
|
|
311
|
+
"""Return True when multi-line instructions lack leading numbered steps.
|
|
312
|
+
|
|
313
|
+
::
|
|
314
|
+
|
|
315
|
+
ok: single-line "Run the migration." -> False
|
|
316
|
+
ok: numbered steps first, then prose -> False
|
|
317
|
+
flag: prose then "Install the package." -> True
|
|
344
318
|
|
|
345
319
|
Args:
|
|
346
|
-
reply_word_count: The reader-visible word count of the reply.
|
|
347
320
|
all_prose_lines: The non-empty lines of reply prose.
|
|
348
321
|
|
|
349
322
|
Returns:
|
|
350
|
-
True when the reply
|
|
351
|
-
numbered step among its lead lines.
|
|
323
|
+
True when the reply has more than one line, carries instruction
|
|
324
|
+
lines, and shows no numbered step among its lead lines.
|
|
352
325
|
"""
|
|
353
|
-
if
|
|
326
|
+
if len(all_prose_lines) <= 1:
|
|
354
327
|
return False
|
|
355
328
|
if not has_imperative_instruction_line(all_prose_lines):
|
|
356
329
|
return False
|
|
357
330
|
return not has_leading_numbered_step(all_prose_lines)
|
|
358
331
|
|
|
359
332
|
|
|
360
|
-
def collect_shape_violations(
|
|
361
|
-
reply_word_count: int, all_prose_lines: list[str]
|
|
362
|
-
) -> list[str]:
|
|
333
|
+
def collect_shape_violations(all_prose_lines: list[str]) -> list[str]:
|
|
363
334
|
"""Return the violation texts for every reply-shape rule the reply breaks.
|
|
364
335
|
|
|
365
336
|
Args:
|
|
366
|
-
reply_word_count: The reader-visible word count of the reply.
|
|
367
337
|
all_prose_lines: The non-empty lines of reply prose.
|
|
368
338
|
|
|
369
339
|
Returns:
|
|
370
340
|
One violation text per broken rule, empty when the reply is in shape.
|
|
371
341
|
"""
|
|
372
342
|
all_violations: list[str] = []
|
|
373
|
-
if
|
|
374
|
-
all_violations.append(describe_word_cap_violation(reply_word_count))
|
|
375
|
-
if is_action_first_violation(reply_word_count, all_prose_lines):
|
|
343
|
+
if is_action_first_violation(all_prose_lines):
|
|
376
344
|
all_violations.append(describe_action_first_violation())
|
|
377
345
|
bullet_line_count = count_bullet_lines(all_prose_lines)
|
|
378
346
|
if bullet_line_count > MAXIMUM_BULLET_LINE_COUNT:
|
|
379
347
|
all_violations.append(describe_bullet_cap_violation(bullet_line_count))
|
|
380
|
-
|
|
381
|
-
if
|
|
348
|
+
overpacked_list_line_count = count_overpacked_list_lines(all_prose_lines)
|
|
349
|
+
if overpacked_list_line_count > MAXIMUM_OVERPACKED_LIST_LINE_COUNT:
|
|
382
350
|
all_violations.append(
|
|
383
|
-
|
|
351
|
+
describe_overpacked_list_line_violation(overpacked_list_line_count)
|
|
384
352
|
)
|
|
385
353
|
return all_violations
|
|
386
354
|
|
|
@@ -390,11 +358,9 @@ def find_reply_shape_violations(assistant_message: str) -> list[str]:
|
|
|
390
358
|
|
|
391
359
|
::
|
|
392
360
|
|
|
393
|
-
ok: a
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
A reply opening with the escape prefix, and a reply under the enforced word
|
|
397
|
-
floor, are both returned as in shape.
|
|
361
|
+
ok: a short outcome sentence -> []
|
|
362
|
+
ok: a long requested audit with few bullets -> []
|
|
363
|
+
flag: prose then buried "Install the package." -> [action-first]
|
|
398
364
|
|
|
399
365
|
Args:
|
|
400
366
|
assistant_message: The raw final assistant message.
|
|
@@ -402,19 +368,12 @@ def find_reply_shape_violations(assistant_message: str) -> list[str]:
|
|
|
402
368
|
Returns:
|
|
403
369
|
One violation text per broken rule, empty when the reply is in shape.
|
|
404
370
|
"""
|
|
405
|
-
if opens_with_long_form_escape(assistant_message):
|
|
406
|
-
return []
|
|
407
371
|
prose_text = extract_reply_prose(assistant_message)
|
|
408
|
-
|
|
409
|
-
if reply_word_count < MINIMUM_ENFORCED_WORD_COUNT:
|
|
410
|
-
return []
|
|
411
|
-
return collect_shape_violations(
|
|
412
|
-
reply_word_count, collect_non_empty_lines(prose_text)
|
|
413
|
-
)
|
|
372
|
+
return collect_shape_violations(collect_non_empty_lines(prose_text))
|
|
414
373
|
|
|
415
374
|
|
|
416
375
|
def build_block_reason(all_violations: list[str]) -> str:
|
|
417
|
-
"""Return the corrective message naming each violation and the
|
|
376
|
+
"""Return the corrective message naming each violation and the rewrite path.
|
|
418
377
|
|
|
419
378
|
Args:
|
|
420
379
|
all_violations: The violation texts the reply earned.
|
|
@@ -424,16 +383,14 @@ def build_block_reason(all_violations: list[str]) -> str:
|
|
|
424
383
|
"""
|
|
425
384
|
formatted_violation_list = VIOLATION_SEPARATOR.join(all_violations)
|
|
426
385
|
return (
|
|
427
|
-
f"ELI11 REPLY SHAPE:
|
|
386
|
+
f"ELI11 REPLY SHAPE: Rewrite the reply into a short action-first shape "
|
|
428
387
|
f"({formatted_violation_list}).\n\n"
|
|
429
|
-
f"
|
|
430
|
-
f"
|
|
431
|
-
f"
|
|
432
|
-
f"
|
|
433
|
-
f"
|
|
434
|
-
f"links are already exempt from
|
|
435
|
-
f"When the user asked for a full report, start the reply with "
|
|
436
|
-
f'"Long form:" to opt out of this check.\n\n'
|
|
388
|
+
f"When the user must act, open with numbered click-by-click steps, one "
|
|
389
|
+
f"short line each. When nothing is needed, open with the outcome in one "
|
|
390
|
+
f"sentence. Keep findings to at most {TARGET_BULLET_LINE_COUNT} short "
|
|
391
|
+
f"bullets. A requested full report may run long; keep list lines short "
|
|
392
|
+
f"and lead with the outcome. Code fences, blockquotes, tables, and "
|
|
393
|
+
f"links are already exempt from shape counting.\n\n"
|
|
437
394
|
f"You MUST re-output the complete, revised response with the correction "
|
|
438
395
|
f"applied."
|
|
439
396
|
)
|
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
3
|
-
|
|
2
|
+
"""Stop hook for bare hedging words that mark unverified claims.
|
|
3
|
+
|
|
4
|
+
Words like "likely" or "probably" stand in for evidence. When prose-style
|
|
5
|
+
enforcement is on, a hedge word blocks the response unless the same sentence
|
|
6
|
+
carries an explicit uncertainty label (unverified, I don't know, no source for
|
|
7
|
+
this claim). A label in another sentence does not exempt a bare hedge.
|
|
8
|
+
|
|
9
|
+
::
|
|
4
10
|
|
|
5
|
-
|
|
6
|
-
|
|
11
|
+
pass: This claim is unverified; the deploy is probably blocked.
|
|
12
|
+
flag: This claim is unverified. The deploy is probably blocked.
|
|
13
|
+
|
|
14
|
+
When enforcement is off, surviving bare hedges emit privacy-safe advisory
|
|
15
|
+
candidates (OP-07B). See rules/hedging-claims.md and
|
|
16
|
+
docs/references/prose-style-enforcement.md.
|
|
7
17
|
"""
|
|
8
18
|
|
|
9
19
|
import json
|
|
@@ -16,9 +26,25 @@ _hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
|
16
26
|
if _hooks_dir not in sys.path:
|
|
17
27
|
sys.path.insert(0, _hooks_dir)
|
|
18
28
|
|
|
29
|
+
from blocking.config.prose_style_enforcement_constants import ( # noqa: E402
|
|
30
|
+
prose_style_enforcement_enabled_in_environment,
|
|
31
|
+
)
|
|
32
|
+
from hooks_constants.hedging_uncertainty_constants import ( # noqa: E402
|
|
33
|
+
ALL_EXPLICIT_UNCERTAINTY_LABEL_PATTERNS,
|
|
34
|
+
BLOCK_REASON_PREFIX,
|
|
35
|
+
HEDGING_TERM_LIST_SEPARATOR,
|
|
36
|
+
POSITIVE_CORRECTIVE_GUIDANCE,
|
|
37
|
+
SENTENCE_BOUNDARY_PATTERN,
|
|
38
|
+
)
|
|
19
39
|
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
20
40
|
from hooks_constants.messages import USER_FACING_NOTICE # noqa: E402
|
|
41
|
+
from hooks_constants.prose_matcher_precision_constants import ( # noqa: E402
|
|
42
|
+
ADVISORY_CONTEXT_SNIPPET_MAX_CHARS,
|
|
43
|
+
MATCHER_ID_HEDGING_WORD,
|
|
44
|
+
MAXIMUM_ADVISORY_EMITS_PER_CALL,
|
|
45
|
+
)
|
|
21
46
|
from hooks_constants.text_stripping import strip_code_and_quotes # noqa: E402
|
|
47
|
+
from observability.prose_matcher_advisory import emit_advisory_candidate # noqa: E402
|
|
22
48
|
|
|
23
49
|
PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
24
50
|
|
|
@@ -59,18 +85,65 @@ ALL_HEDGING_PATTERNS = [
|
|
|
59
85
|
re.compile(pattern, re.IGNORECASE) for pattern in HEDGING_WORDS + HEDGING_PHRASES
|
|
60
86
|
]
|
|
61
87
|
|
|
62
|
-
def find_hedging_words(text: str) -> list[str]:
|
|
63
|
-
"""Return all hedging words/phrases found in the text."""
|
|
64
|
-
prose_text = strip_code_and_quotes(text)
|
|
65
|
-
matched_terms = []
|
|
66
88
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
89
|
+
def _sentence_carries_explicit_uncertainty(sentence: str) -> bool:
|
|
90
|
+
"""Return True when the sentence labels the claim as unknown or unverified."""
|
|
91
|
+
for each_label_pattern in ALL_EXPLICIT_UNCERTAINTY_LABEL_PATTERNS:
|
|
92
|
+
if each_label_pattern.search(sentence):
|
|
93
|
+
return True
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _hedging_terms_in_prose_slice(prose_slice: str) -> list[str]:
|
|
98
|
+
"""Return unique hedge terms found in one prose slice (sentence or full text)."""
|
|
99
|
+
matched_terms: list[str] = []
|
|
100
|
+
for each_pattern in ALL_HEDGING_PATTERNS:
|
|
101
|
+
for each_match in each_pattern.findall(prose_slice):
|
|
70
102
|
normalized_term = each_match.strip().lower()
|
|
71
103
|
if normalized_term not in matched_terms:
|
|
72
104
|
matched_terms.append(normalized_term)
|
|
105
|
+
return matched_terms
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def find_hedging_words(text: str) -> list[str]:
|
|
109
|
+
"""Return all hedging words/phrases in the stripped prose (any sentence)."""
|
|
110
|
+
prose_text = strip_code_and_quotes(text)
|
|
111
|
+
return _hedging_terms_in_prose_slice(prose_text)
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def find_blocking_hedging_terms(text: str) -> list[str]:
|
|
115
|
+
"""Return hedge terms that lack an explicit uncertainty label in-sentence.
|
|
116
|
+
|
|
117
|
+
::
|
|
118
|
+
|
|
119
|
+
find_blocking_hedging_terms("Unverified: the port is probably down.")
|
|
120
|
+
-> []
|
|
121
|
+
find_blocking_hedging_terms("Unverified. The port is probably down.")
|
|
122
|
+
-> ["probably"]
|
|
73
123
|
|
|
124
|
+
Args:
|
|
125
|
+
text: Full assistant message, including code and quotes.
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Unique bare hedge terms that still warrant a block or advisory emit.
|
|
129
|
+
"""
|
|
130
|
+
prose_text = strip_code_and_quotes(text)
|
|
131
|
+
if not prose_text.strip():
|
|
132
|
+
return []
|
|
133
|
+
|
|
134
|
+
all_sentences = SENTENCE_BOUNDARY_PATTERN.split(prose_text)
|
|
135
|
+
if len(all_sentences) <= 1:
|
|
136
|
+
all_sentences = [prose_text]
|
|
137
|
+
|
|
138
|
+
matched_terms: list[str] = []
|
|
139
|
+
for each_sentence in all_sentences:
|
|
140
|
+
if not each_sentence.strip():
|
|
141
|
+
continue
|
|
142
|
+
if _sentence_carries_explicit_uncertainty(each_sentence):
|
|
143
|
+
continue
|
|
144
|
+
for each_term in _hedging_terms_in_prose_slice(each_sentence):
|
|
145
|
+
if each_term not in matched_terms:
|
|
146
|
+
matched_terms.append(each_term)
|
|
74
147
|
return matched_terms
|
|
75
148
|
|
|
76
149
|
|
|
@@ -88,12 +161,26 @@ def main() -> None:
|
|
|
88
161
|
if not assistant_message:
|
|
89
162
|
sys.exit(0)
|
|
90
163
|
|
|
91
|
-
found_hedging_terms =
|
|
164
|
+
found_hedging_terms = find_blocking_hedging_terms(assistant_message)
|
|
92
165
|
|
|
93
166
|
if not found_hedging_terms:
|
|
94
167
|
sys.exit(0)
|
|
95
168
|
|
|
96
|
-
|
|
169
|
+
if not prose_style_enforcement_enabled_in_environment():
|
|
170
|
+
try:
|
|
171
|
+
for each_term in found_hedging_terms[:MAXIMUM_ADVISORY_EMITS_PER_CALL]:
|
|
172
|
+
emit_advisory_candidate(
|
|
173
|
+
MATCHER_ID_HEDGING_WORD,
|
|
174
|
+
"Stop",
|
|
175
|
+
f"{each_term}:{assistant_message[:ADVISORY_CONTEXT_SNIPPET_MAX_CHARS]}",
|
|
176
|
+
)
|
|
177
|
+
except (ImportError, OSError, TypeError, ValueError):
|
|
178
|
+
pass
|
|
179
|
+
sys.exit(0)
|
|
180
|
+
|
|
181
|
+
formatted_term_list = HEDGING_TERM_LIST_SEPARATOR.join(
|
|
182
|
+
f'"{term}"' for term in found_hedging_terms
|
|
183
|
+
)
|
|
97
184
|
|
|
98
185
|
resolved_skill_path: str | None = None
|
|
99
186
|
for each_skill_path in RESEARCH_MODE_SKILL_SEARCH_PATHS:
|
|
@@ -110,13 +197,9 @@ def main() -> None:
|
|
|
110
197
|
)
|
|
111
198
|
|
|
112
199
|
block_reason = (
|
|
113
|
-
f"
|
|
114
|
-
f"{
|
|
115
|
-
f"
|
|
116
|
-
f"{skill_reference}\n\n"
|
|
117
|
-
f"Do NOT simply remove the hedging word and keep the unverified claim. "
|
|
118
|
-
f"Do more research to VERIFY it with a source, or prompt the user via AskUserQuestion with some potential options + context if you are unable to find anything online.\n\n"
|
|
119
|
-
f"You MUST re-output the complete, revised response with the corrections applied."
|
|
200
|
+
f"{BLOCK_REASON_PREFIX}{formatted_term_list}. "
|
|
201
|
+
f"Rewrite {skill_reference}\n\n"
|
|
202
|
+
f"{POSITIVE_CORRECTIVE_GUIDANCE}"
|
|
120
203
|
)
|
|
121
204
|
block_response = {
|
|
122
205
|
"decision": "block",
|
|
@@ -39,6 +39,9 @@ _hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
|
39
39
|
if _hooks_dir not in sys.path:
|
|
40
40
|
sys.path.insert(0, _hooks_dir)
|
|
41
41
|
|
|
42
|
+
from blocking.config.prose_style_enforcement_constants import ( # noqa: E402
|
|
43
|
+
prose_style_enforcement_enabled_in_environment,
|
|
44
|
+
)
|
|
42
45
|
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
43
46
|
from hooks_constants.hook_prose_detector_consistency_constants import ( # noqa: E402
|
|
44
47
|
CONSTANTS_MODULE_SUFFIX,
|
|
@@ -118,6 +121,9 @@ def main() -> None:
|
|
|
118
121
|
except json.JSONDecodeError:
|
|
119
122
|
sys.exit(0)
|
|
120
123
|
|
|
124
|
+
if not prose_style_enforcement_enabled_in_environment():
|
|
125
|
+
sys.exit(0)
|
|
126
|
+
|
|
121
127
|
tool_name = hook_input.get("tool_name", "")
|
|
122
128
|
if tool_name not in (WRITE_TOOL_NAME, EDIT_TOOL_NAME):
|
|
123
129
|
sys.exit(0)
|
|
@@ -18,6 +18,9 @@ _hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
|
18
18
|
if _hooks_dir not in sys.path:
|
|
19
19
|
sys.path.insert(0, _hooks_dir)
|
|
20
20
|
|
|
21
|
+
from blocking.config.prose_style_enforcement_constants import ( # noqa: E402
|
|
22
|
+
prose_style_enforcement_enabled_in_environment,
|
|
23
|
+
)
|
|
21
24
|
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
22
25
|
from hooks_constants.messages import USER_FACING_INTENT_ENDING_NOTICE # noqa: E402
|
|
23
26
|
from hooks_constants.text_stripping import strip_code_and_quotes # noqa: E402
|
|
@@ -110,6 +113,9 @@ def main() -> None:
|
|
|
110
113
|
if hook_input.get("stop_hook_active", False):
|
|
111
114
|
sys.exit(0)
|
|
112
115
|
|
|
116
|
+
if not prose_style_enforcement_enabled_in_environment():
|
|
117
|
+
sys.exit(0)
|
|
118
|
+
|
|
113
119
|
assistant_message = hook_input.get("last_assistant_message", "")
|
|
114
120
|
|
|
115
121
|
if not assistant_message:
|