claude-dev-env 2.12.0 → 2.14.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/AGENTS.md +61 -0
- package/CLAUDE.md +1 -57
- package/_shared/AGENTS.md +15 -0
- package/_shared/CLAUDE.md +1 -15
- package/_shared/advisor/AGENTS.md +22 -0
- package/_shared/advisor/CLAUDE.md +1 -22
- package/_shared/pr-loop/AGENTS.md +27 -0
- package/_shared/pr-loop/CLAUDE.md +1 -27
- package/_shared/pr-loop/scripts/AGENTS.md +35 -0
- package/_shared/pr-loop/scripts/CLAUDE.md +1 -35
- package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
- package/_shared/pr-loop/scripts/code_rules_gate_parts/AGENTS.md +42 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +1 -42
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/AGENTS.md +26 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -26
- package/_shared/pr-loop/scripts/test_code_rules_gate.py +0 -26
- package/_shared/pr-loop/scripts/tests/AGENTS.md +44 -0
- package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -44
- package/_shared/process-tree/AGENTS.md +41 -0
- package/_shared/process-tree/CLAUDE.md +1 -41
- package/agents/AGENTS.md +31 -0
- package/agents/CLAUDE.md +1 -32
- package/agents/clean-coder.md +6 -6
- package/agents/pr-description-writer.md +11 -81
- package/agents/test_agent_frontmatter.py +4 -4
- package/audit-rubrics/AGENTS.md +43 -0
- package/audit-rubrics/CLAUDE.md +1 -43
- package/audit-rubrics/category_rubrics/AGENTS.md +37 -0
- package/audit-rubrics/category_rubrics/CLAUDE.md +1 -37
- package/audit-rubrics/prompts/AGENTS.md +37 -0
- package/audit-rubrics/prompts/CLAUDE.md +1 -37
- package/bin/AGENTS.md +112 -0
- package/bin/CLAUDE.md +1 -112
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install-constants.mjs +6 -7
- package/bin/install.mjs +71 -13
- package/bin/install.prune.test.mjs +26 -0
- package/bin/install.settings-defaults.test.mjs +60 -33
- package/bin/install.test.mjs +36 -4
- package/commands/AGENTS.md +23 -0
- package/commands/CLAUDE.md +1 -23
- package/commands/hook-log-extract.md +3 -3
- package/commands/sr-loop.md +9 -5
- package/docs/AGENTS.md +32 -0
- package/docs/CLAUDE.md +1 -33
- package/docs/CODE_RULES.md +1 -1
- package/docs/imagegen.md +22 -0
- package/docs/references/AGENTS.md +17 -0
- package/docs/references/CLAUDE.md +1 -18
- package/hooks/AGENTS.md +31 -0
- package/hooks/CLAUDE.md +1 -31
- package/hooks/_gh_pr_author_swap_utils.py +14 -0
- package/hooks/advisory/AGENTS.md +16 -0
- package/hooks/advisory/CLAUDE.md +1 -16
- package/hooks/blocking/AGENTS.md +123 -0
- package/hooks/blocking/CLAUDE.md +1 -148
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/AGENTS.md +28 -0
- package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +1 -28
- package/hooks/blocking/code_rules_banned_identifiers.py +1 -1
- package/hooks/blocking/code_rules_dead_module_constant.py +149 -37
- package/hooks/blocking/code_rules_probe_chains.py +6 -2
- package/hooks/blocking/config/AGENTS.md +10 -0
- package/hooks/blocking/config/CLAUDE.md +1 -22
- package/hooks/blocking/conventional_pr_title_gate.py +1 -2
- package/hooks/blocking/gh_pr_author_restore.py +7 -2
- package/hooks/blocking/intent_only_ending_blocker.py +11 -16
- package/hooks/blocking/inventory_intent_records/AGENTS.md +26 -0
- package/hooks/blocking/inventory_intent_records/CLAUDE.md +1 -26
- package/hooks/blocking/package_inventory_stale_blocker_parts/AGENTS.md +26 -0
- package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +1 -26
- package/hooks/blocking/pii_prevention_blocker_parts/AGENTS.md +24 -0
- package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +1 -24
- package/hooks/blocking/send_user_file_open_locally_blocker.py +1 -1
- package/hooks/blocking/stop_dispatcher.py +4 -4
- package/hooks/blocking/tdd_enforcer_parts/AGENTS.md +30 -0
- package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +1 -30
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -3
- package/hooks/blocking/test_code_rules_enforcer_banned_prefixes.py +1 -1
- package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +171 -0
- package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
- package/hooks/blocking/test_gh_pr_author_restore.py +2 -1
- package/hooks/blocking/test_intent_only_ending_blocker.py +4 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +6 -7
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +4 -3
- package/hooks/blocking/test_shared_stdin_adoption.py +0 -2
- package/hooks/diagnostic/AGENTS.md +43 -0
- package/hooks/diagnostic/CLAUDE.md +1 -43
- package/hooks/diagnostic/hook_log_extractor.py +4 -38
- package/hooks/diagnostic/hook_log_stop_wrapper.py +6 -155
- package/hooks/diagnostic/migrations/AGENTS.md +16 -0
- package/hooks/diagnostic/migrations/CLAUDE.md +1 -16
- package/hooks/diagnostic/queries/AGENTS.md +19 -0
- package/hooks/diagnostic/queries/CLAUDE.md +1 -19
- package/hooks/diagnostic/test_hook_log_extractor.py +10 -23
- package/hooks/diagnostic/test_hook_log_stop_wrapper.py +3 -331
- package/hooks/git-hooks/AGENTS.md +31 -0
- package/hooks/git-hooks/CLAUDE.md +1 -31
- package/hooks/git-hooks/git_hooks_constants/AGENTS.md +21 -0
- package/hooks/git-hooks/git_hooks_constants/CLAUDE.md +1 -21
- package/hooks/git-hooks/git_hooks_constants/__init__.py +4 -12
- package/hooks/git-hooks/pre_push.py +49 -118
- package/hooks/git-hooks/test_pre_push.py +111 -137
- package/hooks/hooks.json +0 -35
- package/hooks/hooks_constants/AGENTS.md +104 -0
- package/hooks/hooks_constants/CLAUDE.md +1 -113
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -11
- package/hooks/hooks_constants/convergence_branch_constants.py +0 -1
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +3 -4
- package/hooks/hooks_constants/hook_log_extractor_constants.py +1 -2
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -8
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +1 -6
- package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/task_list_loop_starter_constants.py +4 -3
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -26
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +0 -5
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -1
- package/hooks/hooks_constants/working_style_prompt_constants.py +29 -20
- package/hooks/lifecycle/AGENTS.md +18 -0
- package/hooks/lifecycle/CLAUDE.md +1 -20
- package/hooks/observability/AGENTS.md +20 -0
- package/hooks/observability/CLAUDE.md +1 -20
- package/hooks/session/AGENTS.md +33 -0
- package/hooks/session/CLAUDE.md +1 -33
- package/hooks/session/gh_pr_author_session_cleanup.py +7 -2
- package/hooks/session/task_list_loop_starter.py +3 -3
- package/hooks/session/test_gh_pr_author_session_cleanup.py +2 -1
- package/hooks/session/test_task_list_loop_starter.py +10 -2
- package/hooks/session/test_working_style_prompt.py +9 -4
- package/hooks/validation/AGENTS.md +20 -0
- package/hooks/validation/CLAUDE.md +1 -20
- package/hooks/validators/AGENTS.md +51 -0
- package/hooks/validators/CLAUDE.md +1 -51
- package/hooks/validators/health_check.py +52 -19
- package/hooks/validators/python_style_checks.py +1 -1
- package/hooks/validators/test_health_check.py +112 -27
- package/hooks/workflow/AGENTS.md +16 -0
- package/hooks/workflow/CLAUDE.md +1 -16
- package/installable-surfaces.manifest.json +1 -0
- package/output-styles/AGENTS.md +15 -0
- package/output-styles/CLAUDE.md +1 -15
- package/package.json +2 -1
- package/rules/AGENTS.md +61 -0
- package/rules/CLAUDE.md +1 -63
- package/rules/code-standards.md +5 -9
- package/rules/doc-prose-cuts.md +1 -1
- package/rules/gh-cli-conventions.md +0 -1
- package/rules/git-workflow.md +1 -3
- package/scripts/AGENTS.md +62 -0
- package/scripts/CLAUDE.md +1 -63
- package/scripts/_code_review_test_support.py +0 -95
- package/scripts/codex_compat_materializer.py +4 -0
- package/scripts/dev_env_scripts_constants/AGENTS.md +21 -0
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -21
- package/scripts/dev_env_scripts_constants/code_review_constants.py +9 -98
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +1 -44
- package/scripts/invoke_code_review.py +8 -451
- package/scripts/sync_to_cursor/AGENTS.md +23 -0
- package/scripts/sync_to_cursor/CLAUDE.md +1 -23
- package/scripts/test_invoke_code_review.py +5 -61
- package/scripts/test_invoke_code_review_cli.py +1 -45
- package/scripts/test_invoke_code_review_contract.py +1 -118
- package/scripts/test_validate_instruction_pairs.py +90 -0
- package/scripts/test_verify_installable_package.py +5 -1
- package/scripts/tests/AGENTS.md +33 -0
- package/scripts/tests/CLAUDE.md +1 -33
- package/scripts/tests/test_codex_compat_materializer.py +36 -1
- package/scripts/validate_instruction_pairs.py +183 -0
- package/settings.json +1 -10
- package/skills/AGENTS.md +72 -0
- package/skills/CLAUDE.md +1 -64
- package/skills/_shared/AGENTS.md +44 -0
- package/skills/_shared/CLAUDE.md +1 -44
- package/skills/_shared/advisor/AGENTS.md +9 -0
- package/skills/_shared/advisor/CLAUDE.md +1 -9
- package/skills/_shared/pr-loop/AGENTS.md +50 -0
- package/skills/_shared/pr-loop/CLAUDE.md +1 -50
- package/skills/_shared/pr-loop/prompts/AGENTS.md +9 -0
- package/skills/_shared/pr-loop/prompts/CLAUDE.md +1 -9
- package/skills/_shared/pr-loop/scripts/AGENTS.md +35 -0
- package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -35
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +24 -0
- package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -24
- package/skills/anthropic-plan/AGENTS.md +34 -0
- package/skills/anthropic-plan/CLAUDE.md +1 -34
- package/skills/anthropic-plan/scripts/AGENTS.md +11 -0
- package/skills/anthropic-plan/scripts/CLAUDE.md +1 -11
- package/skills/anthropic-plan/scripts/anthropic_plan_scripts_constants/AGENTS.md +16 -0
- package/skills/anthropic-plan/scripts/anthropic_plan_scripts_constants/CLAUDE.md +1 -16
- package/skills/anthropic-plan/templates/AGENTS.md +13 -0
- package/skills/anthropic-plan/templates/CLAUDE.md +1 -13
- package/skills/anthropic-plan/workflow/AGENTS.md +14 -0
- package/skills/anthropic-plan/workflow/CLAUDE.md +1 -14
- package/skills/auditing-claude-config/AGENTS.md +21 -0
- package/skills/auditing-claude-config/CLAUDE.md +1 -21
- package/skills/autoconverge/AGENTS.md +36 -0
- package/skills/autoconverge/CLAUDE.md +1 -36
- package/skills/autoconverge/reference/AGENTS.md +16 -0
- package/skills/autoconverge/reference/CLAUDE.md +1 -16
- package/skills/autoconverge/reference/convergence.md +7 -7
- package/skills/autoconverge/reference/gotchas.md +3 -10
- package/skills/autoconverge/workflow/AGENTS.md +26 -0
- package/skills/autoconverge/workflow/CLAUDE.md +1 -26
- package/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +16 -0
- package/skills/autoconverge/workflow/autoconverge_report_constants/CLAUDE.md +1 -16
- package/skills/autoconverge/workflow/converge.contract.test.mjs +53 -1388
- package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +4 -3
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +33 -493
- package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +15 -97
- package/skills/autoconverge/workflow/converge.mjs +201 -238
- package/skills/autoconverge/workflow/converge.precatch.test.mjs +1 -1
- package/skills/bugteam/AGENTS.md +30 -0
- package/skills/bugteam/CLAUDE.md +1 -30
- package/skills/bugteam/CONSTRAINTS.md +1 -1
- package/skills/bugteam/SKILL.md +9 -0
- package/skills/bugteam/reference/AGENTS.md +20 -0
- package/skills/bugteam/reference/CLAUDE.md +1 -20
- package/skills/bugteam/reference/audit-and-teammates.md +10 -6
- package/skills/bugteam/reference/obstacles/AGENTS.md +24 -0
- package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -24
- package/skills/bugteam/scripts/AGENTS.md +30 -0
- package/skills/bugteam/scripts/CLAUDE.md +1 -30
- package/skills/bugteam/scripts/bugteam_scripts_constants/AGENTS.md +18 -0
- package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +1 -18
- package/skills/bugteam/test_skill_additions.py +9 -0
- package/skills/closeout/reference/handoff-prompt-template.md +1 -1
- package/skills/codex-review/AGENTS.md +46 -0
- package/skills/codex-review/CLAUDE.md +1 -46
- package/skills/codex-review/reference/AGENTS.md +15 -0
- package/skills/codex-review/reference/CLAUDE.md +1 -15
- package/skills/codex-review/scripts/codex_review_scripts_constants/AGENTS.md +18 -0
- package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +1 -18
- package/skills/codex-review/test_skill_scaffold.py +3 -3
- package/skills/comments/SKILL.md +65 -0
- package/skills/copilot-review/AGENTS.md +18 -0
- package/skills/copilot-review/CLAUDE.md +1 -18
- package/skills/descriptions/SKILL.md +51 -0
- package/skills/emergencies/SKILL.md +42 -0
- package/skills/everything-search/AGENTS.md +17 -0
- package/skills/everything-search/CLAUDE.md +1 -17
- package/skills/fresh-branch/AGENTS.md +14 -0
- package/skills/fresh-branch/CLAUDE.md +1 -14
- package/skills/grok-spawn/AGENTS.md +28 -0
- package/skills/grok-spawn/CLAUDE.md +1 -28
- package/skills/orchestrator/SKILL.md +2 -4
- package/skills/plan-to-pr/SKILL.md +6 -7
- package/skills/plan-to-pr/reference/final-validation-tasks.md +1 -1
- package/skills/plan-to-pr/reference/process-inventory.md +2 -2
- package/skills/plan-to-pr/reference/review-loop.md +2 -2
- package/skills/plan-to-pr/reference/run-record.schema.json +1 -1
- package/skills/plan-to-pr/reference/task-seeds.md +1 -1
- package/skills/plan-to-pr/reference/task-ticket.md +3 -4
- package/skills/plan-to-pr/scripts/config/constants.py +1 -2
- package/skills/plan-to-pr/scripts/test_validate_protocol.py +1 -1
- package/skills/plan-to-pr/test_skill_contract.py +2 -2
- package/skills/pr-converge/AGENTS.md +32 -0
- package/skills/pr-converge/CLAUDE.md +1 -32
- package/skills/pr-converge/SKILL.md +13 -16
- package/skills/pr-converge/pr_converge_skill_constants/AGENTS.md +26 -0
- package/skills/pr-converge/pr_converge_skill_constants/CLAUDE.md +1 -26
- package/skills/pr-converge/reference/AGENTS.md +28 -0
- package/skills/pr-converge/reference/CLAUDE.md +1 -28
- package/skills/pr-converge/reference/obstacles/AGENTS.md +23 -0
- package/skills/pr-converge/reference/obstacles/CLAUDE.md +1 -23
- package/skills/pr-converge/reference/progress-checklist.md +6 -5
- package/skills/pr-converge/reference/state-schema.md +1 -27
- package/skills/pr-converge/scripts/AGENTS.md +46 -0
- package/skills/pr-converge/scripts/CLAUDE.md +1 -46
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/AGENTS.md +18 -0
- package/skills/pr-converge/scripts/pr_converge_scripts_constants/CLAUDE.md +1 -18
- package/skills/pr-converge/workflows/AGENTS.md +16 -0
- package/skills/pr-converge/workflows/CLAUDE.md +1 -16
- package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
- package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +3 -3
- package/skills/pr-loop-lifecycle/SKILL.md +1 -1
- package/skills/pr-loop-lifecycle/reference/teardown-publish-permissions.md +1 -1
- package/skills/prototype/SKILL.md +4 -4
- package/skills/prototype/reference/honest-limitations.md +5 -5
- package/skills/prototype/reference/promotion-tasks.md +2 -2
- package/skills/prototype/workflows/promotion.md +4 -6
- package/skills/rebase/AGENTS.md +32 -0
- package/skills/rebase/CLAUDE.md +1 -32
- package/skills/recall/AGENTS.md +30 -0
- package/skills/recall/CLAUDE.md +1 -30
- package/skills/remember/AGENTS.md +31 -0
- package/skills/remember/CLAUDE.md +1 -31
- package/skills/reviews/SKILL.md +85 -0
- package/skills/session-log/AGENTS.md +32 -0
- package/skills/session-log/CLAUDE.md +1 -32
- package/skills/session-tidy/AGENTS.md +36 -0
- package/skills/session-tidy/CLAUDE.md +1 -36
- package/skills/skill-builder/AGENTS.md +49 -0
- package/skills/skill-builder/CLAUDE.md +1 -49
- package/skills/skill-builder/references/AGENTS.md +25 -0
- package/skills/skill-builder/references/CLAUDE.md +1 -25
- package/skills/skill-builder/references/deterministic-elements.md +1 -1
- package/skills/skill-builder/templates/AGENTS.md +14 -0
- package/skills/skill-builder/templates/CLAUDE.md +1 -14
- package/skills/skill-builder/workflows/AGENTS.md +19 -0
- package/skills/skill-builder/workflows/CLAUDE.md +1 -19
- package/skills/small-cl/SKILL.md +52 -0
- package/skills/task-build/AGENTS.md +29 -0
- package/skills/task-build/CLAUDE.md +1 -29
- package/skills/update/AGENTS.md +38 -0
- package/skills/update/CLAUDE.md +1 -38
- package/system-prompts/AGENTS.md +17 -0
- package/system-prompts/CLAUDE.md +1 -17
- package/system-prompts/software-engineer.xml +3 -3
- package/agents/code-verifier.md +0 -74
- package/docs/PR_DESCRIPTION_GUIDE.md +0 -157
- package/docs/references/code-review-enforcement.md +0 -132
- package/hooks/blocking/code_review_enforcement_config_bootstrap.py +0 -53
- package/hooks/blocking/code_review_gate_deny.py +0 -74
- package/hooks/blocking/code_review_pr_create_gate.py +0 -198
- package/hooks/blocking/code_review_push_gate.py +0 -145
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +0 -348
- package/hooks/blocking/code_review_stamp_store.py +0 -233
- package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +0 -7
- package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +0 -15
- package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +0 -212
- package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +0 -138
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +0 -49
- package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +0 -38
- package/hooks/blocking/code_verifier_spawn_preflight_gate.py +0 -483
- package/hooks/blocking/config/code_review_enforcement_constants.py +0 -143
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +0 -166
- package/hooks/blocking/config/verified_commit_constants.py +0 -160
- package/hooks/blocking/config/verified_commit_context_constants.py +0 -21
- package/hooks/blocking/config/verified_commit_gate_output_constants.py +0 -14
- package/hooks/blocking/conftest.py +0 -123
- package/hooks/blocking/convergence_gate_blocker.py +0 -310
- package/hooks/blocking/pr_converge_bugteam_enforcer.py +0 -170
- package/hooks/blocking/pr_description_body_audit.py +0 -148
- package/hooks/blocking/pr_description_command_parser.py +0 -237
- package/hooks/blocking/pr_description_enforcer.py +0 -227
- package/hooks/blocking/pr_description_pr_number.py +0 -155
- package/hooks/blocking/pr_description_proof_of_work.py +0 -385
- package/hooks/blocking/pr_description_readability.py +0 -364
- package/hooks/blocking/reviewer_spawn_gate.py +0 -182
- package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +0 -62
- package/hooks/blocking/test_code_review_gate_deny.py +0 -54
- package/hooks/blocking/test_code_review_pr_create_gate.py +0 -199
- package/hooks/blocking/test_code_review_push_gate.py +0 -205
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +0 -199
- package/hooks/blocking/test_code_review_stamp_store.py +0 -205
- package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +0 -710
- package/hooks/blocking/test_code_verifier_tools_contract.py +0 -28
- package/hooks/blocking/test_convergence_gate_blocker.py +0 -428
- package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +0 -311
- package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +0 -184
- package/hooks/blocking/test_pr_description_enforcer.py +0 -221
- package/hooks/blocking/test_pr_description_enforcer_body_audit.py +0 -247
- package/hooks/blocking/test_pr_description_enforcer_body_rules.py +0 -493
- package/hooks/blocking/test_pr_description_enforcer_command_parser.py +0 -366
- package/hooks/blocking/test_pr_description_enforcer_pr_number.py +0 -159
- package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +0 -175
- package/hooks/blocking/test_pr_description_enforcer_readability.py +0 -443
- package/hooks/blocking/test_pr_description_proof_of_work.py +0 -313
- package/hooks/blocking/test_reviewer_spawn_gate.py +0 -230
- package/hooks/blocking/test_verdict_directory_write_blocker.py +0 -804
- package/hooks/blocking/test_verification_verdict_store.py +0 -974
- package/hooks/blocking/test_verified_commit_config_bootstrap.py +0 -67
- package/hooks/blocking/test_verified_commit_docs_delta.py +0 -176
- package/hooks/blocking/test_verified_commit_gate.py +0 -581
- package/hooks/blocking/test_verified_commit_gate_additional_context.py +0 -134
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +0 -131
- package/hooks/blocking/test_verifier_verdict_minter.py +0 -299
- package/hooks/blocking/tests/test_verified_commit_gate.py +0 -41
- package/hooks/blocking/verdict_directory_write_blocker.py +0 -687
- package/hooks/blocking/verification_verdict_store.py +0 -1039
- package/hooks/blocking/verified_commit_config_bootstrap.py +0 -63
- package/hooks/blocking/verified_commit_gate.py +0 -113
- package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +0 -28
- package/hooks/blocking/verified_commit_gate_parts/__init__.py +0 -1
- package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +0 -174
- package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +0 -53
- package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +0 -80
- package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +0 -170
- package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +0 -217
- package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +0 -10
- package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +0 -94
- package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +0 -17
- package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +0 -38
- package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +0 -71
- package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +0 -96
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +0 -167
- package/hooks/blocking/verifier_verdict_minter.py +0 -280
- package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +0 -64
- package/hooks/hooks_constants/convergence_gate_blocker_constants.py +0 -53
- package/hooks/hooks_constants/pr_converge_bugteam_enforcer_constants.py +0 -55
- package/hooks/hooks_constants/pr_converge_bugteam_enforcer_state.py +0 -67
- package/hooks/hooks_constants/pr_description_enforcer_constants.py +0 -167
- package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -107
- package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +0 -41
- package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +0 -292
- package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +0 -198
- package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +0 -283
- package/rules/proof-of-work-pr-comments.md +0 -22
- package/rules/verified-commit-gate-skip.md +0 -4
- package/scripts/Show-Asset.ps1 +0 -106
|
@@ -1,237 +0,0 @@
|
|
|
1
|
-
"""Parse gh pr create/edit/comment commands into auditable body content.
|
|
2
|
-
|
|
3
|
-
Tokenizes the captured shell command and extracts the PR body that should be
|
|
4
|
-
audited, resolving body-file paths and rejecting unauditable shell variables,
|
|
5
|
-
stdin sentinels, and path-traversal targets. Positional PR-number extraction
|
|
6
|
-
lives in pr_description_pr_number.py.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import shlex
|
|
10
|
-
import sys
|
|
11
|
-
from pathlib import Path
|
|
12
|
-
|
|
13
|
-
_hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
14
|
-
if _hooks_dir not in sys.path:
|
|
15
|
-
sys.path.insert(0, _hooks_dir)
|
|
16
|
-
|
|
17
|
-
from blocking._gh_body_arg_utils import ( # noqa: E402
|
|
18
|
-
all_body_flags,
|
|
19
|
-
body_file_flag,
|
|
20
|
-
body_file_short_flag,
|
|
21
|
-
count_extra_tokens_to_skip_for_split_quoted_value,
|
|
22
|
-
get_logical_first_line,
|
|
23
|
-
is_flag_shaped_token,
|
|
24
|
-
is_unresolvable_shell_value,
|
|
25
|
-
iter_significant_tokens,
|
|
26
|
-
match_body_file_equals_prefix,
|
|
27
|
-
match_body_flag_equals_prefix,
|
|
28
|
-
match_non_body_value_flag_equals_prefix,
|
|
29
|
-
non_body_value_flags,
|
|
30
|
-
strip_surrounding_quotes,
|
|
31
|
-
)
|
|
32
|
-
from hooks_constants.pr_description_enforcer_constants import ( # noqa: E402
|
|
33
|
-
BODY_FILE_STDIN_SENTINEL,
|
|
34
|
-
)
|
|
35
|
-
from hooks_constants.setup_project_paths_constants import UTF8_ENCODING # noqa: E402
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class PathTraversalError(Exception):
|
|
39
|
-
pass
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
def _read_body_file_contents(file_path: str) -> str | None:
|
|
43
|
-
given_path = Path(file_path)
|
|
44
|
-
allowed_root = Path.cwd().resolve()
|
|
45
|
-
if given_path.is_symlink():
|
|
46
|
-
resolved_target = given_path.resolve()
|
|
47
|
-
try:
|
|
48
|
-
resolved_target.relative_to(allowed_root)
|
|
49
|
-
except ValueError as traversal_error:
|
|
50
|
-
raise PathTraversalError(
|
|
51
|
-
"symlink target resolves outside allowed root"
|
|
52
|
-
) from traversal_error
|
|
53
|
-
resolved_path = given_path.resolve()
|
|
54
|
-
if not given_path.is_absolute():
|
|
55
|
-
try:
|
|
56
|
-
resolved_path.relative_to(allowed_root)
|
|
57
|
-
except ValueError as traversal_error:
|
|
58
|
-
raise PathTraversalError(
|
|
59
|
-
"relative path resolves outside allowed root"
|
|
60
|
-
) from traversal_error
|
|
61
|
-
try:
|
|
62
|
-
with open(resolved_path, "r", encoding=UTF8_ENCODING, errors="replace") as body_file:
|
|
63
|
-
return body_file.read()
|
|
64
|
-
except (FileNotFoundError, IsADirectoryError, PermissionError, OSError):
|
|
65
|
-
return None
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def _resolve_body_file_value(raw_value_token: str) -> str | None:
|
|
69
|
-
"""Return file contents, or None when the body cannot be audited.
|
|
70
|
-
|
|
71
|
-
None means body is present but unauditable -- skip enforcement.
|
|
72
|
-
This covers: stdin sentinel, unresolvable shell variables, and path-traversal-rejected paths.
|
|
73
|
-
"""
|
|
74
|
-
stripped_value = strip_surrounding_quotes(raw_value_token)
|
|
75
|
-
if not stripped_value:
|
|
76
|
-
return None
|
|
77
|
-
if stripped_value == BODY_FILE_STDIN_SENTINEL:
|
|
78
|
-
return None
|
|
79
|
-
if is_unresolvable_shell_value(stripped_value):
|
|
80
|
-
return None
|
|
81
|
-
try:
|
|
82
|
-
return _read_body_file_contents(stripped_value)
|
|
83
|
-
except PathTraversalError:
|
|
84
|
-
return None
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def _resolve_body_string_value(raw_value_token: str) -> str | None:
|
|
88
|
-
"""Return the literal body string, or None when the value is an
|
|
89
|
-
unresolvable shell variable.
|
|
90
|
-
|
|
91
|
-
Distinguishing the two cases lets `pr_description_enforcer.main()` skip enforcement only for
|
|
92
|
-
unauditable bodies; a literal `--body ""` still returns `""` and flows
|
|
93
|
-
into `validate_pr_body` so the substantive-prose check blocks it.
|
|
94
|
-
"""
|
|
95
|
-
stripped_value = strip_surrounding_quotes(raw_value_token)
|
|
96
|
-
if is_unresolvable_shell_value(stripped_value):
|
|
97
|
-
return None
|
|
98
|
-
return stripped_value
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def _reassemble_split_quoted_value(
|
|
102
|
-
first_value_token: str, all_remaining_tokens: list[str]
|
|
103
|
-
) -> str | None:
|
|
104
|
-
extra_tokens_consumed = count_extra_tokens_to_skip_for_split_quoted_value(
|
|
105
|
-
all_remaining_tokens,
|
|
106
|
-
first_value_token,
|
|
107
|
-
)
|
|
108
|
-
if extra_tokens_consumed is None:
|
|
109
|
-
return None
|
|
110
|
-
if extra_tokens_consumed == 0:
|
|
111
|
-
return first_value_token
|
|
112
|
-
continuation_tokens = all_remaining_tokens[:extra_tokens_consumed]
|
|
113
|
-
return " ".join([first_value_token, *continuation_tokens])
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def _scan_raw_tokens_for_body(all_raw_tokens: list[str]) -> str | None | bool:
|
|
117
|
-
"""Return the body value from a raw token list, or False if no body flag found.
|
|
118
|
-
|
|
119
|
-
Returns False when no body/body-file flag is present (caller should continue).
|
|
120
|
-
Returns None when a body-file flag is present but malformed (no value
|
|
121
|
-
follows), OR when the body value is an unresolvable shell variable (e.g.
|
|
122
|
-
`--body "$VAR"`) — in either case the body is unauditable and the caller
|
|
123
|
-
skips enforcement.
|
|
124
|
-
Returns str for resolved body string values. An empty string `""` is a
|
|
125
|
-
literal-empty body (e.g. `--body ""`) and must still flow into
|
|
126
|
-
`validate_pr_body` so the substantive-prose check blocks it.
|
|
127
|
-
"""
|
|
128
|
-
token_index = 0
|
|
129
|
-
while token_index < len(all_raw_tokens):
|
|
130
|
-
current_token = all_raw_tokens[token_index]
|
|
131
|
-
remaining_raw = all_raw_tokens[token_index + 1 :]
|
|
132
|
-
non_body_equals_prefix = match_non_body_value_flag_equals_prefix(current_token)
|
|
133
|
-
if non_body_equals_prefix is not None:
|
|
134
|
-
first_value_token = current_token[len(non_body_equals_prefix) :]
|
|
135
|
-
extra_skip = count_extra_tokens_to_skip_for_split_quoted_value(
|
|
136
|
-
remaining_raw, first_value_token
|
|
137
|
-
)
|
|
138
|
-
token_index += 1 + (extra_skip or 0)
|
|
139
|
-
continue
|
|
140
|
-
if current_token in non_body_value_flags:
|
|
141
|
-
if remaining_raw and not is_flag_shaped_token(remaining_raw[0]):
|
|
142
|
-
first_value_token = remaining_raw[0]
|
|
143
|
-
extra_skip = count_extra_tokens_to_skip_for_split_quoted_value(
|
|
144
|
-
remaining_raw[1:], first_value_token
|
|
145
|
-
)
|
|
146
|
-
token_index += 1 + 1 + (extra_skip or 0)
|
|
147
|
-
continue
|
|
148
|
-
token_index += 1
|
|
149
|
-
continue
|
|
150
|
-
body_equals_prefix = match_body_flag_equals_prefix(current_token)
|
|
151
|
-
if body_equals_prefix is not None:
|
|
152
|
-
first_value_token = current_token[len(body_equals_prefix) :]
|
|
153
|
-
full_value_token = _reassemble_split_quoted_value(first_value_token, remaining_raw)
|
|
154
|
-
if full_value_token is None:
|
|
155
|
-
return None
|
|
156
|
-
return _resolve_body_string_value(full_value_token)
|
|
157
|
-
body_file_equals_prefix = match_body_file_equals_prefix(current_token)
|
|
158
|
-
if body_file_equals_prefix is not None:
|
|
159
|
-
first_value_token = current_token[len(body_file_equals_prefix) :]
|
|
160
|
-
full_value_token = _reassemble_split_quoted_value(first_value_token, remaining_raw)
|
|
161
|
-
if full_value_token is None:
|
|
162
|
-
return None
|
|
163
|
-
return _resolve_body_file_value(full_value_token)
|
|
164
|
-
if current_token in all_body_flags:
|
|
165
|
-
if not remaining_raw or is_flag_shaped_token(remaining_raw[0]):
|
|
166
|
-
return None
|
|
167
|
-
first_value_token = remaining_raw[0]
|
|
168
|
-
full_value_token = _reassemble_split_quoted_value(first_value_token, remaining_raw[1:])
|
|
169
|
-
if full_value_token is None:
|
|
170
|
-
return None
|
|
171
|
-
return _resolve_body_string_value(full_value_token)
|
|
172
|
-
if current_token in {body_file_flag, body_file_short_flag}:
|
|
173
|
-
if not remaining_raw or is_flag_shaped_token(remaining_raw[0]):
|
|
174
|
-
return None
|
|
175
|
-
first_value_token = remaining_raw[0]
|
|
176
|
-
full_value_token = _reassemble_split_quoted_value(first_value_token, remaining_raw[1:])
|
|
177
|
-
if full_value_token is None:
|
|
178
|
-
return None
|
|
179
|
-
return _resolve_body_file_value(full_value_token)
|
|
180
|
-
token_index += 1
|
|
181
|
-
return False
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
def extract_body_from_command(
|
|
185
|
-
command: str,
|
|
186
|
-
all_pre_tokenized: tuple[str, list[str]] | None = None,
|
|
187
|
-
) -> str | None:
|
|
188
|
-
"""Return the PR body content for validation, or None if unextractable.
|
|
189
|
-
|
|
190
|
-
Uses iter_significant_tokens to skip values of non-body value-taking flags
|
|
191
|
-
so that --body/--body-file embedded in a quoted --title value never false-matches.
|
|
192
|
-
For space-form body-file flags, scans the raw token list directly because
|
|
193
|
-
iter_significant_tokens consumes the value token (yielding remaining-after-value).
|
|
194
|
-
|
|
195
|
-
If all_pre_tokenized is provided as (logical_line, raw_tokens), reuses those instead
|
|
196
|
-
of recomputing the logical line and shlex split a second time.
|
|
197
|
-
"""
|
|
198
|
-
if all_pre_tokenized is not None:
|
|
199
|
-
logical_line, all_raw_tokens = all_pre_tokenized
|
|
200
|
-
else:
|
|
201
|
-
logical_line = get_logical_first_line(command)
|
|
202
|
-
if not logical_line:
|
|
203
|
-
return None
|
|
204
|
-
try:
|
|
205
|
-
all_raw_tokens = shlex.split(logical_line, posix=False)
|
|
206
|
-
except ValueError:
|
|
207
|
-
return None
|
|
208
|
-
try:
|
|
209
|
-
all_significant_tokens = list(
|
|
210
|
-
iter_significant_tokens(command, pre_tokenized=(logical_line, all_raw_tokens))
|
|
211
|
-
)
|
|
212
|
-
except ValueError:
|
|
213
|
-
return None
|
|
214
|
-
|
|
215
|
-
significant_token_set = {each_token for each_token, _ in all_significant_tokens}
|
|
216
|
-
body_flag_found_in_significant = (
|
|
217
|
-
any(each_token in all_body_flags for each_token in significant_token_set)
|
|
218
|
-
or any(
|
|
219
|
-
match_body_flag_equals_prefix(each_token) is not None
|
|
220
|
-
for each_token in significant_token_set
|
|
221
|
-
)
|
|
222
|
-
or any(
|
|
223
|
-
match_body_file_equals_prefix(each_token) is not None
|
|
224
|
-
for each_token in significant_token_set
|
|
225
|
-
)
|
|
226
|
-
or any(
|
|
227
|
-
each_token in {body_file_flag, body_file_short_flag}
|
|
228
|
-
for each_token in significant_token_set
|
|
229
|
-
)
|
|
230
|
-
)
|
|
231
|
-
if not body_flag_found_in_significant:
|
|
232
|
-
return None
|
|
233
|
-
|
|
234
|
-
scan_outcome = _scan_raw_tokens_for_body(all_raw_tokens)
|
|
235
|
-
if isinstance(scan_outcome, bool):
|
|
236
|
-
return None
|
|
237
|
-
return scan_outcome
|
|
@@ -1,227 +0,0 @@
|
|
|
1
|
-
"""PreToolUse hook gating gh pr create/edit/comment bodies and gh pr ready.
|
|
2
|
-
|
|
3
|
-
Reads a PreToolUse JSON payload on stdin, audits body-carrying gh pr
|
|
4
|
-
create/edit/comment invocations against the Anthropic claude-code style
|
|
5
|
-
rules plus the proof-of-work audit for proof-shaped comment bodies, and
|
|
6
|
-
denies gh pr ready while the PR carries no passing proof comment.
|
|
7
|
-
Readability-management CLI flags short-circuit the stdin path to adjust
|
|
8
|
-
the persisted readability state.
|
|
9
|
-
"""
|
|
10
|
-
|
|
11
|
-
import json
|
|
12
|
-
import os
|
|
13
|
-
import sys
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
|
|
16
|
-
_hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
17
|
-
if _hooks_dir not in sys.path:
|
|
18
|
-
sys.path.insert(0, _hooks_dir)
|
|
19
|
-
|
|
20
|
-
from blocking.pr_description_body_audit import ( # noqa: E402
|
|
21
|
-
_body_contains_any_header,
|
|
22
|
-
_compute_pr_body_shape,
|
|
23
|
-
_count_substantive_prose_chars,
|
|
24
|
-
_extract_vague_scan_text,
|
|
25
|
-
_iter_section_headers,
|
|
26
|
-
_matches_self_closing_reference,
|
|
27
|
-
_opens_with_this_pr_phrase,
|
|
28
|
-
)
|
|
29
|
-
from blocking.pr_description_command_parser import ( # noqa: E402
|
|
30
|
-
extract_body_from_command,
|
|
31
|
-
)
|
|
32
|
-
from blocking.pr_description_pr_number import ( # noqa: E402
|
|
33
|
-
_command_carries_body_flag,
|
|
34
|
-
_extract_pr_number_from_command,
|
|
35
|
-
)
|
|
36
|
-
from blocking.pr_description_proof_of_work import ( # noqa: E402
|
|
37
|
-
audit_proof_comment_body,
|
|
38
|
-
evaluate_pr_ready_gate,
|
|
39
|
-
is_pr_ready_command,
|
|
40
|
-
is_proof_shaped_body,
|
|
41
|
-
)
|
|
42
|
-
from blocking.pr_description_readability import ( # noqa: E402
|
|
43
|
-
_build_readability_escape_hatch_message,
|
|
44
|
-
_dispatch_cli_flag,
|
|
45
|
-
_evaluate_readability_metrics,
|
|
46
|
-
_extract_readability_target_text,
|
|
47
|
-
_increment_strike_count,
|
|
48
|
-
_is_readability_enabled,
|
|
49
|
-
_load_readability_thresholds,
|
|
50
|
-
)
|
|
51
|
-
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
52
|
-
from hooks_constants.pr_description_enforcer_constants import ( # noqa: E402
|
|
53
|
-
ALL_HEAVY_OPENING_HEADERS,
|
|
54
|
-
ALL_HEAVY_TESTING_HEADERS,
|
|
55
|
-
ALL_READABILITY_CLI_FLAG_TOKENS,
|
|
56
|
-
HEAVY_SHAPE,
|
|
57
|
-
MINIMUM_SUBSTANTIVE_PROSE_CHARS,
|
|
58
|
-
PR_GUIDE_PATH,
|
|
59
|
-
READABILITY_STRIKE_THRESHOLD,
|
|
60
|
-
SELF_CLOSING_REFERENCE_MESSAGE_PREFIX,
|
|
61
|
-
SELF_CLOSING_REFERENCE_MESSAGE_SUFFIX,
|
|
62
|
-
TRIVIAL_BODY_CHAR_THRESHOLD,
|
|
63
|
-
VAGUE_LANGUAGE_PATTERN,
|
|
64
|
-
)
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
def validate_pr_body(body: str, pr_number: int | None = None) -> list[str]:
|
|
68
|
-
"""Audit a PR body against the Anthropic claude-code style rules.
|
|
69
|
-
|
|
70
|
-
Args:
|
|
71
|
-
body: The PR body markdown text to audit.
|
|
72
|
-
pr_number: The PR number when known (gh pr edit / gh pr comment); None at gh pr create time.
|
|
73
|
-
|
|
74
|
-
Returns:
|
|
75
|
-
A list of human-readable violation messages. Empty when the body passes.
|
|
76
|
-
"""
|
|
77
|
-
violations: list[str] = []
|
|
78
|
-
|
|
79
|
-
substantive_chars = _count_substantive_prose_chars(body)
|
|
80
|
-
if substantive_chars < MINIMUM_SUBSTANTIVE_PROSE_CHARS:
|
|
81
|
-
violations.append(
|
|
82
|
-
"PR body lacks substantive prose -- include a Why paragraph or "
|
|
83
|
-
"substantive explanation, not only headers and bullets"
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
body_shape = _compute_pr_body_shape(body)
|
|
87
|
-
|
|
88
|
-
if body_shape == HEAVY_SHAPE:
|
|
89
|
-
if not _body_contains_any_header(body, ALL_HEAVY_OPENING_HEADERS):
|
|
90
|
-
violations.append(
|
|
91
|
-
f"Heavy PR body missing required opening header -- add one of "
|
|
92
|
-
f"{sorted(ALL_HEAVY_OPENING_HEADERS)}"
|
|
93
|
-
)
|
|
94
|
-
if not _body_contains_any_header(body, ALL_HEAVY_TESTING_HEADERS):
|
|
95
|
-
violations.append(
|
|
96
|
-
f"Heavy PR body missing required testing-category header -- add one of "
|
|
97
|
-
f"{sorted(ALL_HEAVY_TESTING_HEADERS)}"
|
|
98
|
-
)
|
|
99
|
-
|
|
100
|
-
body_has_any_header = len(_iter_section_headers(body)) > 0
|
|
101
|
-
body_is_trivial_sized = substantive_chars < TRIVIAL_BODY_CHAR_THRESHOLD
|
|
102
|
-
if body_has_any_header and body_is_trivial_sized:
|
|
103
|
-
violations.append(
|
|
104
|
-
"Trivial PR body contains a ceremony header -- drop every header "
|
|
105
|
-
"and write the one-sentence body directly"
|
|
106
|
-
)
|
|
107
|
-
|
|
108
|
-
if pr_number is not None and _matches_self_closing_reference(body, pr_number):
|
|
109
|
-
violations.append(
|
|
110
|
-
f"{SELF_CLOSING_REFERENCE_MESSAGE_PREFIX}{pr_number}{SELF_CLOSING_REFERENCE_MESSAGE_SUFFIX}"
|
|
111
|
-
)
|
|
112
|
-
|
|
113
|
-
if _opens_with_this_pr_phrase(body):
|
|
114
|
-
violations.append(
|
|
115
|
-
"PR body opens with 'This PR ...' -- open with an imperative verb "
|
|
116
|
-
"(Adds, Fixes, Updates, Removes, Tightens, Ports)"
|
|
117
|
-
)
|
|
118
|
-
|
|
119
|
-
vague_scan_text = _extract_vague_scan_text(body)
|
|
120
|
-
vague_matches = VAGUE_LANGUAGE_PATTERN.findall(vague_scan_text)
|
|
121
|
-
if vague_matches:
|
|
122
|
-
violations.append(
|
|
123
|
-
f"Vague language detected: {', '.join(vague_matches)} -- "
|
|
124
|
-
"be specific about what changed and why"
|
|
125
|
-
)
|
|
126
|
-
|
|
127
|
-
if _is_readability_enabled():
|
|
128
|
-
thresholds = _load_readability_thresholds()
|
|
129
|
-
target_text = _extract_readability_target_text(body)
|
|
130
|
-
metric_violations = _evaluate_readability_metrics(target_text, thresholds)
|
|
131
|
-
if metric_violations:
|
|
132
|
-
post_increment_count = _increment_strike_count()
|
|
133
|
-
if post_increment_count >= READABILITY_STRIKE_THRESHOLD:
|
|
134
|
-
violations.append(_build_readability_escape_hatch_message())
|
|
135
|
-
else:
|
|
136
|
-
violations.extend(metric_violations)
|
|
137
|
-
|
|
138
|
-
return violations
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def _emit_denial(denial_reason: str) -> None:
|
|
142
|
-
"""Print the PreToolUse deny payload for a violation and log the block.
|
|
143
|
-
|
|
144
|
-
Args:
|
|
145
|
-
denial_reason: The permissionDecisionReason text for the deny payload.
|
|
146
|
-
"""
|
|
147
|
-
denial_payload = {
|
|
148
|
-
"hookSpecificOutput": {
|
|
149
|
-
"hookEventName": "PreToolUse",
|
|
150
|
-
"permissionDecision": "deny",
|
|
151
|
-
"permissionDecisionReason": denial_reason,
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
log_hook_block(
|
|
155
|
-
calling_hook_name="pr_description_enforcer.py",
|
|
156
|
-
hook_event="PreToolUse",
|
|
157
|
-
block_reason=denial_reason,
|
|
158
|
-
)
|
|
159
|
-
print(json.dumps(denial_payload))
|
|
160
|
-
sys.stdout.flush()
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
def main() -> None:
|
|
164
|
-
for each_argv_token in sys.argv[1:]:
|
|
165
|
-
if each_argv_token in ALL_READABILITY_CLI_FLAG_TOKENS:
|
|
166
|
-
_dispatch_cli_flag(
|
|
167
|
-
each_argv_token,
|
|
168
|
-
output_stream=sys.stdout,
|
|
169
|
-
error_stream=sys.stderr,
|
|
170
|
-
)
|
|
171
|
-
return
|
|
172
|
-
|
|
173
|
-
try:
|
|
174
|
-
input_data = json.load(sys.stdin)
|
|
175
|
-
except json.JSONDecodeError:
|
|
176
|
-
sys.exit(0)
|
|
177
|
-
|
|
178
|
-
tool_name = input_data.get("tool_name", "")
|
|
179
|
-
if tool_name != "Bash":
|
|
180
|
-
sys.exit(0)
|
|
181
|
-
|
|
182
|
-
command = input_data.get("tool_input", {}).get("command", "")
|
|
183
|
-
if not command:
|
|
184
|
-
sys.exit(0)
|
|
185
|
-
|
|
186
|
-
if is_pr_ready_command(command):
|
|
187
|
-
ready_denial_reason = evaluate_pr_ready_gate(command)
|
|
188
|
-
if ready_denial_reason is not None:
|
|
189
|
-
_emit_denial(ready_denial_reason)
|
|
190
|
-
sys.exit(0)
|
|
191
|
-
|
|
192
|
-
has_any_body_flag = _command_carries_body_flag(command)
|
|
193
|
-
is_pr_create = "gh pr create" in command and has_any_body_flag
|
|
194
|
-
is_pr_edit = "gh pr edit" in command and has_any_body_flag
|
|
195
|
-
is_pr_comment = "gh pr comment" in command and has_any_body_flag
|
|
196
|
-
|
|
197
|
-
if not (is_pr_create or is_pr_edit or is_pr_comment):
|
|
198
|
-
sys.exit(0)
|
|
199
|
-
|
|
200
|
-
body = extract_body_from_command(command)
|
|
201
|
-
|
|
202
|
-
if body is None:
|
|
203
|
-
sys.exit(0)
|
|
204
|
-
|
|
205
|
-
extracted_pr_number = None
|
|
206
|
-
if is_pr_edit or is_pr_comment:
|
|
207
|
-
extracted_pr_number = _extract_pr_number_from_command(command)
|
|
208
|
-
|
|
209
|
-
violations = validate_pr_body(body, pr_number=extracted_pr_number)
|
|
210
|
-
|
|
211
|
-
if is_pr_comment and is_proof_shaped_body(body):
|
|
212
|
-
violations.extend(audit_proof_comment_body(body, extracted_pr_number))
|
|
213
|
-
|
|
214
|
-
if violations:
|
|
215
|
-
violation_list = "; ".join(violations)
|
|
216
|
-
pr_guide_reference = f" @{PR_GUIDE_PATH}" if os.path.exists(PR_GUIDE_PATH) else ""
|
|
217
|
-
_emit_denial(
|
|
218
|
-
f"BLOCKED: [PR_DESCRIPTION] {violation_list}. "
|
|
219
|
-
f"Rewrite the body yourself in Anthropic claude-code style. "
|
|
220
|
-
f"Guide:{pr_guide_reference}"
|
|
221
|
-
)
|
|
222
|
-
|
|
223
|
-
sys.exit(0)
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
if __name__ == "__main__":
|
|
227
|
-
main()
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
"""Detect body flags and recover the positional PR number from a gh command.
|
|
2
|
-
|
|
3
|
-
Reports whether a captured shell command carries any body or body-file flag,
|
|
4
|
-
and extracts the positional PR number (bare integer or GitHub PR URL) from a
|
|
5
|
-
gh pr edit/comment/ready command while skipping value-taking flags and their
|
|
6
|
-
values.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import re
|
|
10
|
-
import shlex
|
|
11
|
-
import sys
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
|
|
14
|
-
_hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
15
|
-
if _hooks_dir not in sys.path:
|
|
16
|
-
sys.path.insert(0, _hooks_dir)
|
|
17
|
-
|
|
18
|
-
from blocking._gh_body_arg_utils import ( # noqa: E402
|
|
19
|
-
all_body_flags,
|
|
20
|
-
body_file_flag,
|
|
21
|
-
body_file_short_flag,
|
|
22
|
-
count_extra_tokens_to_skip_for_split_quoted_value,
|
|
23
|
-
get_logical_first_line,
|
|
24
|
-
is_flag_shaped_token,
|
|
25
|
-
is_unresolvable_shell_value,
|
|
26
|
-
match_body_file_equals_prefix,
|
|
27
|
-
match_body_flag_equals_prefix,
|
|
28
|
-
match_non_body_value_flag_equals_prefix,
|
|
29
|
-
non_body_value_flags,
|
|
30
|
-
strip_surrounding_quotes,
|
|
31
|
-
)
|
|
32
|
-
from hooks_constants.pr_description_enforcer_constants import ( # noqa: E402
|
|
33
|
-
ALL_PR_NUMBER_BEARING_GH_PR_SUBCOMMANDS,
|
|
34
|
-
GH_PR_COMMAND_MIN_TOKEN_COUNT,
|
|
35
|
-
)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _resolve_positional_pr_number(token: str) -> int | None:
|
|
39
|
-
"""Return the PR number named by a positional token, or None if it is not one.
|
|
40
|
-
|
|
41
|
-
Accepts either a bare integer literal or a GitHub PR URL whose final path
|
|
42
|
-
segment is ``/pull/<number>``. The token may carry surrounding quotes;
|
|
43
|
-
unresolvable shell variables are rejected.
|
|
44
|
-
"""
|
|
45
|
-
stripped_candidate = strip_surrounding_quotes(token)
|
|
46
|
-
if is_unresolvable_shell_value(stripped_candidate):
|
|
47
|
-
return None
|
|
48
|
-
url_match = re.match(
|
|
49
|
-
r"^https?://[^/]+/[^/]+/[^/]+/pull/(\d+)(?:[/?#].*)?$",
|
|
50
|
-
stripped_candidate,
|
|
51
|
-
)
|
|
52
|
-
if url_match is not None:
|
|
53
|
-
try:
|
|
54
|
-
return int(url_match.group(1))
|
|
55
|
-
except ValueError:
|
|
56
|
-
return None
|
|
57
|
-
try:
|
|
58
|
-
return int(stripped_candidate)
|
|
59
|
-
except ValueError:
|
|
60
|
-
return None
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def _extract_pr_number_from_command(command: str) -> int | None:
|
|
64
|
-
"""Return the PR number positional argument from a `gh pr edit|comment|ready` command.
|
|
65
|
-
|
|
66
|
-
Skips value-taking non-body flags (and their value tokens) so that ``--repo owner/r``
|
|
67
|
-
pairs do not consume the trailing PR number. Accepts both a bare integer literal
|
|
68
|
-
and a GitHub PR URL (``https://github.com/o/r/pull/<n>``) in the positional slot.
|
|
69
|
-
|
|
70
|
-
Args:
|
|
71
|
-
command: The raw shell command captured by the hook.
|
|
72
|
-
|
|
73
|
-
Returns:
|
|
74
|
-
The PR number when one positional value (integer or URL) is present, else None.
|
|
75
|
-
"""
|
|
76
|
-
logical_line = get_logical_first_line(command)
|
|
77
|
-
if not logical_line:
|
|
78
|
-
return None
|
|
79
|
-
try:
|
|
80
|
-
all_tokens = shlex.split(logical_line, posix=False)
|
|
81
|
-
except ValueError:
|
|
82
|
-
return None
|
|
83
|
-
if len(all_tokens) < GH_PR_COMMAND_MIN_TOKEN_COUNT:
|
|
84
|
-
return None
|
|
85
|
-
if all_tokens[0] != "gh" or all_tokens[1] != "pr":
|
|
86
|
-
return None
|
|
87
|
-
subcommand_token = all_tokens[2]
|
|
88
|
-
if subcommand_token not in ALL_PR_NUMBER_BEARING_GH_PR_SUBCOMMANDS:
|
|
89
|
-
return None
|
|
90
|
-
all_value_taking_bare_flags: frozenset[str] = (
|
|
91
|
-
non_body_value_flags | all_body_flags | {body_file_flag, body_file_short_flag}
|
|
92
|
-
)
|
|
93
|
-
token_index = GH_PR_COMMAND_MIN_TOKEN_COUNT
|
|
94
|
-
while token_index < len(all_tokens):
|
|
95
|
-
current_token = all_tokens[token_index]
|
|
96
|
-
matched_equals_prefix = (
|
|
97
|
-
match_non_body_value_flag_equals_prefix(current_token)
|
|
98
|
-
or match_body_flag_equals_prefix(current_token)
|
|
99
|
-
or match_body_file_equals_prefix(current_token)
|
|
100
|
-
)
|
|
101
|
-
if matched_equals_prefix is not None:
|
|
102
|
-
first_value_token = current_token[len(matched_equals_prefix) :]
|
|
103
|
-
remaining_raw_tokens = all_tokens[token_index + 1 :]
|
|
104
|
-
extra_skip = (
|
|
105
|
-
count_extra_tokens_to_skip_for_split_quoted_value(
|
|
106
|
-
remaining_raw_tokens, first_value_token
|
|
107
|
-
)
|
|
108
|
-
or 0
|
|
109
|
-
)
|
|
110
|
-
token_index += 1 + extra_skip
|
|
111
|
-
continue
|
|
112
|
-
if current_token in all_value_taking_bare_flags:
|
|
113
|
-
token_index += 1
|
|
114
|
-
if token_index < len(all_tokens):
|
|
115
|
-
token_index += 1
|
|
116
|
-
continue
|
|
117
|
-
if is_flag_shaped_token(current_token):
|
|
118
|
-
token_index += 1
|
|
119
|
-
continue
|
|
120
|
-
resolved_pr_number = _resolve_positional_pr_number(current_token)
|
|
121
|
-
if resolved_pr_number is not None:
|
|
122
|
-
return resolved_pr_number
|
|
123
|
-
return None
|
|
124
|
-
return None
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def _command_carries_body_flag(command: str) -> bool:
|
|
128
|
-
"""Return True when the command string carries any body or body-file flag.
|
|
129
|
-
|
|
130
|
-
Detects the body/body-file forms accepted by ``gh pr {create,edit,comment}``:
|
|
131
|
-
|
|
132
|
-
- Long flags: a single ``"--body" in command`` substring check catches
|
|
133
|
-
every long form — ``--body``, ``--body=<value>``, ``--body-file``, and
|
|
134
|
-
``--body-file=<value>`` — because ``--body`` is a prefix of
|
|
135
|
-
``--body-file``. No separate ``--body-file`` check is needed.
|
|
136
|
-
- Short flags, space-separated: ``-b <value>``, ``-F <value>`` — matched
|
|
137
|
-
as `` -b `` and `` -F `` so the literal substring cannot collide with a
|
|
138
|
-
surrounding token (e.g. ``-base``, ``-Foo``).
|
|
139
|
-
- Short flags, equal-attached: ``-b=<value>``, ``-F=<value>`` — matched
|
|
140
|
-
as `` -b=`` and `` -F=`` for the same anti-collision reason. The test
|
|
141
|
-
suite relies on this detection path.
|
|
142
|
-
|
|
143
|
-
Args:
|
|
144
|
-
command: The raw shell command captured by the hook.
|
|
145
|
-
|
|
146
|
-
Returns:
|
|
147
|
-
True if any documented body or body-file flag appears in the command.
|
|
148
|
-
"""
|
|
149
|
-
return (
|
|
150
|
-
"--body" in command
|
|
151
|
-
or " -b " in command
|
|
152
|
-
or " -b=" in command
|
|
153
|
-
or " -F " in command
|
|
154
|
-
or " -F=" in command
|
|
155
|
-
)
|