claude-dev-env 2.8.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 +8 -2
- 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 +62 -562
- package/agents/code-quality-agent.md +10 -2
- package/agents/code-verifier.md +1 -1
- package/agents/test_agent_frontmatter.py +58 -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/CODE_RULES.md +4 -2
- package/docs/references/CLAUDE.md +3 -2
- package/docs/references/advisor-tool.md +44 -6
- package/docs/references/prose-style-enforcement.md +25 -0
- package/docs/references/team-advisor-skill.md +14 -8
- 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/code_rules_path_utils_constants.py +1 -0
- 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 +15 -0
- package/package.json +5 -2
- package/rules/CLAUDE.md +1 -0
- package/rules/code-standards.md +33 -7
- package/rules/durable-post-artifacts.md +2 -2
- package/rules/eli11-replies.md +7 -2
- 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 +13 -2
- 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 +3 -1
- 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/grok_run_ledger_constants.py +50 -0
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +104 -0
- package/scripts/dev_env_scripts_constants/verify_installable_package_constants.py +116 -0
- package/scripts/grok_patch_artifacts.py +123 -0
- package/scripts/grok_run_ledger.py +318 -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 +556 -9
- 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_grok_patch_artifacts.py +82 -0
- package/scripts/test_grok_run_ledger.py +116 -0
- package/scripts/test_resolve_worker_spawn.py +2 -2
- package/scripts/test_spawn_grok_batch.py +295 -0
- 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 +6 -2
- package/skills/_shared/CLAUDE.md +37 -4
- package/skills/_shared/advisor/CLAUDE.md +9 -0
- package/skills/_shared/advisor/advisor-protocol.md +5 -0
- package/skills/_shared/advisor/scripts/README.md +9 -0
- package/skills/_shared/end-of-run-gotcha-recommendations.md +156 -0
- package/skills/_shared/pr-loop/CLAUDE.md +18 -1
- package/skills/_shared/pr-loop/audit-contract.md +5 -0
- package/skills/_shared/pr-loop/audit-reply-template.md +5 -0
- package/skills/_shared/pr-loop/code-rules-gate.md +5 -0
- package/skills/_shared/pr-loop/fix-protocol.md +5 -0
- package/skills/_shared/pr-loop/gh-payloads.md +5 -0
- package/skills/_shared/pr-loop/post-audit-thread-contract.md +5 -0
- package/skills/_shared/pr-loop/precatch-rubric.md +5 -0
- package/skills/_shared/pr-loop/scripts/CLAUDE.md +9 -1
- package/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +29 -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/_shared/pr-loop/state-schema.md +5 -0
- package/skills/_shared/pr-loop/worker-spawn.md +5 -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 +29 -3
- 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/reference/runner-selection.md +40 -0
- 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/__init__.py +1 -0
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/finding_pipeline_constants.py +49 -0
- package/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +55 -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 +230 -0
- 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 +289 -0
- package/skills/grok-spawn/SKILL.md +5 -0
- package/skills/orchestrator/SKILL.md +5 -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/skills/task-build/reference/tool-routing.md +3 -0
- package/skills/team-advisor/SKILL.md +23 -44
- package/system-prompts/software-engineer.xml +17 -5
- package/commands/initialize.md +0 -90
- package/commands/stubcheck.md +0 -88
- package/skills/test_markdown_link_integrity.py +0 -107
|
@@ -15,16 +15,16 @@ if _HOOKS_ROOT not in sys.path:
|
|
|
15
15
|
import eli11_reply_enforcer
|
|
16
16
|
from hooks_constants.eli11_reply_enforcer_constants import (
|
|
17
17
|
MAXIMUM_BULLET_LINE_COUNT,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
MAXIMUM_WORDS_PER_LINE,
|
|
21
|
-
MINIMUM_ENFORCED_WORD_COUNT,
|
|
18
|
+
MAXIMUM_OVERPACKED_LIST_LINE_COUNT,
|
|
19
|
+
MAXIMUM_WORDS_PER_LIST_LINE,
|
|
22
20
|
TARGET_BULLET_LINE_COUNT,
|
|
23
21
|
)
|
|
24
22
|
from hooks_constants.text_stripping import strip_code_and_quotes
|
|
25
23
|
|
|
26
|
-
SAFE_LINE_WORD_COUNT =
|
|
27
|
-
|
|
24
|
+
SAFE_LINE_WORD_COUNT = MAXIMUM_WORDS_PER_LIST_LINE // 2
|
|
25
|
+
OVERPACKED_LIST_LINE_WORD_COUNT = MAXIMUM_WORDS_PER_LIST_LINE + 5
|
|
26
|
+
LONG_REPORT_WORD_COUNT = 240
|
|
27
|
+
SHORT_REPLY_WORD_COUNT = 30
|
|
28
28
|
|
|
29
29
|
|
|
30
30
|
def build_filler_prose(word_count: int) -> str:
|
|
@@ -45,17 +45,13 @@ def build_prose_block(line_count: int, words_per_line: int) -> str:
|
|
|
45
45
|
|
|
46
46
|
|
|
47
47
|
def build_reply_of_exactly(total_word_count: int) -> str:
|
|
48
|
-
"""Return a filler reply holding exactly the requested words, no line overpacked.
|
|
49
|
-
|
|
50
|
-
::
|
|
51
|
-
|
|
52
|
-
in: 22 -> three lines of 10, 10, and 2 filler words
|
|
48
|
+
"""Return a filler reply holding exactly the requested words, no list line overpacked.
|
|
53
49
|
|
|
54
50
|
Args:
|
|
55
51
|
total_word_count: How many countable words the whole reply carries.
|
|
56
52
|
|
|
57
53
|
Returns:
|
|
58
|
-
A newline-joined reply whose every line stays under the
|
|
54
|
+
A newline-joined reply whose every line stays under the list-line word cap.
|
|
59
55
|
"""
|
|
60
56
|
all_lines = []
|
|
61
57
|
remaining_word_count = total_word_count
|
|
@@ -66,63 +62,34 @@ def build_reply_of_exactly(total_word_count: int) -> str:
|
|
|
66
62
|
return "\n".join(all_lines)
|
|
67
63
|
|
|
68
64
|
|
|
69
|
-
SHORT_REPLY_WORD_COUNT = MINIMUM_ENFORCED_WORD_COUNT // 2
|
|
70
|
-
OVERLONG_REPLY_WORD_COUNT = MAXIMUM_REPLY_WORD_COUNT * 2
|
|
71
|
-
JUST_OVER_WORD_CAP_WORD_COUNT = MAXIMUM_REPLY_WORD_COUNT + 1
|
|
72
|
-
UNDER_FLOOR_WORD_COUNT = MINIMUM_ENFORCED_WORD_COUNT - 1
|
|
73
|
-
|
|
74
65
|
SHORT_REPLY = build_reply_of_exactly(SHORT_REPLY_WORD_COUNT)
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
MAXIMUM_OVERPACKED_LINE_COUNT + 1, OVERPACKED_LINE_WORD_COUNT
|
|
66
|
+
LONG_REPORT_REPLY = build_reply_of_exactly(LONG_REPORT_WORD_COUNT)
|
|
67
|
+
REQUESTED_FULL_REPORT = (
|
|
68
|
+
"Outcome: the audit is complete.\n\n"
|
|
69
|
+
+ build_reply_of_exactly(LONG_REPORT_WORD_COUNT)
|
|
80
70
|
)
|
|
81
|
-
|
|
82
|
-
|
|
71
|
+
THREE_OVERPACKED_LIST_LINE_REPLY = build_bullet_block(
|
|
72
|
+
MAXIMUM_OVERPACKED_LIST_LINE_COUNT + 1, OVERPACKED_LIST_LINE_WORD_COUNT
|
|
83
73
|
)
|
|
84
|
-
|
|
85
|
-
|
|
74
|
+
TWO_OVERPACKED_LIST_LINE_REPLY = build_bullet_block(
|
|
75
|
+
MAXIMUM_OVERPACKED_LIST_LINE_COUNT, OVERPACKED_LIST_LINE_WORD_COUNT
|
|
86
76
|
)
|
|
87
|
-
|
|
77
|
+
SEVEN_BULLET_REPLY = build_bullet_block(
|
|
88
78
|
MAXIMUM_BULLET_LINE_COUNT + 1, SAFE_LINE_WORD_COUNT
|
|
89
79
|
)
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
HEADING_LONG_FORM_OVERLONG_REPLY = f"# Long form: the audit report follows.\n\n{OVERLONG_REPLY}"
|
|
94
|
-
|
|
80
|
+
SIX_BULLET_REPLY = build_bullet_block(
|
|
81
|
+
MAXIMUM_BULLET_LINE_COUNT, SAFE_LINE_WORD_COUNT
|
|
82
|
+
)
|
|
95
83
|
INSTRUCTION_LINE = "Run the migration script."
|
|
96
|
-
INSTRUCTION_LINE_WORD_COUNT = len(INSTRUCTION_LINE.split())
|
|
97
|
-
|
|
98
84
|
ACTION_WITHOUT_STEPS_FIRST_REPLY = (
|
|
99
|
-
f"{build_reply_of_exactly(
|
|
100
|
-
f"{INSTRUCTION_LINE}\n\nMerge the branch."
|
|
85
|
+
f"{build_reply_of_exactly(40)}\n\n{INSTRUCTION_LINE}\n\nMerge the branch."
|
|
101
86
|
)
|
|
102
87
|
ACTION_WITH_STEPS_FIRST_REPLY = (
|
|
103
88
|
"1. **Run** the migration script.\n"
|
|
104
89
|
"2. **Merge** the branch.\n\n"
|
|
105
|
-
f"{build_reply_of_exactly(
|
|
106
|
-
)
|
|
107
|
-
AT_FLOOR_ACTION_REPLY = (
|
|
108
|
-
f"{build_reply_of_exactly(MINIMUM_ENFORCED_WORD_COUNT - INSTRUCTION_LINE_WORD_COUNT)}"
|
|
109
|
-
f"\n\n{INSTRUCTION_LINE}"
|
|
110
|
-
)
|
|
111
|
-
SEVEN_BULLET_REPLY = build_bullet_block(
|
|
112
|
-
MAXIMUM_BULLET_LINE_COUNT + 1, SAFE_LINE_WORD_COUNT + 3
|
|
113
|
-
)
|
|
114
|
-
SIX_BULLET_REPLY = build_bullet_block(
|
|
115
|
-
MAXIMUM_BULLET_LINE_COUNT, SAFE_LINE_WORD_COUNT + 5
|
|
116
|
-
)
|
|
117
|
-
FENCED_CODE_REPLY = f"{SHORT_REPLY}\n\n```python\n{OVERLONG_REPLY}\n```\n"
|
|
118
|
-
BLOCKQUOTE_REPLY = f"{SHORT_REPLY}\n\n> {build_filler_prose(OVERLONG_REPLY_WORD_COUNT)}\n"
|
|
119
|
-
TABLE_REPLY = "{}\n\n{}\n".format(
|
|
120
|
-
SHORT_REPLY,
|
|
121
|
-
"\n".join(
|
|
122
|
-
f"| {build_filler_prose(MAXIMUM_WORDS_PER_LINE)} | cell |" for _ in range(15)
|
|
123
|
-
),
|
|
90
|
+
f"{build_reply_of_exactly(40)}"
|
|
124
91
|
)
|
|
125
|
-
|
|
92
|
+
SINGLE_LINE_INSTRUCTION = INSTRUCTION_LINE
|
|
126
93
|
ALL_NARRATIVE_OPENER_LINES = (
|
|
127
94
|
"Open questions remain about the stripper edge cases.",
|
|
128
95
|
"Run time stays under one second on the package suite.",
|
|
@@ -130,10 +97,10 @@ ALL_NARRATIVE_OPENER_LINES = (
|
|
|
130
97
|
)
|
|
131
98
|
NARRATIVE_OPENER_REPLY = "{}\n\n{}".format(
|
|
132
99
|
"\n".join(ALL_NARRATIVE_OPENER_LINES),
|
|
133
|
-
build_reply_of_exactly(
|
|
100
|
+
build_reply_of_exactly(40),
|
|
134
101
|
)
|
|
135
102
|
INSTALL_WITHOUT_STEPS_FIRST_REPLY = (
|
|
136
|
-
f"{build_reply_of_exactly(
|
|
103
|
+
f"{build_reply_of_exactly(40)}\n\n"
|
|
137
104
|
"Install the package from the registry."
|
|
138
105
|
)
|
|
139
106
|
DO_THINGS_WITH_STEPS_FIRST_REPLY = (
|
|
@@ -141,12 +108,24 @@ DO_THINGS_WITH_STEPS_FIRST_REPLY = (
|
|
|
141
108
|
"1. Install the package.\n"
|
|
142
109
|
"2. Restart the daemon.\n"
|
|
143
110
|
"3. Save the file.\n\n"
|
|
144
|
-
f"{build_reply_of_exactly(
|
|
111
|
+
f"{build_reply_of_exactly(40)}"
|
|
145
112
|
)
|
|
146
113
|
DO_THINGS_WITHOUT_STEPS_FIRST_REPLY = (
|
|
147
|
-
f"{build_reply_of_exactly(
|
|
114
|
+
f"{build_reply_of_exactly(40)}\n\n"
|
|
148
115
|
"Do 3 things: install, restart, save."
|
|
149
116
|
)
|
|
117
|
+
FENCED_CODE_REPLY = f"{SHORT_REPLY}\n\n```python\n{LONG_REPORT_REPLY}\n```\n"
|
|
118
|
+
BLOCKQUOTE_REPLY = f"{SHORT_REPLY}\n\n> {build_filler_prose(LONG_REPORT_WORD_COUNT)}\n"
|
|
119
|
+
TABLE_REPLY = "{}\n\n{}\n".format(
|
|
120
|
+
SHORT_REPLY,
|
|
121
|
+
"\n".join(
|
|
122
|
+
f"| {build_filler_prose(MAXIMUM_WORDS_PER_LIST_LINE)} | cell |"
|
|
123
|
+
for _ in range(15)
|
|
124
|
+
),
|
|
125
|
+
)
|
|
126
|
+
LONG_PROSE_PARAGRAPHS_REPLY = "\n\n".join(
|
|
127
|
+
build_filler_prose(MAXIMUM_WORDS_PER_LIST_LINE + 10) for _ in range(4)
|
|
128
|
+
)
|
|
150
129
|
|
|
151
130
|
|
|
152
131
|
def run_hook_with_payload(payload: dict) -> subprocess.CompletedProcess:
|
|
@@ -171,8 +150,8 @@ def test_blocker_uses_shared_strip_code_and_quotes() -> None:
|
|
|
171
150
|
assert eli11_reply_enforcer.strip_code_and_quotes is strip_code_and_quotes
|
|
172
151
|
|
|
173
152
|
|
|
174
|
-
def
|
|
175
|
-
"""A
|
|
153
|
+
def test_short_correct_reply_passes_through() -> None:
|
|
154
|
+
"""A concise outcome reply passes without padding."""
|
|
176
155
|
completed_process = run_hook_with_message(SHORT_REPLY)
|
|
177
156
|
assert completed_process.returncode == 0
|
|
178
157
|
assert completed_process.stdout == ""
|
|
@@ -188,84 +167,56 @@ def test_empty_message_passes_through() -> None:
|
|
|
188
167
|
def test_stop_hook_active_flag_passes_through() -> None:
|
|
189
168
|
"""A re-entrant Stop invocation never blocks again."""
|
|
190
169
|
completed_process = run_hook_with_payload(
|
|
191
|
-
{
|
|
170
|
+
{
|
|
171
|
+
"last_assistant_message": ACTION_WITHOUT_STEPS_FIRST_REPLY,
|
|
172
|
+
"stop_hook_active": True,
|
|
173
|
+
}
|
|
192
174
|
)
|
|
193
175
|
assert completed_process.returncode == 0
|
|
194
176
|
assert completed_process.stdout == ""
|
|
195
177
|
|
|
196
178
|
|
|
197
|
-
def
|
|
198
|
-
"""A
|
|
199
|
-
completed_process = run_hook_with_message(
|
|
200
|
-
assert completed_process.returncode == 0
|
|
201
|
-
parsed_response = json.loads(completed_process.stdout)
|
|
202
|
-
assert parsed_response["decision"] == "block"
|
|
203
|
-
assert str(OVERLONG_REPLY_WORD_COUNT) in parsed_response["reason"]
|
|
204
|
-
assert str(MAXIMUM_REPLY_WORD_COUNT) in parsed_response["reason"]
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
def test_reply_just_over_word_cap_emits_block() -> None:
|
|
208
|
-
"""A reply one word past the cap blocks on length alone."""
|
|
209
|
-
completed_process = run_hook_with_message(JUST_OVER_WORD_CAP_REPLY)
|
|
210
|
-
assert completed_process.returncode == 0
|
|
211
|
-
parsed_response = json.loads(completed_process.stdout)
|
|
212
|
-
assert parsed_response["decision"] == "block"
|
|
213
|
-
assert (
|
|
214
|
-
f"{JUST_OVER_WORD_CAP_WORD_COUNT} words, over the "
|
|
215
|
-
f"{MAXIMUM_REPLY_WORD_COUNT}-word cap"
|
|
216
|
-
) in parsed_response["reason"]
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
def test_reply_under_word_floor_passes_through() -> None:
|
|
220
|
-
"""A reply one word under the floor is never judged."""
|
|
221
|
-
completed_process = run_hook_with_message(UNDER_FLOOR_REPLY)
|
|
179
|
+
def test_long_requested_report_passes_without_magic_prefix() -> None:
|
|
180
|
+
"""A thorough report passes with no Long form: prefix and no word ceiling."""
|
|
181
|
+
completed_process = run_hook_with_message(REQUESTED_FULL_REPORT)
|
|
222
182
|
assert completed_process.returncode == 0
|
|
223
183
|
assert completed_process.stdout == ""
|
|
224
184
|
|
|
225
185
|
|
|
226
|
-
def
|
|
227
|
-
"""
|
|
228
|
-
completed_process = run_hook_with_message(
|
|
186
|
+
def test_long_prose_paragraphs_are_not_overpacked() -> None:
|
|
187
|
+
"""Plain paragraphs over the list-line word cap do not block."""
|
|
188
|
+
completed_process = run_hook_with_message(LONG_PROSE_PARAGRAPHS_REPLY)
|
|
229
189
|
assert completed_process.returncode == 0
|
|
230
|
-
|
|
231
|
-
assert parsed_response["decision"] == "block"
|
|
232
|
-
assert "put the steps first" in parsed_response["reason"]
|
|
190
|
+
assert completed_process.stdout == ""
|
|
233
191
|
|
|
234
192
|
|
|
235
|
-
def
|
|
236
|
-
"""A
|
|
237
|
-
completed_process = run_hook_with_message(
|
|
193
|
+
def test_single_line_instruction_passes_through() -> None:
|
|
194
|
+
"""A one-line imperative does not require a numbered list."""
|
|
195
|
+
completed_process = run_hook_with_message(SINGLE_LINE_INSTRUCTION)
|
|
238
196
|
assert completed_process.returncode == 0
|
|
239
|
-
|
|
240
|
-
assert parsed_response["decision"] == "block"
|
|
241
|
-
assert (
|
|
242
|
-
f"cut findings to {TARGET_BULLET_LINE_COUNT} bullets"
|
|
243
|
-
in parsed_response["reason"]
|
|
244
|
-
)
|
|
197
|
+
assert completed_process.stdout == ""
|
|
245
198
|
|
|
246
199
|
|
|
247
|
-
def
|
|
248
|
-
"""
|
|
249
|
-
completed_process = run_hook_with_message(
|
|
200
|
+
def test_three_overpacked_list_lines_emit_block() -> None:
|
|
201
|
+
"""List lines past the overpacked-list cap block on one idea per bullet."""
|
|
202
|
+
completed_process = run_hook_with_message(THREE_OVERPACKED_LIST_LINE_REPLY)
|
|
250
203
|
assert completed_process.returncode == 0
|
|
251
204
|
parsed_response = json.loads(completed_process.stdout)
|
|
252
205
|
assert parsed_response["decision"] == "block"
|
|
253
|
-
assert "lines carry too many words
|
|
254
|
-
parsed_response["reason"]
|
|
255
|
-
)
|
|
206
|
+
assert "list lines carry too many words" in parsed_response["reason"]
|
|
256
207
|
|
|
257
208
|
|
|
258
|
-
def
|
|
259
|
-
"""Over-packed lines at the cap pass."""
|
|
260
|
-
completed_process = run_hook_with_message(
|
|
209
|
+
def test_two_overpacked_list_lines_pass_through() -> None:
|
|
210
|
+
"""Over-packed list lines at the cap pass."""
|
|
211
|
+
completed_process = run_hook_with_message(TWO_OVERPACKED_LIST_LINE_REPLY)
|
|
261
212
|
assert completed_process.returncode == 0
|
|
262
213
|
assert completed_process.stdout == ""
|
|
263
214
|
|
|
264
215
|
|
|
265
216
|
def test_bullet_marker_is_not_counted_as_a_line_word() -> None:
|
|
266
|
-
"""A bullet at the per-line cap stays under it once its marker comes off."""
|
|
217
|
+
"""A bullet at the per-list-line cap stays under it once its marker comes off."""
|
|
267
218
|
capped_bullet_lines = build_bullet_block(
|
|
268
|
-
MAXIMUM_BULLET_LINE_COUNT - 2,
|
|
219
|
+
MAXIMUM_BULLET_LINE_COUNT - 2, MAXIMUM_WORDS_PER_LIST_LINE
|
|
269
220
|
)
|
|
270
221
|
all_violations = eli11_reply_enforcer.find_reply_shape_violations(
|
|
271
222
|
capped_bullet_lines
|
|
@@ -273,41 +224,14 @@ def test_bullet_marker_is_not_counted_as_a_line_word() -> None:
|
|
|
273
224
|
assert all_violations == []
|
|
274
225
|
|
|
275
226
|
|
|
276
|
-
def test_long_form_prefix_exempts_an_overlong_reply() -> None:
|
|
277
|
-
"""The Long form escape hatch clears every reply-shape check."""
|
|
278
|
-
completed_process = run_hook_with_message(LONG_FORM_OVERLONG_REPLY)
|
|
279
|
-
assert completed_process.returncode == 0
|
|
280
|
-
assert completed_process.stdout == ""
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
def test_bold_long_form_prefix_exempts_an_overlong_reply() -> None:
|
|
284
|
-
"""A bold-wrapped Long form prefix still opts the reply out."""
|
|
285
|
-
completed_process = run_hook_with_message(BOLD_LONG_FORM_OVERLONG_REPLY)
|
|
286
|
-
assert completed_process.returncode == 0
|
|
287
|
-
assert completed_process.stdout == ""
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
def test_quoted_long_form_prefix_exempts_an_overlong_reply() -> None:
|
|
291
|
-
"""A blockquoted Long form prefix still opts the reply out."""
|
|
292
|
-
completed_process = run_hook_with_message(QUOTED_LONG_FORM_OVERLONG_REPLY)
|
|
293
|
-
assert completed_process.returncode == 0
|
|
294
|
-
assert completed_process.stdout == ""
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
def test_heading_long_form_prefix_exempts_an_overlong_reply() -> None:
|
|
298
|
-
"""A heading-wrapped Long form prefix still opts the reply out."""
|
|
299
|
-
completed_process = run_hook_with_message(HEADING_LONG_FORM_OVERLONG_REPLY)
|
|
300
|
-
assert completed_process.returncode == 0
|
|
301
|
-
assert completed_process.stdout == ""
|
|
302
|
-
|
|
303
|
-
|
|
304
227
|
def test_instruction_lines_without_leading_steps_emit_block() -> None:
|
|
305
228
|
"""Imperative instructions buried under prose block with a steps-first message."""
|
|
306
229
|
completed_process = run_hook_with_message(ACTION_WITHOUT_STEPS_FIRST_REPLY)
|
|
307
230
|
assert completed_process.returncode == 0
|
|
308
231
|
parsed_response = json.loads(completed_process.stdout)
|
|
309
232
|
assert parsed_response["decision"] == "block"
|
|
310
|
-
assert "
|
|
233
|
+
assert "numbered steps" in parsed_response["reason"]
|
|
234
|
+
assert "Rewrite the reply" in parsed_response["reason"]
|
|
311
235
|
|
|
312
236
|
|
|
313
237
|
def test_numbered_steps_in_lead_lines_pass_through() -> None:
|
|
@@ -350,11 +274,11 @@ def test_names_imperative_object_accepts_determiners_counts_and_paths() -> None:
|
|
|
350
274
|
def test_strip_markdown_lead_markers_removes_every_wrapper() -> None:
|
|
351
275
|
"""Blockquote, heading, and bold wrappers come off the front of a line."""
|
|
352
276
|
assert eli11_reply_enforcer.strip_markdown_lead_markers(
|
|
353
|
-
"> **
|
|
354
|
-
) == "
|
|
277
|
+
"> **Bold lead:** the report follows"
|
|
278
|
+
) == "Bold lead:** the report follows"
|
|
355
279
|
assert eli11_reply_enforcer.strip_markdown_lead_markers(
|
|
356
|
-
"#
|
|
357
|
-
) == "
|
|
280
|
+
"# Heading lead: the report follows"
|
|
281
|
+
) == "Heading lead: the report follows"
|
|
358
282
|
|
|
359
283
|
|
|
360
284
|
def test_install_instruction_without_leading_steps_emits_block() -> None:
|
|
@@ -363,7 +287,7 @@ def test_install_instruction_without_leading_steps_emits_block() -> None:
|
|
|
363
287
|
assert completed_process.returncode == 0
|
|
364
288
|
parsed_response = json.loads(completed_process.stdout)
|
|
365
289
|
assert parsed_response["decision"] == "block"
|
|
366
|
-
assert "
|
|
290
|
+
assert "numbered steps" in parsed_response["reason"]
|
|
367
291
|
|
|
368
292
|
|
|
369
293
|
def test_do_three_things_without_leading_steps_emits_block() -> None:
|
|
@@ -372,7 +296,7 @@ def test_do_three_things_without_leading_steps_emits_block() -> None:
|
|
|
372
296
|
assert completed_process.returncode == 0
|
|
373
297
|
parsed_response = json.loads(completed_process.stdout)
|
|
374
298
|
assert parsed_response["decision"] == "block"
|
|
375
|
-
assert "
|
|
299
|
+
assert "numbered steps" in parsed_response["reason"]
|
|
376
300
|
|
|
377
301
|
|
|
378
302
|
def test_do_three_things_with_numbered_steps_passes_through() -> None:
|
|
@@ -398,13 +322,13 @@ def test_numbered_step_opener_is_a_numbered_step() -> None:
|
|
|
398
322
|
|
|
399
323
|
|
|
400
324
|
def test_more_than_six_bullets_emits_block() -> None:
|
|
401
|
-
"""One bullet past the cap blocks with the
|
|
325
|
+
"""One bullet past the cap blocks with the put-findings message."""
|
|
402
326
|
completed_process = run_hook_with_message(SEVEN_BULLET_REPLY)
|
|
403
327
|
assert completed_process.returncode == 0
|
|
404
328
|
parsed_response = json.loads(completed_process.stdout)
|
|
405
329
|
assert parsed_response["decision"] == "block"
|
|
406
330
|
assert (
|
|
407
|
-
f"
|
|
331
|
+
f"put findings in at most {TARGET_BULLET_LINE_COUNT} bullets"
|
|
408
332
|
in parsed_response["reason"]
|
|
409
333
|
)
|
|
410
334
|
|
|
@@ -416,29 +340,29 @@ def test_six_bullets_pass_through() -> None:
|
|
|
416
340
|
assert completed_process.stdout == ""
|
|
417
341
|
|
|
418
342
|
|
|
419
|
-
def
|
|
420
|
-
"""Words inside a fenced code block
|
|
343
|
+
def test_fenced_code_does_not_force_shape_block() -> None:
|
|
344
|
+
"""Words inside a fenced code block do not create shape violations alone."""
|
|
421
345
|
completed_process = run_hook_with_message(FENCED_CODE_REPLY)
|
|
422
346
|
assert completed_process.returncode == 0
|
|
423
347
|
assert completed_process.stdout == ""
|
|
424
348
|
|
|
425
349
|
|
|
426
|
-
def
|
|
427
|
-
"""Quoted lines are
|
|
350
|
+
def test_blockquote_does_not_force_shape_block() -> None:
|
|
351
|
+
"""Quoted lines are exempt from shape counting."""
|
|
428
352
|
completed_process = run_hook_with_message(BLOCKQUOTE_REPLY)
|
|
429
353
|
assert completed_process.returncode == 0
|
|
430
354
|
assert completed_process.stdout == ""
|
|
431
355
|
|
|
432
356
|
|
|
433
357
|
def test_table_rows_are_not_counted() -> None:
|
|
434
|
-
"""Table rows carry reference data and
|
|
358
|
+
"""Table rows carry reference data and do not create shape violations alone."""
|
|
435
359
|
completed_process = run_hook_with_message(TABLE_REPLY)
|
|
436
360
|
assert completed_process.returncode == 0
|
|
437
361
|
assert completed_process.stdout == ""
|
|
438
362
|
|
|
439
363
|
|
|
440
364
|
def test_urls_are_removed_from_counted_prose() -> None:
|
|
441
|
-
"""A link target is stripped before
|
|
365
|
+
"""A link target is stripped before prose is judged."""
|
|
442
366
|
prose_text = eli11_reply_enforcer.extract_reply_prose(
|
|
443
367
|
"The draft is at https://github.com/owner/repo/pull/704 now"
|
|
444
368
|
)
|
|
@@ -446,12 +370,21 @@ def test_urls_are_removed_from_counted_prose() -> None:
|
|
|
446
370
|
assert "draft" in prose_text
|
|
447
371
|
|
|
448
372
|
|
|
449
|
-
def
|
|
450
|
-
"""
|
|
451
|
-
|
|
373
|
+
def test_block_reason_uses_positive_rewrite_language() -> None:
|
|
374
|
+
"""Corrective output tells the model what to write, not only what failed."""
|
|
375
|
+
reason = eli11_reply_enforcer.build_block_reason(
|
|
376
|
+
[eli11_reply_enforcer.describe_action_first_violation()]
|
|
377
|
+
)
|
|
378
|
+
assert "Rewrite the reply" in reason
|
|
379
|
+
assert "120-word" not in reason
|
|
380
|
+
assert "Long form:" not in reason
|
|
381
|
+
assert "minimum" not in reason.lower()
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def test_hook_never_forces_padding_on_short_replies() -> None:
|
|
385
|
+
"""A correct short reply is not blocked for being under a word floor."""
|
|
386
|
+
tiny_reply = "Done."
|
|
387
|
+
completed_process = run_hook_with_message(tiny_reply)
|
|
452
388
|
assert completed_process.returncode == 0
|
|
453
|
-
|
|
454
|
-
assert
|
|
455
|
-
assert parsed_response["suppressOutput"] is True
|
|
456
|
-
assert parsed_response["systemMessage"]
|
|
457
|
-
assert "Long form:" in parsed_response["reason"]
|
|
389
|
+
assert completed_process.stdout == ""
|
|
390
|
+
assert eli11_reply_enforcer.find_reply_shape_violations(tiny_reply) == []
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
"""Tests for hedging_language_blocker hook response shape."""
|
|
2
2
|
|
|
3
3
|
import importlib.util
|
|
4
|
+
import io
|
|
4
5
|
import json
|
|
5
6
|
import os
|
|
6
7
|
import subprocess
|
|
7
8
|
import sys
|
|
8
9
|
import tempfile
|
|
9
10
|
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
10
13
|
HOOK_SCRIPT_PATH = os.path.join(os.path.dirname(__file__), "hedging_language_blocker.py")
|
|
11
14
|
_HOOKS_DIR = os.path.dirname(HOOK_SCRIPT_PATH)
|
|
12
15
|
_HOOKS_ROOT = os.path.join(_HOOKS_DIR, "..")
|
|
@@ -30,14 +33,22 @@ CLEAN_MESSAGE = "This is verified by the source document."
|
|
|
30
33
|
EMPTY_MESSAGE = ""
|
|
31
34
|
|
|
32
35
|
|
|
33
|
-
def run_hook_with_message(
|
|
36
|
+
def run_hook_with_message(
|
|
37
|
+
assistant_message: str, *, is_prose_style_enabled: bool = True
|
|
38
|
+
) -> subprocess.CompletedProcess:
|
|
34
39
|
hook_input_payload = json.dumps({"last_assistant_message": assistant_message})
|
|
40
|
+
environment_by_key = os.environ.copy()
|
|
41
|
+
if is_prose_style_enabled:
|
|
42
|
+
environment_by_key["CLAUDE_PROSE_STYLE_ENFORCEMENT"] = "1"
|
|
43
|
+
else:
|
|
44
|
+
environment_by_key.pop("CLAUDE_PROSE_STYLE_ENFORCEMENT", None)
|
|
35
45
|
return subprocess.run(
|
|
36
46
|
[sys.executable, HOOK_SCRIPT_PATH],
|
|
37
47
|
input=hook_input_payload,
|
|
38
48
|
capture_output=True,
|
|
39
49
|
text=True,
|
|
40
50
|
check=False,
|
|
51
|
+
env=environment_by_key,
|
|
41
52
|
)
|
|
42
53
|
|
|
43
54
|
|
|
@@ -58,6 +69,8 @@ def run_hook_with_patched_search_paths(
|
|
|
58
69
|
wrapper_file_path = wrapper_file.name
|
|
59
70
|
|
|
60
71
|
hook_input_payload = json.dumps({"last_assistant_message": assistant_message})
|
|
72
|
+
environment_by_key = os.environ.copy()
|
|
73
|
+
environment_by_key["CLAUDE_PROSE_STYLE_ENFORCEMENT"] = "1"
|
|
61
74
|
try:
|
|
62
75
|
completed_process = subprocess.run(
|
|
63
76
|
[sys.executable, wrapper_file_path],
|
|
@@ -65,6 +78,7 @@ def run_hook_with_patched_search_paths(
|
|
|
65
78
|
capture_output=True,
|
|
66
79
|
text=True,
|
|
67
80
|
check=False,
|
|
81
|
+
env=environment_by_key,
|
|
68
82
|
)
|
|
69
83
|
finally:
|
|
70
84
|
os.unlink(wrapper_file_path)
|
|
@@ -80,6 +94,49 @@ def test_user_facing_notice_matches_config_messages_module():
|
|
|
80
94
|
assert module.USER_FACING_NOTICE == USER_FACING_NOTICE
|
|
81
95
|
|
|
82
96
|
|
|
97
|
+
def test_hedging_scan_is_default_off() -> None:
|
|
98
|
+
completed_process = run_hook_with_message(
|
|
99
|
+
HEDGING_MESSAGE, is_prose_style_enabled=False
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
assert completed_process.returncode == 0
|
|
103
|
+
assert completed_process.stdout == ""
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_default_off_emits_privacy_safe_advisory_candidates(
|
|
107
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
108
|
+
) -> None:
|
|
109
|
+
all_emitted: list[tuple[str, str, str]] = []
|
|
110
|
+
|
|
111
|
+
def _record_advisory(
|
|
112
|
+
matcher_id: str, surface: str, context_text: str, **_kwargs: object
|
|
113
|
+
) -> dict[str, object]:
|
|
114
|
+
all_emitted.append((matcher_id, surface, context_text))
|
|
115
|
+
return {}
|
|
116
|
+
|
|
117
|
+
monkeypatch.setattr(
|
|
118
|
+
hedging_language_blocker, "emit_advisory_candidate", _record_advisory
|
|
119
|
+
)
|
|
120
|
+
monkeypatch.setattr(
|
|
121
|
+
hedging_language_blocker,
|
|
122
|
+
"prose_style_enforcement_enabled_in_environment",
|
|
123
|
+
lambda: False,
|
|
124
|
+
)
|
|
125
|
+
monkeypatch.setattr(
|
|
126
|
+
hedging_language_blocker.sys,
|
|
127
|
+
"stdin",
|
|
128
|
+
io.StringIO(json.dumps({"last_assistant_message": HEDGING_MESSAGE})),
|
|
129
|
+
)
|
|
130
|
+
with pytest.raises(SystemExit) as exit_info:
|
|
131
|
+
hedging_language_blocker.main()
|
|
132
|
+
assert exit_info.value.code == 0
|
|
133
|
+
assert all_emitted
|
|
134
|
+
matcher_id, surface, context_text = all_emitted[0]
|
|
135
|
+
assert matcher_id == "hedging_word"
|
|
136
|
+
assert surface == "Stop"
|
|
137
|
+
assert "likely" in context_text
|
|
138
|
+
|
|
139
|
+
|
|
83
140
|
def test_hedging_message_emits_block_with_short_user_notice():
|
|
84
141
|
completed_process = run_hook_with_message(HEDGING_MESSAGE)
|
|
85
142
|
|
|
@@ -140,3 +197,65 @@ def test_empty_message_passes_through_with_no_output():
|
|
|
140
197
|
|
|
141
198
|
assert completed_process.returncode == 0
|
|
142
199
|
assert completed_process.stdout == ""
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def test_explicit_unverified_label_in_same_sentence_passes() -> None:
|
|
203
|
+
completed_process = run_hook_with_message(
|
|
204
|
+
"This claim is unverified; the deploy is probably blocked."
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
assert completed_process.returncode == 0
|
|
208
|
+
assert completed_process.stdout == ""
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def test_i_dont_know_label_in_same_sentence_passes() -> None:
|
|
212
|
+
completed_process = run_hook_with_message(
|
|
213
|
+
"I don't know whether the port is probably open."
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
assert completed_process.returncode == 0
|
|
217
|
+
assert completed_process.stdout == ""
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_supported_probability_without_hedge_word_passes() -> None:
|
|
221
|
+
completed_process = run_hook_with_message(
|
|
222
|
+
"The suite reports 0.92 precision on the labeled fixture set in "
|
|
223
|
+
"test_prose_matcher_advisory.py."
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
assert completed_process.returncode == 0
|
|
227
|
+
assert completed_process.stdout == ""
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_label_in_one_sentence_does_not_exempt_bare_hedge_in_another() -> None:
|
|
231
|
+
completed_process = run_hook_with_message(
|
|
232
|
+
"This claim is unverified. The deploy is probably blocked."
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
assert completed_process.returncode == 0
|
|
236
|
+
parsed_response = json.loads(completed_process.stdout)
|
|
237
|
+
assert parsed_response["decision"] == "block"
|
|
238
|
+
assert "probably" in parsed_response["reason"]
|
|
239
|
+
assert "explicit uncertainty label" in parsed_response["reason"]
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_bare_probably_still_blocks_with_positive_corrective() -> None:
|
|
243
|
+
completed_process = run_hook_with_message("The deploy is probably blocked.")
|
|
244
|
+
|
|
245
|
+
assert completed_process.returncode == 0
|
|
246
|
+
parsed_response = json.loads(completed_process.stdout)
|
|
247
|
+
assert parsed_response["decision"] == "block"
|
|
248
|
+
assert "probably" in parsed_response["reason"]
|
|
249
|
+
assert "label that claim unverified" in parsed_response["reason"]
|
|
250
|
+
assert "AskUserQuestion" in parsed_response["reason"]
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def test_find_blocking_hedging_terms_is_sentence_scoped() -> None:
|
|
254
|
+
bare = hedging_language_blocker.find_blocking_hedging_terms(
|
|
255
|
+
"This claim is unverified. The deploy is probably blocked."
|
|
256
|
+
)
|
|
257
|
+
labeled = hedging_language_blocker.find_blocking_hedging_terms(
|
|
258
|
+
"This claim is unverified; the deploy is probably blocked."
|
|
259
|
+
)
|
|
260
|
+
assert bare == ["probably"]
|
|
261
|
+
assert labeled == []
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import importlib.util
|
|
4
4
|
import io
|
|
5
5
|
import json
|
|
6
|
+
import os
|
|
6
7
|
import pathlib
|
|
7
8
|
import sys
|
|
8
9
|
from unittest import mock
|
|
@@ -192,14 +193,33 @@ def test_written_content_reads_edit_new_string() -> None:
|
|
|
192
193
|
assert written_content("Edit", edit_input) == "edited body"
|
|
193
194
|
|
|
194
195
|
|
|
195
|
-
def _run_main_with_io(
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
196
|
+
def _run_main_with_io(
|
|
197
|
+
input_text: str, *, is_prose_style_enabled: bool = True
|
|
198
|
+
) -> str:
|
|
199
|
+
environment_by_key = os.environ.copy()
|
|
200
|
+
if is_prose_style_enabled:
|
|
201
|
+
environment_by_key["CLAUDE_PROSE_STYLE_ENFORCEMENT"] = "1"
|
|
202
|
+
else:
|
|
203
|
+
environment_by_key.pop("CLAUDE_PROSE_STYLE_ENFORCEMENT", None)
|
|
204
|
+
with mock.patch.dict(os.environ, environment_by_key, clear=True):
|
|
205
|
+
with mock.patch("sys.stdin", io.StringIO(input_text)):
|
|
206
|
+
with mock.patch("sys.stdout", new_callable=io.StringIO) as mock_stdout:
|
|
207
|
+
try:
|
|
208
|
+
hook_module.main()
|
|
209
|
+
except SystemExit:
|
|
210
|
+
pass
|
|
211
|
+
return mock_stdout.getvalue()
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_main_is_default_off_for_overstated_hook_module_write() -> None:
|
|
215
|
+
hook_input = {
|
|
216
|
+
"tool_name": "Write",
|
|
217
|
+
"tool_input": {
|
|
218
|
+
"file_path": "/repo/hooks/hooks_constants/some_blocker_constants.py",
|
|
219
|
+
"content": _OVERSTATED_MESSAGE_MODULE,
|
|
220
|
+
},
|
|
221
|
+
}
|
|
222
|
+
assert _run_main_with_io(json.dumps(hook_input), is_prose_style_enabled=False) == ""
|
|
203
223
|
|
|
204
224
|
|
|
205
225
|
def test_main_blocks_overstated_hook_module_write() -> None:
|