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
|
@@ -42,24 +42,40 @@ USER_DIRECTED_NEXT_STEPS_MESSAGE = (
|
|
|
42
42
|
EMPTY_MESSAGE = ""
|
|
43
43
|
|
|
44
44
|
|
|
45
|
-
def run_hook_with_message(
|
|
45
|
+
def run_hook_with_message(
|
|
46
|
+
assistant_message: str, *, is_prose_style_enabled: bool = True
|
|
47
|
+
) -> subprocess.CompletedProcess:
|
|
46
48
|
hook_input_payload = json.dumps({"last_assistant_message": assistant_message})
|
|
49
|
+
environment_by_key = os.environ.copy()
|
|
50
|
+
if is_prose_style_enabled:
|
|
51
|
+
environment_by_key["CLAUDE_PROSE_STYLE_ENFORCEMENT"] = "1"
|
|
52
|
+
else:
|
|
53
|
+
environment_by_key.pop("CLAUDE_PROSE_STYLE_ENFORCEMENT", None)
|
|
47
54
|
return subprocess.run(
|
|
48
55
|
[sys.executable, HOOK_SCRIPT_PATH],
|
|
49
56
|
input=hook_input_payload,
|
|
50
57
|
capture_output=True,
|
|
51
58
|
text=True,
|
|
52
59
|
check=False,
|
|
60
|
+
env=environment_by_key,
|
|
53
61
|
)
|
|
54
62
|
|
|
55
63
|
|
|
56
|
-
def run_hook_with_payload(
|
|
64
|
+
def run_hook_with_payload(
|
|
65
|
+
hook_input_payload: dict, *, is_prose_style_enabled: bool = True
|
|
66
|
+
) -> subprocess.CompletedProcess:
|
|
67
|
+
environment_by_key = os.environ.copy()
|
|
68
|
+
if is_prose_style_enabled:
|
|
69
|
+
environment_by_key["CLAUDE_PROSE_STYLE_ENFORCEMENT"] = "1"
|
|
70
|
+
else:
|
|
71
|
+
environment_by_key.pop("CLAUDE_PROSE_STYLE_ENFORCEMENT", None)
|
|
57
72
|
return subprocess.run(
|
|
58
73
|
[sys.executable, HOOK_SCRIPT_PATH],
|
|
59
74
|
input=json.dumps(hook_input_payload),
|
|
60
75
|
capture_output=True,
|
|
61
76
|
text=True,
|
|
62
77
|
check=False,
|
|
78
|
+
env=environment_by_key,
|
|
63
79
|
)
|
|
64
80
|
|
|
65
81
|
|
|
@@ -76,6 +92,15 @@ def test_user_facing_notice_matches_config_messages_module():
|
|
|
76
92
|
)
|
|
77
93
|
|
|
78
94
|
|
|
95
|
+
def test_intent_scan_is_default_off() -> None:
|
|
96
|
+
completed_process = run_hook_with_message(
|
|
97
|
+
INTENT_ENDING_MESSAGE, is_prose_style_enabled=False
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
assert completed_process.returncode == 0
|
|
101
|
+
assert completed_process.stdout == ""
|
|
102
|
+
|
|
103
|
+
|
|
79
104
|
def test_intent_ending_message_emits_block_with_short_user_notice():
|
|
80
105
|
completed_process = run_hook_with_message(INTENT_ENDING_MESSAGE)
|
|
81
106
|
|
|
@@ -367,9 +367,16 @@ def test_find_stale_inventory_returns_survey_for_omission(tmp_path: Path):
|
|
|
367
367
|
assert survey.named_basenames == {"dialer_compose.py", "compose_dialer_cli.py"}
|
|
368
368
|
|
|
369
369
|
|
|
370
|
-
def
|
|
371
|
-
|
|
372
|
-
|
|
370
|
+
def test_find_stale_inventory_skips_directory_below_minimum_named_entries(
|
|
371
|
+
tmp_path: Path,
|
|
372
|
+
) -> None:
|
|
373
|
+
"""Skip a package inventory that names fewer than the minimum on-disk siblings."""
|
|
374
|
+
package_directory = tmp_path / "prose_package"
|
|
375
|
+
package_directory.mkdir()
|
|
376
|
+
(package_directory / "CLAUDE.md").write_text(
|
|
377
|
+
"| File | Role |\n|---|---|\n| `only_one.py` | sole entry |\n",
|
|
378
|
+
encoding="utf-8",
|
|
373
379
|
)
|
|
374
|
-
|
|
380
|
+
(package_directory / "only_one.py").write_text("FLAG = 1\n", encoding="utf-8")
|
|
381
|
+
new_file_path = package_directory / "new_helper.py"
|
|
375
382
|
assert find_stale_inventory(str(new_file_path)) is None
|
|
@@ -42,18 +42,31 @@ strip_non_prose_regions = hook_module.strip_non_prose_regions
|
|
|
42
42
|
build_block_reason = hook_module.build_block_reason
|
|
43
43
|
find_question_block_violations = hook_module.find_question_block_violations
|
|
44
44
|
build_lean_block_reason = hook_module.build_lean_block_reason
|
|
45
|
-
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def evaluate(payload_by_key: dict[str, object]) -> str | None:
|
|
48
|
+
"""Call evaluate with opinionated prose enforcement enabled."""
|
|
49
|
+
with patch.dict(os.environ, {"CLAUDE_PROSE_STYLE_ENFORCEMENT": "1"}):
|
|
50
|
+
return hook_module.evaluate(payload_by_key)
|
|
46
51
|
|
|
47
52
|
from pre_tool_use_dispatcher import NativeHook, run_native_hook # noqa: E402
|
|
48
53
|
|
|
49
54
|
|
|
50
|
-
def _run_hook_with_payload(
|
|
55
|
+
def _run_hook_with_payload(
|
|
56
|
+
payload: dict, *, is_prose_style_enabled: bool = True
|
|
57
|
+
) -> subprocess.CompletedProcess[str]:
|
|
58
|
+
environment_by_key = os.environ.copy()
|
|
59
|
+
if is_prose_style_enabled:
|
|
60
|
+
environment_by_key["CLAUDE_PROSE_STYLE_ENFORCEMENT"] = "1"
|
|
61
|
+
else:
|
|
62
|
+
environment_by_key.pop("CLAUDE_PROSE_STYLE_ENFORCEMENT", None)
|
|
51
63
|
return subprocess.run(
|
|
52
64
|
[sys.executable, str(HOOK_SCRIPT_PATH)],
|
|
53
65
|
input=json.dumps(payload),
|
|
54
66
|
capture_output=True,
|
|
55
67
|
text=True,
|
|
56
68
|
check=False,
|
|
69
|
+
env=environment_by_key,
|
|
57
70
|
)
|
|
58
71
|
|
|
59
72
|
|
|
@@ -64,10 +77,81 @@ def _decision_from(completed: subprocess.CompletedProcess[str]) -> str | None:
|
|
|
64
77
|
return parsed.get("hookSpecificOutput", {}).get("permissionDecision")
|
|
65
78
|
|
|
66
79
|
|
|
80
|
+
def _system_message_from(completed: subprocess.CompletedProcess[str]) -> str | None:
|
|
81
|
+
if not completed.stdout:
|
|
82
|
+
return None
|
|
83
|
+
parsed = json.loads(completed.stdout)
|
|
84
|
+
system_message = parsed.get("systemMessage")
|
|
85
|
+
return system_message if isinstance(system_message, str) else None
|
|
86
|
+
|
|
87
|
+
|
|
67
88
|
def test_canonical_hook_script_exists_at_expected_path() -> None:
|
|
68
89
|
assert HOOK_SCRIPT_PATH.is_file()
|
|
69
90
|
|
|
70
91
|
|
|
92
|
+
def test_heavy_word_scan_is_default_off(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
93
|
+
monkeypatch.delenv("CLAUDE_PROSE_STYLE_ENFORCEMENT", raising=False)
|
|
94
|
+
payload = {
|
|
95
|
+
"tool_name": "Write",
|
|
96
|
+
"tool_input": {
|
|
97
|
+
"file_path": "notes.md",
|
|
98
|
+
"content": "We initiate the worker pool at boot.",
|
|
99
|
+
},
|
|
100
|
+
}
|
|
101
|
+
assert hook_module.evaluate(payload) is None
|
|
102
|
+
completed = _run_hook_with_payload(payload, is_prose_style_enabled=False)
|
|
103
|
+
assert _decision_from(completed) is None
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_default_off_emits_privacy_safe_advisory_candidates(
|
|
107
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
108
|
+
) -> None:
|
|
109
|
+
monkeypatch.delenv("CLAUDE_PROSE_STYLE_ENFORCEMENT", raising=False)
|
|
110
|
+
all_emitted: list[tuple[str, str, str]] = []
|
|
111
|
+
|
|
112
|
+
def _record_advisory(
|
|
113
|
+
matcher_id: str, surface: str, context_text: str, **_kwargs: object
|
|
114
|
+
) -> dict[str, object]:
|
|
115
|
+
all_emitted.append((matcher_id, surface, context_text))
|
|
116
|
+
return {}
|
|
117
|
+
|
|
118
|
+
monkeypatch.setattr(hook_module, "emit_advisory_candidate", _record_advisory)
|
|
119
|
+
payload = {
|
|
120
|
+
"tool_name": "Write",
|
|
121
|
+
"tool_input": {
|
|
122
|
+
"file_path": "notes.md",
|
|
123
|
+
"content": "We initiate the worker pool at boot.",
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
assert hook_module.evaluate(payload) is None
|
|
127
|
+
assert all_emitted
|
|
128
|
+
matcher_id, surface, context_text = all_emitted[0]
|
|
129
|
+
assert matcher_id == "plain_language_heavy_word"
|
|
130
|
+
assert surface == "Write"
|
|
131
|
+
assert "initiate" in context_text
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def test_heavy_word_scan_opt_in_allows_with_system_message_swaps() -> None:
|
|
135
|
+
payload = {
|
|
136
|
+
"tool_name": "Write",
|
|
137
|
+
"tool_input": {
|
|
138
|
+
"file_path": "notes.md",
|
|
139
|
+
"content": "We initiate the worker pool at boot.",
|
|
140
|
+
},
|
|
141
|
+
}
|
|
142
|
+
assert evaluate(payload) is None
|
|
143
|
+
with patch.dict(os.environ, {"CLAUDE_PROSE_STYLE_ENFORCEMENT": "1"}):
|
|
144
|
+
deny_reason, advisory_message = hook_module.evaluate_with_advisory(payload)
|
|
145
|
+
assert deny_reason is None
|
|
146
|
+
assert advisory_message is not None
|
|
147
|
+
assert 'use "start" instead of "initiate"' in advisory_message
|
|
148
|
+
completed = _run_hook_with_payload(payload, is_prose_style_enabled=True)
|
|
149
|
+
assert _decision_from(completed) == "allow"
|
|
150
|
+
system_message = _system_message_from(completed)
|
|
151
|
+
assert system_message is not None
|
|
152
|
+
assert 'use "start" instead of "initiate"' in system_message
|
|
153
|
+
|
|
154
|
+
|
|
71
155
|
def test_bare_prose_banned_term_is_detected() -> None:
|
|
72
156
|
matched = find_banned_terms("We initiate the worker pool at boot.")
|
|
73
157
|
assert any(each_term == "initiate" for each_term, _replacement in matched)
|
|
@@ -127,7 +211,7 @@ def test_block_reason_names_term_and_replacement() -> None:
|
|
|
127
211
|
assert "start" in reason
|
|
128
212
|
|
|
129
213
|
|
|
130
|
-
def
|
|
214
|
+
def test_ask_user_question_with_banned_term_is_advisory_allow() -> None:
|
|
131
215
|
payload = {
|
|
132
216
|
"tool_name": "AskUserQuestion",
|
|
133
217
|
"tool_input": {
|
|
@@ -141,10 +225,13 @@ def test_ask_user_question_with_banned_term_is_denied() -> None:
|
|
|
141
225
|
},
|
|
142
226
|
}
|
|
143
227
|
completed = _run_hook_with_payload(payload)
|
|
144
|
-
assert _decision_from(completed) == "
|
|
228
|
+
assert _decision_from(completed) == "allow"
|
|
229
|
+
system_message = _system_message_from(completed)
|
|
230
|
+
assert system_message is not None
|
|
231
|
+
assert "utilize" in system_message
|
|
145
232
|
|
|
146
233
|
|
|
147
|
-
def
|
|
234
|
+
def test_ask_user_question_banned_term_in_option_label_is_advisory_allow() -> None:
|
|
148
235
|
payload = {
|
|
149
236
|
"tool_name": "AskUserQuestion",
|
|
150
237
|
"tool_input": {
|
|
@@ -158,7 +245,10 @@ def test_ask_user_question_banned_term_in_option_label_is_denied() -> None:
|
|
|
158
245
|
},
|
|
159
246
|
}
|
|
160
247
|
completed = _run_hook_with_payload(payload)
|
|
161
|
-
assert _decision_from(completed) == "
|
|
248
|
+
assert _decision_from(completed) == "allow"
|
|
249
|
+
system_message = _system_message_from(completed)
|
|
250
|
+
assert system_message is not None
|
|
251
|
+
assert "utilize" in system_message
|
|
162
252
|
|
|
163
253
|
|
|
164
254
|
def test_clean_ask_user_question_passes_through() -> None:
|
|
@@ -178,7 +268,7 @@ def test_clean_ask_user_question_passes_through() -> None:
|
|
|
178
268
|
assert _decision_from(completed) is None
|
|
179
269
|
|
|
180
270
|
|
|
181
|
-
def
|
|
271
|
+
def test_write_markdown_with_banned_term_is_advisory_allow(tmp_path: Path) -> None:
|
|
182
272
|
target = tmp_path / "notes.md"
|
|
183
273
|
payload = {
|
|
184
274
|
"tool_name": "Write",
|
|
@@ -188,7 +278,10 @@ def test_write_markdown_with_banned_term_is_denied(tmp_path: Path) -> None:
|
|
|
188
278
|
},
|
|
189
279
|
}
|
|
190
280
|
completed = _run_hook_with_payload(payload)
|
|
191
|
-
assert _decision_from(completed) == "
|
|
281
|
+
assert _decision_from(completed) == "allow"
|
|
282
|
+
system_message = _system_message_from(completed)
|
|
283
|
+
assert system_message is not None
|
|
284
|
+
assert "utilize" in system_message
|
|
192
285
|
|
|
193
286
|
|
|
194
287
|
def test_write_markdown_at_ephemeral_path_is_allowed(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
@@ -230,7 +323,7 @@ def test_edit_markdown_clean_content_passes_through(tmp_path: Path) -> None:
|
|
|
230
323
|
assert _decision_from(completed) is None
|
|
231
324
|
|
|
232
325
|
|
|
233
|
-
def
|
|
326
|
+
def test_multiedit_markdown_with_banned_term_is_advisory_allow(tmp_path: Path) -> None:
|
|
234
327
|
target = tmp_path / "notes.md"
|
|
235
328
|
payload = {
|
|
236
329
|
"tool_name": "MultiEdit",
|
|
@@ -243,7 +336,10 @@ def test_multiedit_markdown_with_banned_term_is_denied(tmp_path: Path) -> None:
|
|
|
243
336
|
},
|
|
244
337
|
}
|
|
245
338
|
completed = _run_hook_with_payload(payload)
|
|
246
|
-
assert _decision_from(completed) == "
|
|
339
|
+
assert _decision_from(completed) == "allow"
|
|
340
|
+
system_message = _system_message_from(completed)
|
|
341
|
+
assert system_message is not None
|
|
342
|
+
assert "utilize" in system_message
|
|
247
343
|
|
|
248
344
|
|
|
249
345
|
def test_other_tool_is_ignored() -> None:
|
|
@@ -270,19 +366,24 @@ def test_real_file_path_is_still_stripped() -> None:
|
|
|
270
366
|
|
|
271
367
|
|
|
272
368
|
def test_native_dispatch_path_logs_the_block(tmp_path: Path) -> None:
|
|
273
|
-
"""A deny routed through the dispatcher's native path logs
|
|
369
|
+
"""A lean-block deny routed through the dispatcher's native path logs once.
|
|
274
370
|
|
|
275
371
|
On the Write|Edit|MultiEdit surface this hook runs only through
|
|
276
372
|
pre_tool_use_dispatcher's native path, which calls evaluate() and
|
|
277
|
-
build_deny_payload() — never _emit_deny() or main().
|
|
278
|
-
|
|
279
|
-
|
|
373
|
+
build_deny_payload() — never _emit_deny() or main(). Heavy words no longer
|
|
374
|
+
deny (OP-07D), so this probe uses AskUserQuestion lean-block chat detail.
|
|
375
|
+
The log call still lives on build_deny_payload.
|
|
280
376
|
"""
|
|
281
377
|
deny_payload = {
|
|
282
|
-
"tool_name": "
|
|
378
|
+
"tool_name": "AskUserQuestion",
|
|
283
379
|
"tool_input": {
|
|
284
|
-
"
|
|
285
|
-
|
|
380
|
+
"questions": [
|
|
381
|
+
{
|
|
382
|
+
"question": "Which gate should run first?\n\nPlan the whole stack.",
|
|
383
|
+
"header": "Gate",
|
|
384
|
+
"options": [{"label": "Write", "description": "Runs on write."}],
|
|
385
|
+
}
|
|
386
|
+
]
|
|
286
387
|
},
|
|
287
388
|
}
|
|
288
389
|
native_hook = NativeHook(
|
|
@@ -291,7 +392,9 @@ def test_native_dispatch_path_logs_the_block(tmp_path: Path) -> None:
|
|
|
291
392
|
)
|
|
292
393
|
|
|
293
394
|
with patch.object(Path, "home", return_value=tmp_path):
|
|
294
|
-
hosted_result = run_native_hook(
|
|
395
|
+
hosted_result = run_native_hook(
|
|
396
|
+
native_hook, deny_payload, is_blocking=True
|
|
397
|
+
)
|
|
295
398
|
|
|
296
399
|
assert hosted_result.captured_stdout
|
|
297
400
|
log_path = tmp_path / ".claude" / "logs" / "hook-blocks.log"
|
|
@@ -527,7 +630,14 @@ def test_markdown_write_keeps_its_bullets_and_tables(tmp_path: Path) -> None:
|
|
|
527
630
|
"tool_input": {"file_path": target_path, "content": bulleted_prose},
|
|
528
631
|
}
|
|
529
632
|
|
|
530
|
-
assert evaluate(heavy_payload) is
|
|
633
|
+
assert evaluate(heavy_payload) is None
|
|
634
|
+
with patch.dict(os.environ, {"CLAUDE_PROSE_STYLE_ENFORCEMENT": "1"}):
|
|
635
|
+
_deny_reason, advisory_message = hook_module.evaluate_with_advisory(
|
|
636
|
+
heavy_payload
|
|
637
|
+
)
|
|
638
|
+
assert _deny_reason is None
|
|
639
|
+
assert advisory_message is not None
|
|
640
|
+
assert "utilize" in advisory_message
|
|
531
641
|
assert evaluate(clean_payload) is None
|
|
532
642
|
|
|
533
643
|
|
|
@@ -2,12 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
import importlib.util
|
|
4
4
|
import json
|
|
5
|
+
import os
|
|
5
6
|
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
6
9
|
from types import ModuleType
|
|
10
|
+
from unittest.mock import patch
|
|
7
11
|
|
|
8
12
|
BLOCKER_PATH = Path(__file__).parent / "plain_language_blocker.py"
|
|
9
13
|
ALLOWLIST_RELATIVE_PATH = Path(".claude") / "plain-language-allow.json"
|
|
10
14
|
ALWAYS_HEAVY_WORD = "utilize"
|
|
15
|
+
_PROSE_STYLE_ENFORCEMENT_ENVIRONMENT = {
|
|
16
|
+
"CLAUDE_PROSE_STYLE_ENFORCEMENT": "1",
|
|
17
|
+
}
|
|
11
18
|
|
|
12
19
|
|
|
13
20
|
def _load_blocker() -> ModuleType:
|
|
@@ -22,6 +29,24 @@ def _load_blocker() -> ModuleType:
|
|
|
22
29
|
|
|
23
30
|
_BLOCKER = _load_blocker()
|
|
24
31
|
|
|
32
|
+
@pytest.fixture(autouse=True)
|
|
33
|
+
def enable_prose_style_enforcement(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
34
|
+
"""Opinionated heavy-word scan is default-off; these tests arm it."""
|
|
35
|
+
monkeypatch.setenv("CLAUDE_PROSE_STYLE_ENFORCEMENT", "1")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _evaluate_advisory(
|
|
40
|
+
payload_by_key: dict[str, object],
|
|
41
|
+
) -> tuple[str | None, str | None]:
|
|
42
|
+
"""Return (deny_reason, advisory_message) with prose enforcement on.
|
|
43
|
+
|
|
44
|
+
Heavy words never deny (OP-07D); allowlist coverage asserts on the advisory
|
|
45
|
+
message and on ``find_banned_terms`` so the scanner still sees domain terms.
|
|
46
|
+
"""
|
|
47
|
+
with patch.dict(os.environ, _PROSE_STYLE_ENFORCEMENT_ENVIRONMENT):
|
|
48
|
+
return _BLOCKER.evaluate_with_advisory(payload_by_key)
|
|
49
|
+
|
|
25
50
|
|
|
26
51
|
def _init_repo(root: Path) -> Path:
|
|
27
52
|
(root / ".git").mkdir(parents=True, exist_ok=True)
|
|
@@ -63,15 +88,17 @@ def test_allowlisted_word_passes_in_markdown_write(tmp_path: Path) -> None:
|
|
|
63
88
|
project_with_allowlist = _init_repo(tmp_path / "domain")
|
|
64
89
|
_write_allowlist(project_with_allowlist, ["submit"])
|
|
65
90
|
|
|
66
|
-
|
|
91
|
+
control_deny, control_advisory = _evaluate_advisory(
|
|
67
92
|
_markdown_write_payload(project_without_allowlist, prose)
|
|
68
93
|
)
|
|
69
|
-
|
|
94
|
+
allowlisted_deny, allowlisted_advisory = _evaluate_advisory(
|
|
70
95
|
_markdown_write_payload(project_with_allowlist, prose)
|
|
71
96
|
)
|
|
72
97
|
|
|
73
|
-
assert
|
|
74
|
-
assert
|
|
98
|
+
assert control_deny is None
|
|
99
|
+
assert control_advisory is not None and "submit" in control_advisory
|
|
100
|
+
assert allowlisted_deny is None
|
|
101
|
+
assert allowlisted_advisory is None
|
|
75
102
|
|
|
76
103
|
|
|
77
104
|
def test_allowlisted_word_passes_in_ask_user_question(tmp_path: Path) -> None:
|
|
@@ -80,26 +107,29 @@ def test_allowlisted_word_passes_in_ask_user_question(tmp_path: Path) -> None:
|
|
|
80
107
|
project_with_allowlist = _init_repo(tmp_path / "domain")
|
|
81
108
|
_write_allowlist(project_with_allowlist, ["identify"])
|
|
82
109
|
|
|
83
|
-
|
|
110
|
+
control_deny, control_advisory = _evaluate_advisory(
|
|
84
111
|
_ask_user_question_payload(project_without_allowlist, prose)
|
|
85
112
|
)
|
|
86
|
-
|
|
113
|
+
allowlisted_deny, allowlisted_advisory = _evaluate_advisory(
|
|
87
114
|
_ask_user_question_payload(project_with_allowlist, prose)
|
|
88
115
|
)
|
|
89
116
|
|
|
90
|
-
assert
|
|
91
|
-
assert
|
|
117
|
+
assert control_deny is None
|
|
118
|
+
assert control_advisory is not None and "identify" in control_advisory
|
|
119
|
+
assert allowlisted_deny is None
|
|
120
|
+
assert allowlisted_advisory is None
|
|
92
121
|
|
|
93
122
|
|
|
94
|
-
def
|
|
123
|
+
def test_non_allowlisted_heavy_word_still_flagged(tmp_path: Path) -> None:
|
|
95
124
|
_init_repo(tmp_path)
|
|
96
125
|
_write_allowlist(tmp_path, ["submit"])
|
|
97
126
|
|
|
98
|
-
deny_reason =
|
|
127
|
+
deny_reason, advisory_message = _evaluate_advisory(
|
|
99
128
|
_markdown_write_payload(tmp_path, f"Please {ALWAYS_HEAVY_WORD} the cache now.")
|
|
100
129
|
)
|
|
101
130
|
|
|
102
|
-
assert deny_reason is
|
|
131
|
+
assert deny_reason is None
|
|
132
|
+
assert advisory_message is not None and ALWAYS_HEAVY_WORD in advisory_message
|
|
103
133
|
|
|
104
134
|
|
|
105
135
|
def test_allowlist_match_is_case_insensitive(tmp_path: Path) -> None:
|
|
@@ -108,26 +138,29 @@ def test_allowlist_match_is_case_insensitive(tmp_path: Path) -> None:
|
|
|
108
138
|
project_with_allowlist = _init_repo(tmp_path / "domain")
|
|
109
139
|
_write_allowlist(project_with_allowlist, ["Submit"])
|
|
110
140
|
|
|
111
|
-
|
|
141
|
+
control_deny, control_advisory = _evaluate_advisory(
|
|
112
142
|
_markdown_write_payload(project_without_allowlist, prose)
|
|
113
143
|
)
|
|
114
|
-
|
|
144
|
+
allowlisted_deny, allowlisted_advisory = _evaluate_advisory(
|
|
115
145
|
_markdown_write_payload(project_with_allowlist, prose)
|
|
116
146
|
)
|
|
117
147
|
|
|
118
|
-
assert
|
|
119
|
-
assert
|
|
148
|
+
assert control_deny is None
|
|
149
|
+
assert control_advisory is not None and "submit" in control_advisory
|
|
150
|
+
assert allowlisted_deny is None
|
|
151
|
+
assert allowlisted_advisory is None
|
|
120
152
|
|
|
121
153
|
|
|
122
154
|
def test_malformed_allowlist_json_is_ignored(tmp_path: Path) -> None:
|
|
123
155
|
_init_repo(tmp_path)
|
|
124
156
|
_write_raw_allowlist(tmp_path, "{ this is not valid json ")
|
|
125
157
|
|
|
126
|
-
deny_reason =
|
|
158
|
+
deny_reason, advisory_message = _evaluate_advisory(
|
|
127
159
|
_markdown_write_payload(tmp_path, "Please submit the release notes.")
|
|
128
160
|
)
|
|
129
161
|
|
|
130
|
-
assert deny_reason is
|
|
162
|
+
assert deny_reason is None
|
|
163
|
+
assert advisory_message is not None and "submit" in advisory_message
|
|
131
164
|
|
|
132
165
|
|
|
133
166
|
def test_allowlist_in_a_different_project_root_is_not_applied(tmp_path: Path) -> None:
|
|
@@ -135,43 +168,54 @@ def test_allowlist_in_a_different_project_root_is_not_applied(tmp_path: Path) ->
|
|
|
135
168
|
project_without_allowlist = _init_repo(tmp_path / "project_b")
|
|
136
169
|
_write_allowlist(project_with_allowlist, ["submit"])
|
|
137
170
|
|
|
138
|
-
deny_reason =
|
|
171
|
+
deny_reason, advisory_message = _evaluate_advisory(
|
|
139
172
|
_markdown_write_payload(project_without_allowlist, "Please submit the notes.")
|
|
140
173
|
)
|
|
141
174
|
|
|
142
|
-
assert deny_reason is
|
|
175
|
+
assert deny_reason is None
|
|
176
|
+
assert advisory_message is not None and "submit" in advisory_message
|
|
143
177
|
|
|
144
178
|
|
|
145
179
|
def test_allowlist_above_the_repo_root_is_not_applied(tmp_path: Path) -> None:
|
|
146
180
|
_write_allowlist(tmp_path, ["submit"])
|
|
147
181
|
repository_root = _init_repo(tmp_path / "repo")
|
|
148
182
|
|
|
149
|
-
deny_reason =
|
|
183
|
+
deny_reason, advisory_message = _evaluate_advisory(
|
|
150
184
|
_markdown_write_payload(repository_root, "Please submit the notes.")
|
|
151
185
|
)
|
|
152
186
|
|
|
153
|
-
assert deny_reason is
|
|
187
|
+
assert deny_reason is None
|
|
188
|
+
assert advisory_message is not None and "submit" in advisory_message
|
|
154
189
|
|
|
155
190
|
|
|
156
191
|
def test_allowlist_at_the_repo_root_is_applied(tmp_path: Path) -> None:
|
|
157
192
|
repository_root = _init_repo(tmp_path / "repo")
|
|
158
193
|
_write_allowlist(repository_root, ["submit"])
|
|
159
194
|
|
|
160
|
-
deny_reason =
|
|
195
|
+
deny_reason, advisory_message = _evaluate_advisory(
|
|
161
196
|
_markdown_write_payload(repository_root, "Please submit the notes.")
|
|
162
197
|
)
|
|
163
198
|
|
|
164
199
|
assert deny_reason is None
|
|
200
|
+
assert advisory_message is None
|
|
165
201
|
|
|
166
202
|
|
|
167
203
|
def test_allowlist_without_a_repo_root_is_not_applied(tmp_path: Path) -> None:
|
|
204
|
+
"""An allowlist only applies when the walk finds a ``.git`` repository root.
|
|
205
|
+
|
|
206
|
+
The upward walk can hit a personal home ``.git`` on some developer machines,
|
|
207
|
+
so this case stubs the finder to the no-repository outcome rather than
|
|
208
|
+
relying on the ambient filesystem.
|
|
209
|
+
"""
|
|
168
210
|
_write_allowlist(tmp_path, ["submit"])
|
|
169
211
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
212
|
+
with patch.object(_BLOCKER, "_find_project_allowlist_file", return_value=None):
|
|
213
|
+
deny_reason, advisory_message = _evaluate_advisory(
|
|
214
|
+
_markdown_write_payload(tmp_path, "Please submit the notes.")
|
|
215
|
+
)
|
|
173
216
|
|
|
174
|
-
assert deny_reason is
|
|
217
|
+
assert deny_reason is None
|
|
218
|
+
assert advisory_message is not None and "submit" in advisory_message
|
|
175
219
|
|
|
176
220
|
|
|
177
221
|
def test_find_banned_terms_skips_allowlisted_terms() -> None:
|