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
|
@@ -20,6 +20,9 @@ if _hooks_dir not in sys.path:
|
|
|
20
20
|
sys.path.insert(0, _hooks_dir)
|
|
21
21
|
|
|
22
22
|
from blocking.code_rules_shared import is_ephemeral_path # noqa: E402
|
|
23
|
+
from blocking.config.prose_style_enforcement_constants import ( # noqa: E402
|
|
24
|
+
prose_style_enforcement_enabled_in_environment,
|
|
25
|
+
)
|
|
23
26
|
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
24
27
|
from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin # noqa: E402
|
|
25
28
|
from hooks_constants.state_description_blocker_constants import ( # noqa: E402
|
|
@@ -278,7 +281,8 @@ def evaluate(payload_by_key: dict[str, object]) -> str | None:
|
|
|
278
281
|
|
|
279
282
|
Applies the same tool-name gate, file-extension gate, content selection, and
|
|
280
283
|
pattern scan the standalone hook applies. Returns the deny-reason text when a
|
|
281
|
-
historical phrase is found, or None to allow.
|
|
284
|
+
historical phrase is found, or None to allow. Opinionated prose enforcement
|
|
285
|
+
is default-off via ``prose_style_enforcement_enabled_in_environment()``.
|
|
282
286
|
|
|
283
287
|
Args:
|
|
284
288
|
payload_by_key: The PreToolUse payload with tool_name and tool_input.
|
|
@@ -287,6 +291,8 @@ def evaluate(payload_by_key: dict[str, object]) -> str | None:
|
|
|
287
291
|
The permissionDecisionReason text when the write is denied, or None when
|
|
288
292
|
the write is allowed.
|
|
289
293
|
"""
|
|
294
|
+
if not prose_style_enforcement_enabled_in_environment():
|
|
295
|
+
return None
|
|
290
296
|
raw_tool_name = payload_by_key.get("tool_name", "")
|
|
291
297
|
tool_name = raw_tool_name if isinstance(raw_tool_name, str) else ""
|
|
292
298
|
if tool_name not in ("Write", "Edit"):
|
|
@@ -17,6 +17,7 @@ try:
|
|
|
17
17
|
if each_bootstrap_directory not in sys.path:
|
|
18
18
|
sys.path.insert(0, each_bootstrap_directory)
|
|
19
19
|
from code_rules_shared import (
|
|
20
|
+
is_agent_home_tooling,
|
|
20
21
|
is_ephemeral_script_path,
|
|
21
22
|
is_under_session_scratchpad,
|
|
22
23
|
)
|
|
@@ -40,6 +41,11 @@ def _is_session_scratchpad_write(file_path: str, input_data: dict) -> bool:
|
|
|
40
41
|
return is_under_session_scratchpad(file_path, input_data)
|
|
41
42
|
|
|
42
43
|
|
|
44
|
+
def _is_agent_home_tooling_write(file_path: str) -> bool:
|
|
45
|
+
"""A coding agent's own home-directory tooling is outside the TDD gate."""
|
|
46
|
+
return is_agent_home_tooling(file_path)
|
|
47
|
+
|
|
48
|
+
|
|
43
49
|
production_extensions = path_classification.production_extensions
|
|
44
50
|
skip_extensions = path_classification.skip_extensions
|
|
45
51
|
_is_inside_dotclaude_segment = path_classification._is_inside_dotclaude_segment
|
|
@@ -69,6 +75,8 @@ def _resolve_payload(input_data: dict) -> tuple[str, dict, str]:
|
|
|
69
75
|
def _is_silently_skipped(file_path: str, extension: str, path: Path) -> bool:
|
|
70
76
|
if _is_inside_dotclaude_segment(file_path) or is_ephemeral_script_path(file_path):
|
|
71
77
|
return True
|
|
78
|
+
if _is_agent_home_tooling_write(file_path):
|
|
79
|
+
return True
|
|
72
80
|
if extension in skip_extensions():
|
|
73
81
|
return True
|
|
74
82
|
if extension not in production_extensions():
|
|
@@ -37,3 +37,31 @@ def test_importing_path_setup_puts_the_hooks_directory_on_sys_path() -> None:
|
|
|
37
37
|
)
|
|
38
38
|
assert completed.returncode == 0, completed.stderr
|
|
39
39
|
assert completed.stdout.strip() == "ok"
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_importing_path_setup_puts_the_blocking_directory_on_sys_path() -> None:
|
|
43
|
+
"""A module reached through the package finds its own directory on sys.path.
|
|
44
|
+
|
|
45
|
+
``code_rules_shared`` imports both ``hooks_constants`` (a hooks-directory
|
|
46
|
+
package) and its blocking-directory siblings, so the bootstrap must place
|
|
47
|
+
both roots. Only the hooks directory is on PYTHONPATH here, so the blocking
|
|
48
|
+
entry can only come from the bootstrap itself.
|
|
49
|
+
"""
|
|
50
|
+
driver = (
|
|
51
|
+
"import sys\n"
|
|
52
|
+
"from blocking import _path_setup\n"
|
|
53
|
+
"from pathlib import Path\n"
|
|
54
|
+
"blocking_dir = str(Path(_path_setup.__file__).resolve().parent)\n"
|
|
55
|
+
"assert blocking_dir in sys.path, blocking_dir\n"
|
|
56
|
+
"print('ok')\n"
|
|
57
|
+
)
|
|
58
|
+
subprocess_environment = {**os.environ, "PYTHONPATH": str(_HOOKS_DIR)}
|
|
59
|
+
completed = subprocess.run(
|
|
60
|
+
[sys.executable, "-c", driver],
|
|
61
|
+
check=False,
|
|
62
|
+
capture_output=True,
|
|
63
|
+
text=True,
|
|
64
|
+
env=subprocess_environment,
|
|
65
|
+
)
|
|
66
|
+
assert completed.returncode == 0, completed.stderr
|
|
67
|
+
assert completed.stdout.strip() == "ok"
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"""Agent-home tooling exemption tests for the classifier and the enforcer.
|
|
2
|
+
|
|
3
|
+
A coding agent installs its own helper scripts under a dot-directory in the
|
|
4
|
+
user's home — ``~/.grok/runs/`` sits beside the already-exempt
|
|
5
|
+
``~/.claude/hooks/``. That tree is vendored tooling rather than project code,
|
|
6
|
+
so the repository code rules do not govern it::
|
|
7
|
+
|
|
8
|
+
/home/example/.grok/runs/worktree-health/health.py -> exempt
|
|
9
|
+
C:/dev/my-project/src/grokking.py -> governed
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import tempfile
|
|
18
|
+
from pathlib import Path
|
|
19
|
+
|
|
20
|
+
from blocking import _path_setup # noqa: F401 (pins this checkout ahead of sibling worktrees)
|
|
21
|
+
from blocking.code_rules_shared import is_agent_home_tooling, is_ephemeral_path
|
|
22
|
+
|
|
23
|
+
_ENFORCER_SCRIPT = Path(__file__).resolve().parent / "code_rules_enforcer.py"
|
|
24
|
+
|
|
25
|
+
_VIOLATING_PRODUCTION_SOURCE = "def process_data(payload: str) -> None:\n print(payload)\n"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _check_as(candidate_source: str, target_path: str) -> subprocess.CompletedProcess[str]:
|
|
29
|
+
"""Run the enforcer's pre-check on a candidate judged as another path.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
candidate_source: Python source written to the candidate file.
|
|
33
|
+
target_path: Destination path the candidate is judged as.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
The completed process carrying stdout, stderr, and the exit code.
|
|
37
|
+
"""
|
|
38
|
+
with tempfile.TemporaryDirectory() as scratch:
|
|
39
|
+
candidate = Path(scratch) / "candidate.py"
|
|
40
|
+
candidate.write_text(candidate_source, encoding="utf-8")
|
|
41
|
+
return subprocess.run(
|
|
42
|
+
[sys.executable, str(_ENFORCER_SCRIPT), "--check", str(candidate), "--as", target_path],
|
|
43
|
+
input="",
|
|
44
|
+
capture_output=True,
|
|
45
|
+
text=True,
|
|
46
|
+
check=False,
|
|
47
|
+
)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def test_posix_agent_home_path_is_agent_tooling() -> None:
|
|
51
|
+
assert is_agent_home_tooling("/home/example/.grok/runs/worktree-health/health.py")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def test_windows_agent_home_path_is_agent_tooling() -> None:
|
|
55
|
+
assert is_agent_home_tooling(r"C:\Users\example\.grok\runs\worktree-health\health.py")
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def test_any_depth_under_the_agent_home_is_agent_tooling() -> None:
|
|
59
|
+
assert is_agent_home_tooling("/home/example/.grok/hooks/force_worktree.py")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_ordinary_project_code_is_not_agent_tooling() -> None:
|
|
63
|
+
assert not is_agent_home_tooling("/dev/my-project/src/service.py")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def test_a_filename_spelled_like_the_agent_is_not_agent_tooling() -> None:
|
|
67
|
+
assert not is_agent_home_tooling("/dev/my-project/src/grokking.py")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_a_directory_without_the_leading_dot_is_not_agent_tooling() -> None:
|
|
71
|
+
assert not is_agent_home_tooling("/dev/my-project/grok/runner.py")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def test_an_empty_path_is_not_agent_tooling() -> None:
|
|
75
|
+
assert not is_agent_home_tooling("")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_agent_home_path_is_exempt_from_the_repository_gates() -> None:
|
|
79
|
+
assert is_ephemeral_path("/home/example/.grok/runs/worktree-health/health.py")
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_project_code_is_not_exempt_from_the_repository_gates() -> None:
|
|
83
|
+
assert not is_ephemeral_path("/dev/my-project/src/service.py")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def test_enforcer_allows_violating_source_under_the_agent_home() -> None:
|
|
87
|
+
target = os.path.join("/home/example", ".grok", "runs", "worktree-health", "health.py")
|
|
88
|
+
|
|
89
|
+
completed = _check_as(_VIOLATING_PRODUCTION_SOURCE, target)
|
|
90
|
+
|
|
91
|
+
assert completed.returncode == 0, completed.stdout + completed.stderr
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_enforcer_still_flags_the_same_source_in_a_project_path() -> None:
|
|
95
|
+
target = os.path.join("/dev/my-project", "src", "service.py")
|
|
96
|
+
|
|
97
|
+
completed = _check_as(_VIOLATING_PRODUCTION_SOURCE, target)
|
|
98
|
+
|
|
99
|
+
assert completed.returncode != 0
|
|
@@ -1,13 +1,15 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
"""Span-scope tests for docstring checks.
|
|
2
|
+
|
|
3
|
+
Covers two surfaces:
|
|
4
|
+
|
|
5
|
+
1. ``check_docstring_args_single_line_scope_vs_span`` (Category O6) — an Args
|
|
6
|
+
entry that claims single-line (block-anchor) scope while the body builds a
|
|
7
|
+
multi-line span and routes it through span intersection.
|
|
8
|
+
|
|
9
|
+
2. Changed-span grading for ``check_docstring_runon_sentence`` and
|
|
10
|
+
``check_docstring_prose_wall_without_illustration`` (issue #237) — far-away
|
|
11
|
+
no-op grandfathering, introduced-violation blocking, and
|
|
12
|
+
``defer_scope_to_caller`` pass-through.
|
|
11
13
|
"""
|
|
12
14
|
|
|
13
15
|
from __future__ import annotations
|
|
@@ -201,3 +203,223 @@ def test_hook_infrastructure_is_not_exempt() -> None:
|
|
|
201
203
|
assert (
|
|
202
204
|
len(check_docstring_args_single_line_scope_vs_span(content, HOOK_INFRASTRUCTURE_PATH)) == 1
|
|
203
205
|
)
|
|
206
|
+
|
|
207
|
+
# ---------------------------------------------------------------------------
|
|
208
|
+
# Changed-span grading for the two whole-file docstring checks (issue #237 / P-41)
|
|
209
|
+
# check_docstring_runon_sentence and check_docstring_prose_wall_without_illustration
|
|
210
|
+
# ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def check_docstring_runon_sentence(
|
|
214
|
+
content: str,
|
|
215
|
+
file_path: str,
|
|
216
|
+
all_changed_lines: set[int] | None = None,
|
|
217
|
+
defer_scope_to_caller: bool = False,
|
|
218
|
+
) -> list[str]:
|
|
219
|
+
return code_rules_enforcer.check_docstring_runon_sentence(
|
|
220
|
+
content, file_path, all_changed_lines, defer_scope_to_caller
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def check_docstring_prose_wall_without_illustration(
|
|
225
|
+
content: str,
|
|
226
|
+
file_path: str,
|
|
227
|
+
all_changed_lines: set[int] | None = None,
|
|
228
|
+
defer_scope_to_caller: bool = False,
|
|
229
|
+
) -> list[str]:
|
|
230
|
+
return code_rules_enforcer.check_docstring_prose_wall_without_illustration(
|
|
231
|
+
content, file_path, all_changed_lines, defer_scope_to_caller
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
_CLEAN_HELPER_TAIL = (
|
|
236
|
+
"\n"
|
|
237
|
+
"def clean_helper() -> str:\n"
|
|
238
|
+
' """Return a short status token for the board.\n'
|
|
239
|
+
"\n"
|
|
240
|
+
" Each call names one vessel and its final port.\n"
|
|
241
|
+
' """\n'
|
|
242
|
+
' return "ok"\n'
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _module_with_far_away_runon_and_clean_helper() -> str:
|
|
247
|
+
return (
|
|
248
|
+
'"""Owns the SIGINT install/restore/installability check, the atexit terminal-record\n'
|
|
249
|
+
"registration, and the interrupted-run finalizer — the non-promoter-specific\n"
|
|
250
|
+
"machinery that brackets a run so the JSONL artifact always carries a terminal\n"
|
|
251
|
+
"record and an in-flight theme record on interrupt.\n"
|
|
252
|
+
'"""\n'
|
|
253
|
+
+ _CLEAN_HELPER_TAIL
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _module_with_far_away_prose_wall_and_clean_helper() -> str:
|
|
258
|
+
return (
|
|
259
|
+
'"""Assemble the nightly voyage tally from the harbor scans.\n'
|
|
260
|
+
"\n"
|
|
261
|
+
"A scan names one vessel and where it dropped anchor.\n"
|
|
262
|
+
"The tally walks the scans in arrival order and keeps that order.\n"
|
|
263
|
+
"A calm voyage ends well for every vessel it carried.\n"
|
|
264
|
+
"A halted voyage marks the vessel it was near when the storm arrived.\n"
|
|
265
|
+
"A wrecked voyage marks the vessel that sank and stops the walk there.\n"
|
|
266
|
+
"The tally groups the vessels by their final port for the harbor.\n"
|
|
267
|
+
"The harbor reads the tally and sees every arrival at a glance.\n"
|
|
268
|
+
'"""\n'
|
|
269
|
+
+ _CLEAN_HELPER_TAIL
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def test_runon_far_away_no_op_is_grandfathered() -> None:
|
|
274
|
+
content = _module_with_far_away_runon_and_clean_helper()
|
|
275
|
+
clean_helper_line = content.splitlines().index("def clean_helper() -> str:") + 1
|
|
276
|
+
all_changed_lines = set(range(clean_helper_line, clean_helper_line + 6))
|
|
277
|
+
unscoped = check_docstring_runon_sentence(content, PRODUCTION_FILE_PATH)
|
|
278
|
+
assert any("run-on" in each for each in unscoped), (
|
|
279
|
+
f"control: unscoped must still see the far-away run-on, got {unscoped!r}"
|
|
280
|
+
)
|
|
281
|
+
scoped = check_docstring_runon_sentence(
|
|
282
|
+
content, PRODUCTION_FILE_PATH, all_changed_lines=all_changed_lines
|
|
283
|
+
)
|
|
284
|
+
assert scoped == [], (
|
|
285
|
+
f"far-away run-on must be grandfathered on a clean-helper edit, got {scoped!r}"
|
|
286
|
+
)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def test_runon_introduced_violation_blocks() -> None:
|
|
290
|
+
content = _module_with_far_away_runon_and_clean_helper()
|
|
291
|
+
unscoped = check_docstring_runon_sentence(content, PRODUCTION_FILE_PATH)
|
|
292
|
+
assert any("run-on" in each for each in unscoped)
|
|
293
|
+
scoped = check_docstring_runon_sentence(
|
|
294
|
+
content, PRODUCTION_FILE_PATH, all_changed_lines={1}
|
|
295
|
+
)
|
|
296
|
+
assert any("run-on" in each for each in scoped), (
|
|
297
|
+
f"introduced/touched run-on must block, got {scoped!r}"
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def test_prose_wall_far_away_no_op_is_grandfathered() -> None:
|
|
302
|
+
content = _module_with_far_away_prose_wall_and_clean_helper()
|
|
303
|
+
clean_helper_line = content.splitlines().index("def clean_helper() -> str:") + 1
|
|
304
|
+
all_changed_lines = set(range(clean_helper_line, clean_helper_line + 6))
|
|
305
|
+
unscoped = check_docstring_prose_wall_without_illustration(
|
|
306
|
+
content, PRODUCTION_FILE_PATH
|
|
307
|
+
)
|
|
308
|
+
assert any("worked example" in each for each in unscoped), (
|
|
309
|
+
f"control: unscoped must still see the far-away wall, got {unscoped!r}"
|
|
310
|
+
)
|
|
311
|
+
scoped = check_docstring_prose_wall_without_illustration(
|
|
312
|
+
content, PRODUCTION_FILE_PATH, all_changed_lines=all_changed_lines
|
|
313
|
+
)
|
|
314
|
+
assert scoped == [], (
|
|
315
|
+
f"far-away wall must be grandfathered on a clean-helper edit, got {scoped!r}"
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def test_prose_wall_introduced_violation_blocks() -> None:
|
|
320
|
+
content = _module_with_far_away_prose_wall_and_clean_helper()
|
|
321
|
+
unscoped = check_docstring_prose_wall_without_illustration(
|
|
322
|
+
content, PRODUCTION_FILE_PATH
|
|
323
|
+
)
|
|
324
|
+
assert any("worked example" in each for each in unscoped)
|
|
325
|
+
scoped = check_docstring_prose_wall_without_illustration(
|
|
326
|
+
content, PRODUCTION_FILE_PATH, all_changed_lines={1}
|
|
327
|
+
)
|
|
328
|
+
assert any("worked example" in each for each in scoped), (
|
|
329
|
+
f"introduced/touched wall must block, got {scoped!r}"
|
|
330
|
+
)
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def test_runon_defer_scope_returns_all_violations() -> None:
|
|
334
|
+
content = _module_with_far_away_runon_and_clean_helper()
|
|
335
|
+
deferred = check_docstring_runon_sentence(
|
|
336
|
+
content,
|
|
337
|
+
PRODUCTION_FILE_PATH,
|
|
338
|
+
all_changed_lines={999},
|
|
339
|
+
defer_scope_to_caller=True,
|
|
340
|
+
)
|
|
341
|
+
assert any("run-on" in each for each in deferred)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _function_with_runon_docstring() -> str:
|
|
345
|
+
return (
|
|
346
|
+
"def clean_helper() -> str:\n"
|
|
347
|
+
' """Owns the SIGINT install/restore/installability check, the atexit terminal-record\n'
|
|
348
|
+
" registration, and the interrupted-run finalizer — the non-promoter-specific\n"
|
|
349
|
+
" machinery that brackets a run so the JSONL artifact always carries a terminal\n"
|
|
350
|
+
" record and an in-flight theme record on interrupt.\n"
|
|
351
|
+
' """\n'
|
|
352
|
+
' return "ok"\n'
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def test_runon_docstring_body_edit_blocks_without_def_line() -> None:
|
|
357
|
+
content = _function_with_runon_docstring()
|
|
358
|
+
unscoped = check_docstring_runon_sentence(content, PRODUCTION_FILE_PATH)
|
|
359
|
+
assert any("run-on" in each for each in unscoped), (
|
|
360
|
+
f"control: unscoped must see the function run-on, got {unscoped!r}"
|
|
361
|
+
)
|
|
362
|
+
docstring_body_lines = {2, 3, 4, 5}
|
|
363
|
+
scoped = check_docstring_runon_sentence(
|
|
364
|
+
content, PRODUCTION_FILE_PATH, all_changed_lines=docstring_body_lines
|
|
365
|
+
)
|
|
366
|
+
assert any("run-on" in each for each in scoped), (
|
|
367
|
+
f"docstring-body edit must block even when the def line is untouched, got {scoped!r}"
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def test_runon_post_docstring_body_edit_is_grandfathered() -> None:
|
|
372
|
+
content = _function_with_runon_docstring()
|
|
373
|
+
return_line = content.splitlines().index(' return "ok"') + 1
|
|
374
|
+
scoped = check_docstring_runon_sentence(
|
|
375
|
+
content, PRODUCTION_FILE_PATH, all_changed_lines={return_line}
|
|
376
|
+
)
|
|
377
|
+
assert scoped == [], (
|
|
378
|
+
f"post-docstring body edit must grandfather the pre-existing run-on, got {scoped!r}"
|
|
379
|
+
)
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _function_with_prose_wall_docstring() -> str:
|
|
383
|
+
return (
|
|
384
|
+
"def clean_helper() -> str:\n"
|
|
385
|
+
' """Assemble the nightly voyage tally from the harbor scans.\n'
|
|
386
|
+
"\n"
|
|
387
|
+
" A scan names one vessel and where it dropped anchor.\n"
|
|
388
|
+
" The tally walks the scans in arrival order and keeps that order.\n"
|
|
389
|
+
" A calm voyage ends well for every vessel it carried.\n"
|
|
390
|
+
" A halted voyage marks the vessel it was near when the storm arrived.\n"
|
|
391
|
+
" A wrecked voyage marks the vessel that sank and stops the walk there.\n"
|
|
392
|
+
" The tally groups the vessels by their final port for the harbor.\n"
|
|
393
|
+
" The harbor reads the tally and sees every arrival at a glance.\n"
|
|
394
|
+
' """\n'
|
|
395
|
+
' return "ok"\n'
|
|
396
|
+
)
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def test_prose_wall_docstring_body_edit_blocks_without_def_line() -> None:
|
|
400
|
+
content = _function_with_prose_wall_docstring()
|
|
401
|
+
unscoped = check_docstring_prose_wall_without_illustration(
|
|
402
|
+
content, PRODUCTION_FILE_PATH
|
|
403
|
+
)
|
|
404
|
+
assert any("worked example" in each for each in unscoped), (
|
|
405
|
+
f"control: unscoped must see the function wall, got {unscoped!r}"
|
|
406
|
+
)
|
|
407
|
+
docstring_body_lines = set(range(2, 12))
|
|
408
|
+
scoped = check_docstring_prose_wall_without_illustration(
|
|
409
|
+
content, PRODUCTION_FILE_PATH, all_changed_lines=docstring_body_lines
|
|
410
|
+
)
|
|
411
|
+
assert any("worked example" in each for each in scoped), (
|
|
412
|
+
f"docstring-body edit must block even when the def line is untouched, got {scoped!r}"
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
|
|
416
|
+
def test_prose_wall_defer_scope_returns_all_violations() -> None:
|
|
417
|
+
content = _module_with_far_away_prose_wall_and_clean_helper()
|
|
418
|
+
deferred = check_docstring_prose_wall_without_illustration(
|
|
419
|
+
content,
|
|
420
|
+
PRODUCTION_FILE_PATH,
|
|
421
|
+
all_changed_lines={999},
|
|
422
|
+
defer_scope_to_caller=True,
|
|
423
|
+
)
|
|
424
|
+
assert any("worked example" in each for each in deferred)
|
|
425
|
+
|
|
@@ -186,7 +186,7 @@ def test_should_return_false_for_pytest_tmp_path_when_job_dir_elsewhere(
|
|
|
186
186
|
temp directory was an ephemeral source: pytest tmp_path fixtures live
|
|
187
187
|
under tempfile.gettempdir(), so the enforcer's own test targets must not
|
|
188
188
|
classify as ephemeral. CLAUDE_JOB_DIR points elsewhere (its scratch dir
|
|
189
|
-
lives under .claude-
|
|
189
|
+
lives under .claude-profile-c/jobs, not %TEMP%).
|
|
190
190
|
"""
|
|
191
191
|
monkeypatch.delenv("CLAUDE_CODE_RULES_DISABLE_EPHEMERAL_EXEMPT", raising=False)
|
|
192
192
|
monkeypatch.setenv("CLAUDE_JOB_DIR", str(tmp_path / "elsewhere"))
|
|
@@ -65,3 +65,44 @@ def test_should_report_join_separator_line_number() -> None:
|
|
|
65
65
|
assert any("Line 2" in each_issue for each_issue in issues), (
|
|
66
66
|
f"Expected line 2 reported, got: {issues}"
|
|
67
67
|
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_should_grandfather_unchanged_join_separator_in_wide_edit() -> None:
|
|
71
|
+
prior_fragment = (
|
|
72
|
+
"def render(all_paths: list) -> str:\n"
|
|
73
|
+
" return ', '.join(str(each_path) for each_path in all_paths)\n"
|
|
74
|
+
)
|
|
75
|
+
proposed_fragment = (
|
|
76
|
+
"def render(all_paths: list) -> str:\n"
|
|
77
|
+
" # keep the join separator the same\n"
|
|
78
|
+
" return ', '.join(str(each_path) for each_path in all_paths)\n"
|
|
79
|
+
)
|
|
80
|
+
issues = code_rules_enforcer.validate_content(
|
|
81
|
+
proposed_fragment,
|
|
82
|
+
PRODUCTION_FILE_PATH,
|
|
83
|
+
prior_fragment,
|
|
84
|
+
)
|
|
85
|
+
join_issues = [each for each in issues if "join" in each]
|
|
86
|
+
assert join_issues == [], (
|
|
87
|
+
f"unchanged enclosed join separator must be grandfathered, got: {join_issues}"
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_should_block_new_join_separator_in_wide_edit() -> None:
|
|
92
|
+
prior_fragment = (
|
|
93
|
+
"def render(all_paths: list) -> str:\n"
|
|
94
|
+
" return JOIN_DELIMITER.join(all_paths)\n"
|
|
95
|
+
)
|
|
96
|
+
proposed_fragment = (
|
|
97
|
+
"def render(all_paths: list) -> str:\n"
|
|
98
|
+
" return ', '.join(all_paths)\n"
|
|
99
|
+
)
|
|
100
|
+
issues = code_rules_enforcer.validate_content(
|
|
101
|
+
proposed_fragment,
|
|
102
|
+
PRODUCTION_FILE_PATH,
|
|
103
|
+
prior_fragment,
|
|
104
|
+
)
|
|
105
|
+
join_issues = [each for each in issues if "join" in each]
|
|
106
|
+
assert any("Line 2" in each_issue for each_issue in join_issues), (
|
|
107
|
+
f"introduced join separator must block at its new location, got: {join_issues}"
|
|
108
|
+
)
|
|
@@ -232,3 +232,101 @@ def test_should_still_flag_literal_in_nested_function_body() -> None:
|
|
|
232
232
|
assert len(issues) == 1, (
|
|
233
233
|
f"Inner literal must be flagged exactly once (no duplicate from outer walk), got: {issues}"
|
|
234
234
|
)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def test_should_grandfather_unchanged_string_magic_in_wide_edit() -> None:
|
|
238
|
+
prior_fragment = (
|
|
239
|
+
"import os\n"
|
|
240
|
+
"\n"
|
|
241
|
+
"def fetch_secret() -> str:\n"
|
|
242
|
+
" return os.environ['STRIPE_SECRET']\n"
|
|
243
|
+
)
|
|
244
|
+
proposed_fragment = (
|
|
245
|
+
"import os\n"
|
|
246
|
+
"\n"
|
|
247
|
+
"def fetch_secret() -> str:\n"
|
|
248
|
+
" # wide edit that leaves the magic string in place\n"
|
|
249
|
+
" return os.environ['STRIPE_SECRET']\n"
|
|
250
|
+
)
|
|
251
|
+
issues = code_rules_enforcer.validate_content(
|
|
252
|
+
proposed_fragment,
|
|
253
|
+
PRODUCTION_FILE_PATH,
|
|
254
|
+
prior_fragment,
|
|
255
|
+
)
|
|
256
|
+
magic_issues = [each for each in issues if "STRIPE_SECRET" in each]
|
|
257
|
+
assert magic_issues == [], (
|
|
258
|
+
f"unchanged enclosed string magic must be grandfathered, got: {magic_issues}"
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def test_should_block_new_string_magic_in_wide_edit() -> None:
|
|
263
|
+
prior_fragment = (
|
|
264
|
+
"import os\n"
|
|
265
|
+
"\n"
|
|
266
|
+
"def fetch_secret() -> str:\n"
|
|
267
|
+
" return os.environ.get('missing', '')\n"
|
|
268
|
+
)
|
|
269
|
+
proposed_fragment = (
|
|
270
|
+
"import os\n"
|
|
271
|
+
"\n"
|
|
272
|
+
"def fetch_secret() -> str:\n"
|
|
273
|
+
" return os.environ['STRIPE_SECRET']\n"
|
|
274
|
+
)
|
|
275
|
+
issues = code_rules_enforcer.validate_content(
|
|
276
|
+
proposed_fragment,
|
|
277
|
+
PRODUCTION_FILE_PATH,
|
|
278
|
+
prior_fragment,
|
|
279
|
+
)
|
|
280
|
+
magic_issues = [each for each in issues if "STRIPE_SECRET" in each]
|
|
281
|
+
assert any("Line " in each_issue for each_issue in magic_issues), (
|
|
282
|
+
f"introduced string magic must block with a line locator, got: {magic_issues}"
|
|
283
|
+
)
|
|
284
|
+
assert any("STRIPE_SECRET" in each_issue for each_issue in magic_issues)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def test_should_block_second_identical_string_magic_per_occurrence() -> None:
|
|
288
|
+
prior_fragment = (
|
|
289
|
+
"import os\n"
|
|
290
|
+
"\n"
|
|
291
|
+
"def fetch_secret() -> str:\n"
|
|
292
|
+
" return os.environ['STRIPE_SECRET']\n"
|
|
293
|
+
)
|
|
294
|
+
proposed_fragment = (
|
|
295
|
+
"import os\n"
|
|
296
|
+
"\n"
|
|
297
|
+
"def fetch_secret() -> str:\n"
|
|
298
|
+
" return os.environ['STRIPE_SECRET']\n"
|
|
299
|
+
"\n"
|
|
300
|
+
"def fetch_backup() -> str:\n"
|
|
301
|
+
" return os.environ['STRIPE_SECRET']\n"
|
|
302
|
+
)
|
|
303
|
+
issues = code_rules_enforcer.validate_content(
|
|
304
|
+
proposed_fragment,
|
|
305
|
+
PRODUCTION_FILE_PATH,
|
|
306
|
+
prior_fragment,
|
|
307
|
+
)
|
|
308
|
+
magic_issues = [each for each in issues if "STRIPE_SECRET" in each]
|
|
309
|
+
assert len(magic_issues) == 1, (
|
|
310
|
+
f"exactly one new identical magic must block (per-occurrence), got: {magic_issues}"
|
|
311
|
+
)
|
|
312
|
+
assert any("Line 7" in each_issue for each_issue in magic_issues), (
|
|
313
|
+
f"second occurrence must keep proposed-fragment line, got: {magic_issues}"
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def test_should_surface_magic_when_gate_defers_scope_with_head_as_old() -> None:
|
|
318
|
+
source = (
|
|
319
|
+
"def compute() -> int:\n"
|
|
320
|
+
" old_number = 9999\n"
|
|
321
|
+
" return old_number\n"
|
|
322
|
+
)
|
|
323
|
+
issues = code_rules_enforcer.validate_content(
|
|
324
|
+
source,
|
|
325
|
+
PRODUCTION_FILE_PATH,
|
|
326
|
+
source,
|
|
327
|
+
defer_scope_to_caller=True,
|
|
328
|
+
)
|
|
329
|
+
assert any("9999" in each_issue for each_issue in issues), (
|
|
330
|
+
f"gate path must still surface whole-file magic when old_content equals content, "
|
|
331
|
+
f"got: {issues}"
|
|
332
|
+
)
|