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
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Prose-style enforcement
|
|
2
|
+
|
|
3
|
+
Opinionated prose gates (heavy-word swaps, hedging, historical state phrasing, intent-only endings, hook prose-detector consistency) are **off by default**.
|
|
4
|
+
|
|
5
|
+
## Opt-in
|
|
6
|
+
|
|
7
|
+
Set `CLAUDE_PROSE_STYLE_ENFORCEMENT` to `1`, `true`, `yes`, or `on` (case and spaces ignored). Any other value, and an unset variable, leave those gates off.
|
|
8
|
+
|
|
9
|
+
The flag is `PROSE_STYLE_ENFORCEMENT_ENABLED` in `hooks/blocking/config/prose_style_enforcement_constants.py`.
|
|
10
|
+
|
|
11
|
+
## Always on
|
|
12
|
+
|
|
13
|
+
Structural AskUserQuestion lean-block validation in `plain_language_blocker` (chat detail, length caps on question blocks) stays active regardless of the flag. Code-rules and security blockers are out of scope for this switch.
|
|
14
|
+
|
|
15
|
+
## Advisory precision measurement (OP-07B)
|
|
16
|
+
|
|
17
|
+
When the flag is off, heavy-word and hedging hits still emit privacy-safe advisory candidates to `~/.claude/logs/prose-matcher-advisory.jsonl` (matcher id, surface, hashed context fingerprint, optional label). Classification (`keep` / `narrow` / `drop` / `advisory`) uses a labeled sample floor of 30 and precision floors 0.7 / 0.4. Matchers below the floor stay advisory. No matcher becomes hard-blocking from historical labels alone. See `hooks/observability/prose_matcher_advisory.py`.
|
|
18
|
+
|
|
19
|
+
## Explicit uncertainty (OP-07C)
|
|
20
|
+
|
|
21
|
+
With enforcement on, a hedge word in a sentence that also labels the claim (`unverified`, `I don't know`, `no source for this claim`, …) passes. A bare hedge in a different sentence still blocks. Detail: `rules/hedging-claims.md`.
|
|
22
|
+
|
|
23
|
+
## Plain-language advisory (OP-07D)
|
|
24
|
+
|
|
25
|
+
Heavy-word matches never hard-deny a Write/Edit or AskUserQuestion. With enforcement on, the hook allows the call and returns a `systemMessage` that names everyday swaps for both AskUserQuestion and `.md` writes. Lean-block structure denials stay hard. Detail: `rules/plain-language.md`.
|
|
@@ -1,14 +1,20 @@
|
|
|
1
|
-
# Team-Advisor Skill
|
|
1
|
+
# Team-Advisor Skill
|
|
2
2
|
|
|
3
|
-
`/team-advisor`
|
|
3
|
+
`/team-advisor` binds one standing warm advisor for this session at the strongest reachable tier. Use it when `advisor()` is absent, or when you want a standing four-signal reviewer across many decision points.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- Codex session -> consult Sol xhigh
|
|
5
|
+
## Refs
|
|
7
6
|
|
|
8
|
-
|
|
7
|
+
| Doc | Holds |
|
|
8
|
+
|---|---|
|
|
9
|
+
| `skills/team-advisor/SKILL.md` | Sole-consumer bind wiring and constraints |
|
|
10
|
+
| `advisor-tool.md` | Consult timing, hard rule, how to treat advice |
|
|
11
|
+
| `~/.claude/_shared/advisor/advisor-protocol.md` | Host bind, floor, lifecycle |
|
|
12
|
+
| `agents/session-advisor.md` | ENDORSE / CORRECTION / PLAN / STOP |
|
|
9
13
|
|
|
10
|
-
|
|
14
|
+
## When to use
|
|
11
15
|
|
|
12
|
-
|
|
16
|
+
Follow the call rules in `advisor-tool.md` (orientation first, then consult before substantive work; durable deliverable before the completion consult; stuck or reapproach; long tasks twice).
|
|
13
17
|
|
|
14
|
-
|
|
18
|
+
## Relation to `advisor()`
|
|
19
|
+
|
|
20
|
+
`/team-advisor` works with no `advisor()` tool. When both exist: `advisor()` for a fast history-forwarded check; `/team-advisor` for a standing named reviewer consulted at the same cadence.
|
package/hooks/blocking/CLAUDE.md
CHANGED
|
@@ -82,16 +82,16 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
|
|
|
82
82
|
| `destructive_command_blocker.py` | PreToolUse (Bash/PowerShell) | Shell commands with destructive literals (`rm -rf`, `git reset --hard`, etc.) |
|
|
83
83
|
| `docstring_rule_gate_count_blocker.py` | PreToolUse (Write/Edit/MultiEdit) | A stale spelled-out gate-validator count in `docstring-prose-matches-implementation.md` — the "N more gate validators" / "M gated slices" count drifting from the `check_docstring_*` validators the prose names |
|
|
84
84
|
| `duplicate_rmtree_helper_blocker.py` | PreToolUse (Write/Edit) | A local re-definition of the Windows-safe rmtree helper trio (`_strip_read_only_and_retry`, `_force_remove_tree` / `force_rmtree`) in place of importing a shared helper |
|
|
85
|
-
| `eli11_reply_enforcer.py` | Stop | Final replies breaking the `eli11-replies` shape — more than
|
|
85
|
+
| `eli11_reply_enforcer.py` | Stop | Final replies breaking the `eli11-replies` shape — more than 6 bullet lines, more than 2 list lines over 20 words each, or multi-line instructions with no numbered step among the lead lines. Code fences, inline code, blockquotes, table rows, and link targets come off before the reply is judged. |
|
|
86
86
|
| `env_var_table_code_drift_blocker.py` | PreToolUse (Write/Edit/MultiEdit) | A markdown env-var summary table row attributing an environment variable to a code file whose source never references that variable name |
|
|
87
87
|
| `es_exe_path_rewriter.py` | PreToolUse | Rewrites paths referencing `.exe` under the Everything search path |
|
|
88
88
|
| `fable_spawn_gate.py` | PreToolUse (Agent/Task) | An `Agent` or `Task` spawn whose prompt carries no `FABLE-SPAWN-AUTHORIZED` token and whose model field reads `fable` in any letter case — the bare alias, or a delimiter segment of a full model id, so `claude-fable-5` is denied too |
|
|
89
89
|
| `gh_body_arg_blocker.py` | PreToolUse (Bash) | `gh` commands passing `--body`/`-b` directly (requires `--body-file` instead) |
|
|
90
90
|
| `gh_pr_author_enforcer.py` | PreToolUse | Enforces PR author identity rules |
|
|
91
91
|
| `gh_pr_author_restore.py` | PostToolUse | Restores PR author after a tool call |
|
|
92
|
-
| `hedging_language_blocker.py` | Stop | Responses with hedging words (`likely`, `probably`, `appears to`) |
|
|
93
|
-
| `hook_prose_detector_consistency.py` | PreToolUse (Write/Edit) | Hook docstrings/messages that claim a trigger the detector cannot fire on |
|
|
94
|
-
| `intent_only_ending_blocker.py` | Stop | Responses that end on a plan or intent without doing the work |
|
|
92
|
+
| `hedging_language_blocker.py` | Stop | Responses with hedging words (`likely`, `probably`, `appears to`); armed only when `CLAUDE_PROSE_STYLE_ENFORCEMENT` is on (default off) |
|
|
93
|
+
| `hook_prose_detector_consistency.py` | PreToolUse (Write/Edit) | Hook docstrings/messages that claim a trigger the detector cannot fire on; armed only when `CLAUDE_PROSE_STYLE_ENFORCEMENT` is on (default off) |
|
|
94
|
+
| `intent_only_ending_blocker.py` | Stop | Responses that end on a plan or intent without doing the work; armed only when `CLAUDE_PROSE_STYLE_ENFORCEMENT` is on (default off) |
|
|
95
95
|
| `open_questions_in_plans_blocker.py` | PreToolUse (Write/Edit) | Plan documents with unresolved open questions |
|
|
96
96
|
| `nas_ssh_binary_enforcer.py` | PreToolUse (Bash) | A bare `ssh`/`scp`/`sftp` command word targeting the NAS (Git Bash's MSYS ssh stalls on an interactive password prompt), or the full `System32/OpenSSH` binary to that host without `-o BatchMode=yes` |
|
|
97
97
|
| `package_inventory_stale_blocker.py` | PreToolUse (Write) | A new production code file created in a directory whose `README.md`/`CLAUDE.md` inventory (or a parent skill's `SKILL.md` Layout table mapping the `scripts/` subdirectory) names two or more sibling files but no entry for the new file |
|
|
@@ -100,7 +100,7 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
|
|
|
100
100
|
| `pii_prevention_blocker.py` | PreToolUse (Write/Edit/MultiEdit/Bash/PowerShell/MCP GitHub) | Entry hook — content that carries high-confidence personal data or secrets (real emails, home-dir paths, private IPs, credential material) on write, durable GitHub posts, or staged commit paths; resolves the staged-commit repository from the command it gates (via `pii_prevention_blocker_parts`), not the session working directory |
|
|
101
101
|
| `pii_scanner.py` | library | Pure text scanners shared by `pii_prevention_blocker.py` |
|
|
102
102
|
| `piped_pytest_blocker.py` | PreToolUse (Bash) | A pytest run whose output feeds a pipe, where the pipeline reports the exit code of the command on the right |
|
|
103
|
-
| `plain_language_blocker.py` | PreToolUse (Write/Edit/AskUserQuestion) | Heavy or jargon words in user-facing prose |
|
|
103
|
+
| `plain_language_blocker.py` | PreToolUse (Write/Edit/AskUserQuestion) | Heavy or jargon words in user-facing prose when `CLAUDE_PROSE_STYLE_ENFORCEMENT` is on (default off); AskUserQuestion lean-block structure stays always on |
|
|
104
104
|
| `pr_converge_bugteam_enforcer.py` | PreToolUse | Enforces that bugteam runs in parallel with bugbot in pr-converge loops |
|
|
105
105
|
| `pr_description_enforcer.py` | PreToolUse (Bash) | `gh pr create`/`edit`/`comment` bodies that fail the Anthropic claude-code style audit, proof-shaped `gh pr comment` bodies missing proof-of-work parts, and `gh pr ready` while the PR carries no passing proof comment |
|
|
106
106
|
| `precommit_code_rules_gate.py` | PreToolUse (Bash) | Staged changes that fail the CODE_RULES gate at commit time |
|
|
@@ -113,7 +113,7 @@ The check modules it calls are the `code_rules_<concern>.py` files below.
|
|
|
113
113
|
| `session_handoff_blocker.py` | Stop | Responses suggesting a new session mid-task |
|
|
114
114
|
| `shell_substitution_blocker.py` | PreToolUse (Bash) | A command carrying `$(...)`, a live backtick, or `<(...)`/`>(...)` process substitution, which the allowlist matcher cannot descend into |
|
|
115
115
|
| `stale_comment_reference_blocker.py` | PreToolUse (Edit) | An Edit that rewrites a Python code line while keeping the standalone comment directly above it, when that comment names an identifier the rewrite removes from the line |
|
|
116
|
-
| `state_description_blocker.py` | PreToolUse (Write/Edit) | Historical/comparative language in documentation |
|
|
116
|
+
| `state_description_blocker.py` | PreToolUse (Write/Edit) | Historical/comparative language in documentation; armed only when `CLAUDE_PROSE_STYLE_ENFORCEMENT` is on (default off) |
|
|
117
117
|
| `subprocess_budget_completeness.py` | PreToolUse | Subprocess calls missing required budget arguments |
|
|
118
118
|
| `tdd_enforcer.py` | PreToolUse (Write/Edit) | Production code written without a matching failing test |
|
|
119
119
|
| `unscoped_search_blocker.py` | PreToolUse (Bash/PowerShell) | A `find` or recursive listing that walks from the filesystem root, a drive root, bare home, or a network share root |
|
|
@@ -1,13 +1,17 @@
|
|
|
1
|
-
"""Add the hooks
|
|
1
|
+
"""Add the hooks and blocking directories to sys.path for sibling imports.
|
|
2
2
|
|
|
3
3
|
Importing this module inserts the hooks directory (this file's parent's parent)
|
|
4
|
-
at the front of sys.path so a
|
|
5
|
-
|
|
4
|
+
and the blocking directory (its own parent) at the front of sys.path, so a
|
|
5
|
+
module that runs as a standalone script from blocking/ can import both
|
|
6
|
+
``hooks_constants`` and its blocking-directory siblings while every import stays
|
|
7
|
+
at module top.
|
|
6
8
|
"""
|
|
7
9
|
|
|
8
10
|
import sys
|
|
9
11
|
from pathlib import Path
|
|
10
12
|
|
|
13
|
+
_blocking_directory = str(Path(__file__).resolve().parent)
|
|
11
14
|
_hooks_directory = str(Path(__file__).resolve().parent.parent)
|
|
12
|
-
|
|
13
|
-
sys.path
|
|
15
|
+
for each_directory in (_blocking_directory, _hooks_directory):
|
|
16
|
+
if each_directory not in sys.path:
|
|
17
|
+
sys.path.insert(0, each_directory)
|
|
@@ -13,6 +13,7 @@ if _hooks_directory not in sys.path:
|
|
|
13
13
|
sys.path.insert(0, _hooks_directory)
|
|
14
14
|
|
|
15
15
|
from code_rules_shared import ( # noqa: E402
|
|
16
|
+
_scope_violations_to_changed_lines,
|
|
16
17
|
_statement_is_docstring,
|
|
17
18
|
_walk_skipping_nested_functions,
|
|
18
19
|
_walk_skipping_type_checking_blocks,
|
|
@@ -2664,18 +2665,53 @@ def _runon_sentences(narrative_text: str) -> list[str]:
|
|
|
2664
2665
|
return flagged_sentences
|
|
2665
2666
|
|
|
2666
2667
|
|
|
2668
|
+
def _docstring_owner_span(owner_node: ast.AST, anchor_lineno: int) -> range:
|
|
2669
|
+
"""Lines from the owner's anchor through the end of its docstring statement.
|
|
2670
|
+
|
|
2671
|
+
::
|
|
2672
|
+
|
|
2673
|
+
def clean_helper() -> str: <- anchor (def / class line)
|
|
2674
|
+
'''run-on narrative across lines''' <- docstring end
|
|
2675
|
+
return "ok" <- outside the span
|
|
2676
|
+
|
|
2677
|
+
An edit that only rewrites the docstring body intersects this span and
|
|
2678
|
+
re-grades the finding. An edit to the return line does not.
|
|
2679
|
+
"""
|
|
2680
|
+
body = getattr(owner_node, "body", None) or []
|
|
2681
|
+
if not body:
|
|
2682
|
+
return range(anchor_lineno, anchor_lineno + 1)
|
|
2683
|
+
first_statement = body[0]
|
|
2684
|
+
end_lineno = getattr(first_statement, "end_lineno", None) or first_statement.lineno
|
|
2685
|
+
return range(anchor_lineno, end_lineno + 1)
|
|
2686
|
+
|
|
2687
|
+
|
|
2667
2688
|
def _documentable_docstring_targets(
|
|
2668
2689
|
parsed_tree: ast.Module,
|
|
2669
|
-
) -> list[tuple[int, str, str]]:
|
|
2670
|
-
documentable_targets: list[tuple[int, str, str]] = []
|
|
2690
|
+
) -> list[tuple[int, str, str, range]]:
|
|
2691
|
+
documentable_targets: list[tuple[int, str, str, range]] = []
|
|
2671
2692
|
module_docstring = ast.get_docstring(parsed_tree)
|
|
2672
2693
|
if module_docstring and parsed_tree.body:
|
|
2673
|
-
|
|
2694
|
+
module_anchor = parsed_tree.body[0].lineno
|
|
2695
|
+
documentable_targets.append(
|
|
2696
|
+
(
|
|
2697
|
+
module_anchor,
|
|
2698
|
+
"module",
|
|
2699
|
+
module_docstring,
|
|
2700
|
+
_docstring_owner_span(parsed_tree, module_anchor),
|
|
2701
|
+
)
|
|
2702
|
+
)
|
|
2674
2703
|
for each_node in _walk_skipping_type_checking_blocks(parsed_tree):
|
|
2675
2704
|
if isinstance(each_node, ast.ClassDef):
|
|
2676
2705
|
class_docstring = ast.get_docstring(each_node)
|
|
2677
2706
|
if class_docstring:
|
|
2678
|
-
documentable_targets.append(
|
|
2707
|
+
documentable_targets.append(
|
|
2708
|
+
(
|
|
2709
|
+
each_node.lineno,
|
|
2710
|
+
each_node.name,
|
|
2711
|
+
class_docstring,
|
|
2712
|
+
_docstring_owner_span(each_node, each_node.lineno),
|
|
2713
|
+
)
|
|
2714
|
+
)
|
|
2679
2715
|
continue
|
|
2680
2716
|
if not isinstance(each_node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
2681
2717
|
continue
|
|
@@ -2686,34 +2722,58 @@ def _documentable_docstring_targets(
|
|
|
2686
2722
|
function_docstring = _function_docstring_text(each_node)
|
|
2687
2723
|
if function_docstring:
|
|
2688
2724
|
documentable_targets.append(
|
|
2689
|
-
(
|
|
2725
|
+
(
|
|
2726
|
+
each_node.lineno,
|
|
2727
|
+
f"{each_node.name}()",
|
|
2728
|
+
function_docstring,
|
|
2729
|
+
_docstring_owner_span(each_node, each_node.lineno),
|
|
2730
|
+
)
|
|
2690
2731
|
)
|
|
2691
2732
|
return documentable_targets
|
|
2692
2733
|
|
|
2693
2734
|
|
|
2694
|
-
def check_docstring_runon_sentence(
|
|
2735
|
+
def check_docstring_runon_sentence(
|
|
2736
|
+
content: str,
|
|
2737
|
+
file_path: str,
|
|
2738
|
+
all_changed_lines: set[int] | None = None,
|
|
2739
|
+
defer_scope_to_caller: bool = False,
|
|
2740
|
+
) -> list[str]:
|
|
2695
2741
|
"""Flag a docstring narrative sentence that reads as a dense run-on wall.
|
|
2696
2742
|
|
|
2697
2743
|
A readable docstring breaks its narrative into short sentences a general
|
|
2698
2744
|
developer follows on the first read. The one mechanical mark of a wall is a
|
|
2699
2745
|
single sentence that runs past the word limit while chaining clauses with an
|
|
2700
2746
|
em-dash, a double-hyphen, or a semicolon. This check inspects the narrative prose of module,
|
|
2701
|
-
class, and public-function docstrings
|
|
2747
|
+
class, and public-function docstrings - the text before the first structured
|
|
2702
2748
|
section header (``Args:``, ``Arguments:``, ``Returns:``, ``Yields:``,
|
|
2703
|
-
``Raises:``, ``Note:``, ``Notes:``, ``Example:``, or ``Examples:``)
|
|
2749
|
+
``Raises:``, ``Note:``, ``Notes:``, ``Example:``, or ``Examples:``) - and
|
|
2704
2750
|
reports a sentence that is both over the word limit and joined by one of those
|
|
2705
2751
|
marks.
|
|
2706
2752
|
Whether the prose paints a concrete, illustrative picture is judgment the
|
|
2707
2753
|
plain-illustrative-docstrings audit lane carries; this gate catches only the
|
|
2708
2754
|
run-on mark.
|
|
2709
2755
|
|
|
2756
|
+
The caller passes the reconstructed full file as *content* so ``ast.parse``
|
|
2757
|
+
sees a complete module. Findings are then scoped to *all_changed_lines* so an
|
|
2758
|
+
Edit blocks on a run-on it just introduced while a pre-existing far-away
|
|
2759
|
+
run-on on an untouched definition does not block the edit.
|
|
2760
|
+
|
|
2710
2761
|
Args:
|
|
2711
|
-
content: The source text to inspect
|
|
2762
|
+
content: The source text to inspect - the reconstructed full file on an
|
|
2763
|
+
Edit so the parse succeeds.
|
|
2712
2764
|
file_path: The path the source will be written to, used for exemptions.
|
|
2765
|
+
all_changed_lines: Post-edit line numbers the current edit touched, or
|
|
2766
|
+
None to treat the whole file as in scope. When provided, a finding
|
|
2767
|
+
blocks only when its definition-through-docstring span intersects
|
|
2768
|
+
the changed lines.
|
|
2769
|
+
defer_scope_to_caller: When True, return every violation so the
|
|
2770
|
+
commit/push gate's ``split_violations_by_scope`` can scope by added
|
|
2771
|
+
line.
|
|
2713
2772
|
|
|
2714
2773
|
Returns:
|
|
2715
2774
|
One issue per docstring whose narrative carries a run-on sentence, capped
|
|
2716
|
-
at the module limit
|
|
2775
|
+
at the module limit and scoped to the changed lines unless
|
|
2776
|
+
*defer_scope_to_caller* is True or *all_changed_lines* is None.
|
|
2717
2777
|
"""
|
|
2718
2778
|
if is_test_file(file_path) or is_hook_infrastructure(file_path):
|
|
2719
2779
|
return []
|
|
@@ -2721,25 +2781,37 @@ def check_docstring_runon_sentence(content: str, file_path: str) -> list[str]:
|
|
|
2721
2781
|
parsed_tree = ast.parse(content)
|
|
2722
2782
|
except SyntaxError:
|
|
2723
2783
|
return []
|
|
2724
|
-
|
|
2725
|
-
for
|
|
2726
|
-
|
|
2727
|
-
|
|
2784
|
+
all_violations_in_walk_order: list[tuple[range, str]] = []
|
|
2785
|
+
for (
|
|
2786
|
+
each_line_number,
|
|
2787
|
+
each_label,
|
|
2788
|
+
each_docstring,
|
|
2789
|
+
each_span,
|
|
2790
|
+
) in _documentable_docstring_targets(parsed_tree):
|
|
2728
2791
|
flagged_sentences = _runon_sentences(_docstring_narrative_text(each_docstring))
|
|
2729
2792
|
if not flagged_sentences:
|
|
2730
2793
|
continue
|
|
2731
2794
|
run_on_word_count = _sentence_word_count(flagged_sentences[0])
|
|
2732
|
-
|
|
2795
|
+
message = (
|
|
2733
2796
|
f"Line {each_line_number}: {each_label} docstring carries a {run_on_word_count}-word "
|
|
2734
|
-
"run-on sentence
|
|
2797
|
+
"run-on sentence - break the narrative into short, illustrative sentences a general "
|
|
2735
2798
|
"developer reads in one pass (plain-illustrative-docstrings)"
|
|
2736
2799
|
)
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2800
|
+
all_violations_in_walk_order.append((each_span, message))
|
|
2801
|
+
scoped_issues = _scope_violations_to_changed_lines(
|
|
2802
|
+
all_violations_in_walk_order,
|
|
2803
|
+
all_changed_lines,
|
|
2804
|
+
defer_scope_to_caller,
|
|
2805
|
+
)
|
|
2806
|
+
return scoped_issues[:MAX_DOCSTRING_RUNON_SENTENCE_ISSUES]
|
|
2740
2807
|
|
|
2741
2808
|
|
|
2742
|
-
def check_docstring_prose_wall_without_illustration(
|
|
2809
|
+
def check_docstring_prose_wall_without_illustration(
|
|
2810
|
+
content: str,
|
|
2811
|
+
file_path: str,
|
|
2812
|
+
all_changed_lines: set[int] | None = None,
|
|
2813
|
+
defer_scope_to_caller: bool = False,
|
|
2814
|
+
) -> list[str]:
|
|
2743
2815
|
"""Flag a summary that tells for many sentences and shows nothing.
|
|
2744
2816
|
|
|
2745
2817
|
A reader trusts the opening to paint a scene. A run of short sentences with
|
|
@@ -2754,13 +2826,28 @@ def check_docstring_prose_wall_without_illustration(content: str, file_path: str
|
|
|
2754
2826
|
Past the prose-line limit with no ``::`` listing and no ``>>>`` doctest, this
|
|
2755
2827
|
fires. A narrative that shows a worked example, or one at the limit, passes.
|
|
2756
2828
|
|
|
2829
|
+
The caller passes the reconstructed full file as *content* so ``ast.parse``
|
|
2830
|
+
sees a complete module. Findings are then scoped to *all_changed_lines* so an
|
|
2831
|
+
Edit blocks on a wall it just introduced while a pre-existing far-away wall
|
|
2832
|
+
on an untouched definition does not block the edit.
|
|
2833
|
+
|
|
2757
2834
|
Args:
|
|
2758
|
-
content: The source text to inspect
|
|
2835
|
+
content: The source text to inspect - the reconstructed full file on an
|
|
2836
|
+
Edit so the parse succeeds.
|
|
2759
2837
|
file_path: The path the source will be written to, used for exemptions.
|
|
2838
|
+
all_changed_lines: Post-edit line numbers the current edit touched, or
|
|
2839
|
+
None to treat the whole file as in scope. When provided, a finding
|
|
2840
|
+
blocks only when its definition-through-docstring span intersects
|
|
2841
|
+
the changed lines.
|
|
2842
|
+
defer_scope_to_caller: When True, return every violation so the
|
|
2843
|
+
commit/push gate's ``split_violations_by_scope`` can scope by added
|
|
2844
|
+
line.
|
|
2760
2845
|
|
|
2761
2846
|
Returns:
|
|
2762
2847
|
One issue per summary that runs a wall of sentences with no worked
|
|
2763
|
-
example, capped at the issue limit for the rule
|
|
2848
|
+
example, capped at the issue limit for the rule and scoped to the
|
|
2849
|
+
changed lines unless *defer_scope_to_caller* is True or
|
|
2850
|
+
*all_changed_lines* is None.
|
|
2764
2851
|
"""
|
|
2765
2852
|
if is_test_file(file_path) or is_hook_infrastructure(file_path):
|
|
2766
2853
|
return []
|
|
@@ -2768,25 +2855,32 @@ def check_docstring_prose_wall_without_illustration(content: str, file_path: str
|
|
|
2768
2855
|
parsed_tree = ast.parse(content)
|
|
2769
2856
|
except SyntaxError:
|
|
2770
2857
|
return []
|
|
2771
|
-
|
|
2772
|
-
for
|
|
2773
|
-
|
|
2774
|
-
|
|
2858
|
+
all_violations_in_walk_order: list[tuple[range, str]] = []
|
|
2859
|
+
for (
|
|
2860
|
+
each_line_number,
|
|
2861
|
+
each_label,
|
|
2862
|
+
each_docstring,
|
|
2863
|
+
each_span,
|
|
2864
|
+
) in _documentable_docstring_targets(parsed_tree):
|
|
2775
2865
|
prose_lines, has_illustration = _docstring_narrative_partition(each_docstring)
|
|
2776
2866
|
if has_illustration:
|
|
2777
2867
|
continue
|
|
2778
2868
|
prose_line_count = len(prose_lines)
|
|
2779
2869
|
if prose_line_count <= DOCSTRING_NARRATIVE_PROSE_LINE_LIMIT:
|
|
2780
2870
|
continue
|
|
2781
|
-
|
|
2871
|
+
message = (
|
|
2782
2872
|
f"Line {each_line_number}: {each_label} summary runs {prose_line_count} "
|
|
2783
2873
|
"narrative lines with no worked example - show, don't tell: swap the wall for a "
|
|
2784
2874
|
"'::' listing (a sample input, an annotated outcome, ok/flag contrast rows) and "
|
|
2785
2875
|
"keep the narrative to a few short lines (plain-illustrative-docstrings)"
|
|
2786
2876
|
)
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2877
|
+
all_violations_in_walk_order.append((each_span, message))
|
|
2878
|
+
scoped_issues = _scope_violations_to_changed_lines(
|
|
2879
|
+
all_violations_in_walk_order,
|
|
2880
|
+
all_changed_lines,
|
|
2881
|
+
defer_scope_to_caller,
|
|
2882
|
+
)
|
|
2883
|
+
return scoped_issues[:MAX_DOCSTRING_PROSE_WALL_ISSUES]
|
|
2790
2884
|
|
|
2791
2885
|
|
|
2792
2886
|
def _raises_section_text(docstring_text: str) -> str:
|
|
@@ -18,6 +18,7 @@ concern focused. The separate ``tdd_enforcer.py`` hook accepts any
|
|
|
18
18
|
import json
|
|
19
19
|
import sys
|
|
20
20
|
from collections import Counter
|
|
21
|
+
from collections.abc import Callable
|
|
21
22
|
from pathlib import Path
|
|
22
23
|
from typing import TextIO
|
|
23
24
|
|
|
@@ -285,7 +286,15 @@ def validate_content(
|
|
|
285
286
|
all_issues.extend(check_logging_adjacent_string_literals(content, file_path))
|
|
286
287
|
all_issues.extend(check_windows_api_none(content))
|
|
287
288
|
all_issues.extend(check_naive_datetime_construction(content, file_path))
|
|
288
|
-
all_issues.extend(
|
|
289
|
+
all_issues.extend(
|
|
290
|
+
_fragment_or_deferred_check(
|
|
291
|
+
check_magic_values,
|
|
292
|
+
old_content,
|
|
293
|
+
content,
|
|
294
|
+
file_path,
|
|
295
|
+
defer_scope_to_caller,
|
|
296
|
+
)
|
|
297
|
+
)
|
|
289
298
|
all_issues.extend(check_fstring_structural_literals(content, file_path))
|
|
290
299
|
all_issues.extend(check_constants_outside_config(content, file_path))
|
|
291
300
|
all_issues.extend(check_config_duplicate_path_anchor(content, file_path))
|
|
@@ -358,9 +367,21 @@ def validate_content(
|
|
|
358
367
|
all_issues.extend(
|
|
359
368
|
check_class_docstring_names_public_methods(effective_content, file_path)
|
|
360
369
|
)
|
|
361
|
-
all_issues.extend(check_docstring_runon_sentence(effective_content, file_path))
|
|
362
370
|
all_issues.extend(
|
|
363
|
-
|
|
371
|
+
check_docstring_runon_sentence(
|
|
372
|
+
effective_content,
|
|
373
|
+
file_path,
|
|
374
|
+
all_changed_lines,
|
|
375
|
+
defer_scope_to_caller,
|
|
376
|
+
)
|
|
377
|
+
)
|
|
378
|
+
all_issues.extend(
|
|
379
|
+
check_docstring_prose_wall_without_illustration(
|
|
380
|
+
effective_content,
|
|
381
|
+
file_path,
|
|
382
|
+
all_changed_lines,
|
|
383
|
+
defer_scope_to_caller,
|
|
384
|
+
)
|
|
364
385
|
)
|
|
365
386
|
all_issues.extend(
|
|
366
387
|
check_module_docstring_names_public_checks(effective_content, file_path)
|
|
@@ -508,8 +529,24 @@ def validate_content(
|
|
|
508
529
|
all_issues.extend(check_polarity_name_contradiction(content, file_path))
|
|
509
530
|
all_issues.extend(check_inline_literal_collections(content, file_path))
|
|
510
531
|
all_issues.extend(check_inline_tuple_string_magic(content, file_path))
|
|
511
|
-
all_issues.extend(
|
|
512
|
-
|
|
532
|
+
all_issues.extend(
|
|
533
|
+
_fragment_or_deferred_check(
|
|
534
|
+
check_join_separator_string_magic,
|
|
535
|
+
old_content,
|
|
536
|
+
content,
|
|
537
|
+
file_path,
|
|
538
|
+
defer_scope_to_caller,
|
|
539
|
+
)
|
|
540
|
+
)
|
|
541
|
+
all_issues.extend(
|
|
542
|
+
_fragment_or_deferred_check(
|
|
543
|
+
check_string_literal_magic,
|
|
544
|
+
old_content,
|
|
545
|
+
content,
|
|
546
|
+
file_path,
|
|
547
|
+
defer_scope_to_caller,
|
|
548
|
+
)
|
|
549
|
+
)
|
|
513
550
|
all_issues.extend(check_whitespace_indentation_magic(content, file_path))
|
|
514
551
|
all_issues.extend(check_orphan_css_classes(effective_content, file_path))
|
|
515
552
|
check_incomplete_mocks(content, file_path)
|
|
@@ -723,6 +760,124 @@ def _without_line_prefix(violation_text: str) -> str:
|
|
|
723
760
|
return violation_text
|
|
724
761
|
|
|
725
762
|
|
|
763
|
+
def _issues_absent_from_prior_bodies(
|
|
764
|
+
all_candidate_issues: list[str],
|
|
765
|
+
all_prior_issues: list[str],
|
|
766
|
+
) -> list[str]:
|
|
767
|
+
"""Return candidates whose message bodies are not covered by prior issues.
|
|
768
|
+
|
|
769
|
+
Matching is line-number-agnostic with per-occurrence accounting: each prior
|
|
770
|
+
entry consumes exactly one candidate carrying the same body.
|
|
771
|
+
|
|
772
|
+
::
|
|
773
|
+
|
|
774
|
+
prior: ["Line 1: magic 'X'"]
|
|
775
|
+
candidates: ["Line 4: magic 'X'", "Line 9: magic 'Y'"]
|
|
776
|
+
-> ["Line 9: magic 'Y'"]
|
|
777
|
+
|
|
778
|
+
Args:
|
|
779
|
+
all_candidate_issues: Findings from the scan under review.
|
|
780
|
+
all_prior_issues: Findings that already account for a body.
|
|
781
|
+
|
|
782
|
+
Returns:
|
|
783
|
+
Candidates not consumed by a matching prior body.
|
|
784
|
+
"""
|
|
785
|
+
remaining_prior_counts = Counter(
|
|
786
|
+
_without_line_prefix(each_issue) for each_issue in all_prior_issues
|
|
787
|
+
)
|
|
788
|
+
all_uncovered_issues: list[str] = []
|
|
789
|
+
for each_issue in all_candidate_issues:
|
|
790
|
+
message_body = _without_line_prefix(each_issue)
|
|
791
|
+
if remaining_prior_counts[message_body] > 0:
|
|
792
|
+
remaining_prior_counts[message_body] -= 1
|
|
793
|
+
continue
|
|
794
|
+
all_uncovered_issues.append(each_issue)
|
|
795
|
+
return all_uncovered_issues
|
|
796
|
+
|
|
797
|
+
|
|
798
|
+
def _fragment_or_deferred_check(
|
|
799
|
+
check_function: Callable[[str, str], list[str]],
|
|
800
|
+
old_content: str,
|
|
801
|
+
new_content: str,
|
|
802
|
+
file_path: str,
|
|
803
|
+
defer_scope_to_caller: bool,
|
|
804
|
+
) -> list[str]:
|
|
805
|
+
"""Run a check with fragment baselining, or full-file when the gate owns scope.
|
|
806
|
+
|
|
807
|
+
The commit/push gate sets ``defer_scope_to_caller`` and passes HEAD as
|
|
808
|
+
``old_content`` while scanning the current file (often the same blob on a
|
|
809
|
+
clean worktree). Fragment baselining would grandfather every finding there,
|
|
810
|
+
so the gate path runs the check on ``new_content`` alone and classifies by
|
|
811
|
+
added line afterward. PreToolUse Edit keeps baselining against the prior
|
|
812
|
+
fragment.
|
|
813
|
+
|
|
814
|
+
::
|
|
815
|
+
|
|
816
|
+
defer=True, old==new with magic 9999
|
|
817
|
+
-> [Line 2: Magic value 9999 ...] gate still sees it
|
|
818
|
+
defer=False, old and new both carry the same magic
|
|
819
|
+
-> [] grandfathered for wide Edit
|
|
820
|
+
|
|
821
|
+
Args:
|
|
822
|
+
check_function: A ``(content, file_path) -> list[str]`` check.
|
|
823
|
+
old_content: Prior fragment or gate HEAD blob.
|
|
824
|
+
new_content: Proposed fragment or current full file.
|
|
825
|
+
file_path: Destination path used for path-based exemptions.
|
|
826
|
+
defer_scope_to_caller: True when the gate will scope by added line.
|
|
827
|
+
|
|
828
|
+
Returns:
|
|
829
|
+
Findings from the check, baselined against ``old_content`` only when
|
|
830
|
+
``defer_scope_to_caller`` is False.
|
|
831
|
+
"""
|
|
832
|
+
if defer_scope_to_caller:
|
|
833
|
+
return check_function(new_content, file_path)
|
|
834
|
+
return _issues_introduced_in_fragment(
|
|
835
|
+
check_function, old_content, new_content, file_path
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
|
|
839
|
+
def _issues_introduced_in_fragment(
|
|
840
|
+
check_function: Callable[[str, str], list[str]],
|
|
841
|
+
old_content: str,
|
|
842
|
+
new_content: str,
|
|
843
|
+
file_path: str,
|
|
844
|
+
) -> list[str]:
|
|
845
|
+
"""Return fragment findings that are new relative to the pre-edit fragment.
|
|
846
|
+
|
|
847
|
+
Grades both the prior and proposed Edit fragments with the same check, then
|
|
848
|
+
subtracts findings whose message body already appears in the prior scan.
|
|
849
|
+
Per-occurrence accounting keeps a second identical new finding when only
|
|
850
|
+
one matching body existed before. Line numbers on kept findings come from
|
|
851
|
+
the proposed fragment so diagnostics point at the introduced location.
|
|
852
|
+
|
|
853
|
+
::
|
|
854
|
+
|
|
855
|
+
old: def f(): return os.environ['STRIPE_SECRET']
|
|
856
|
+
new: def f(): return os.environ['STRIPE_SECRET'] # same body
|
|
857
|
+
-> [] grandfathered
|
|
858
|
+
old: def f(): return 0
|
|
859
|
+
new: def f(): return os.environ['STRIPE_SECRET']
|
|
860
|
+
-> [Line ...: string magic value 'STRIPE_SECRET' ...]
|
|
861
|
+
|
|
862
|
+
Args:
|
|
863
|
+
check_function: A ``(content, file_path) -> list[str]`` check.
|
|
864
|
+
old_content: The Edit's prior ``old_string`` fragment, or empty when
|
|
865
|
+
there is no prior region to baseline against.
|
|
866
|
+
new_content: The Edit's ``new_string`` fragment (or Write body).
|
|
867
|
+
file_path: Destination path used for path-based exemptions.
|
|
868
|
+
|
|
869
|
+
Returns:
|
|
870
|
+
Findings present in ``new_content`` that are absent from ``old_content``
|
|
871
|
+
under line-number-agnostic body matching. When ``old_content`` is empty,
|
|
872
|
+
every finding from ``new_content`` is returned.
|
|
873
|
+
"""
|
|
874
|
+
all_new_issues = check_function(new_content, file_path)
|
|
875
|
+
if not old_content:
|
|
876
|
+
return all_new_issues
|
|
877
|
+
all_old_issues = check_function(old_content, file_path)
|
|
878
|
+
return _issues_absent_from_prior_bodies(all_new_issues, all_old_issues)
|
|
879
|
+
|
|
880
|
+
|
|
726
881
|
def _forecast_full_file_violations(
|
|
727
882
|
full_file_content_after_edit: str,
|
|
728
883
|
file_path: str,
|
|
@@ -763,17 +918,7 @@ def _forecast_full_file_violations(
|
|
|
763
918
|
all_full_file_issues = validate_content(
|
|
764
919
|
full_file_content_after_edit, file_path, prior_full_file_content
|
|
765
920
|
)
|
|
766
|
-
|
|
767
|
-
_without_line_prefix(each_issue) for each_issue in all_blocking_issues
|
|
768
|
-
)
|
|
769
|
-
forecast_issues: list[str] = []
|
|
770
|
-
for each_issue in all_full_file_issues:
|
|
771
|
-
message_body = _without_line_prefix(each_issue)
|
|
772
|
-
if remaining_blocking_counts[message_body] > 0:
|
|
773
|
-
remaining_blocking_counts[message_body] -= 1
|
|
774
|
-
continue
|
|
775
|
-
forecast_issues.append(each_issue)
|
|
776
|
-
return forecast_issues
|
|
921
|
+
return _issues_absent_from_prior_bodies(all_full_file_issues, all_blocking_issues)
|
|
777
922
|
|
|
778
923
|
|
|
779
924
|
def _precheck_hint() -> str:
|