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,364 +0,0 @@
|
|
|
1
|
-
"""Score PR body readability and manage its persisted strike/threshold state.
|
|
2
|
-
|
|
3
|
-
Computes Flesch Reading Ease and sentence-length metrics over the intro and
|
|
4
|
-
first section of a PR body, escalates repeated readability failures through a
|
|
5
|
-
persisted strike counter, applies the loosen/reset/enable/disable threshold
|
|
6
|
-
overrides, and dispatches the readability-management CLI flags.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import json
|
|
10
|
-
import math
|
|
11
|
-
import os
|
|
12
|
-
import re
|
|
13
|
-
import sys
|
|
14
|
-
from pathlib import Path
|
|
15
|
-
from typing import TextIO
|
|
16
|
-
|
|
17
|
-
_hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
18
|
-
if _hooks_dir not in sys.path:
|
|
19
|
-
sys.path.insert(0, _hooks_dir)
|
|
20
|
-
|
|
21
|
-
from blocking.pr_description_body_audit import strip_markdown_ceremony # noqa: E402
|
|
22
|
-
from hooks_constants.pr_description_enforcer_constants import ( # noqa: E402
|
|
23
|
-
ATOMIC_WRITE_TEMP_SUFFIX,
|
|
24
|
-
DEFAULT_READABILITY_THRESHOLDS,
|
|
25
|
-
FENCED_CODE_BLOCK_PATTERN,
|
|
26
|
-
FLESCH_BASE_SCORE,
|
|
27
|
-
FLESCH_PERFECT_SCORE,
|
|
28
|
-
FLESCH_SYLLABLES_PER_WORD_COEFFICIENT,
|
|
29
|
-
FLESCH_WORDS_PER_SENTENCE_COEFFICIENT,
|
|
30
|
-
HEADING_LINE_PATTERN,
|
|
31
|
-
READABILITY_AVG_SENTENCE_WORDS_CEILING,
|
|
32
|
-
READABILITY_ENABLED_STATE_FILE,
|
|
33
|
-
READABILITY_FLESCH_LOOSEN_FACTOR,
|
|
34
|
-
READABILITY_LOOSEN_CAP,
|
|
35
|
-
READABILITY_MAX_SENTENCE_WORDS_CEILING,
|
|
36
|
-
READABILITY_MIN_FLESCH_FLOOR,
|
|
37
|
-
READABILITY_SENTENCE_WORDS_LOOSEN_FACTOR,
|
|
38
|
-
READABILITY_STATE_FILE,
|
|
39
|
-
READABILITY_THRESHOLD_OVERRIDE_FILE,
|
|
40
|
-
ReadabilityThresholds,
|
|
41
|
-
)
|
|
42
|
-
from hooks_constants.setup_project_paths_constants import UTF8_ENCODING # noqa: E402
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
def _atomic_write_json(target_path: Path, all_payload_fields: dict[str, object]) -> None:
|
|
46
|
-
target_path.parent.mkdir(parents=True, exist_ok=True)
|
|
47
|
-
temporary_path = target_path.with_suffix(target_path.suffix + ATOMIC_WRITE_TEMP_SUFFIX)
|
|
48
|
-
with open(temporary_path, "w", encoding=UTF8_ENCODING) as write_handle:
|
|
49
|
-
json.dump(all_payload_fields, write_handle)
|
|
50
|
-
os.replace(temporary_path, target_path)
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def _read_json_or_default(
|
|
54
|
-
target_path: Path, all_default_payload_fields: dict[str, object]
|
|
55
|
-
) -> dict[str, object]:
|
|
56
|
-
if not target_path.exists():
|
|
57
|
-
return dict(all_default_payload_fields)
|
|
58
|
-
try:
|
|
59
|
-
with open(target_path, "r", encoding=UTF8_ENCODING) as read_handle:
|
|
60
|
-
loaded_payload = json.load(read_handle)
|
|
61
|
-
except (FileNotFoundError, PermissionError, OSError, json.JSONDecodeError):
|
|
62
|
-
return dict(all_default_payload_fields)
|
|
63
|
-
if not isinstance(loaded_payload, dict):
|
|
64
|
-
return dict(all_default_payload_fields)
|
|
65
|
-
return loaded_payload
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def _read_strike_count() -> int:
|
|
69
|
-
payload = _read_json_or_default(READABILITY_STATE_FILE, {"strikes": 0})
|
|
70
|
-
raw_count = payload.get("strikes", 0)
|
|
71
|
-
if isinstance(raw_count, int) and not isinstance(raw_count, bool):
|
|
72
|
-
return max(raw_count, 0)
|
|
73
|
-
return 0
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
def _increment_strike_count() -> int:
|
|
77
|
-
payload = _read_json_or_default(READABILITY_STATE_FILE, {"strikes": 0})
|
|
78
|
-
raw_count = payload.get("strikes", 0)
|
|
79
|
-
if isinstance(raw_count, int) and not isinstance(raw_count, bool):
|
|
80
|
-
starting_count = max(raw_count, 0)
|
|
81
|
-
else:
|
|
82
|
-
starting_count = 0
|
|
83
|
-
new_count = starting_count + 1
|
|
84
|
-
_atomic_write_json(READABILITY_STATE_FILE, {"strikes": new_count})
|
|
85
|
-
return new_count
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def _reset_strike_count() -> None:
|
|
89
|
-
_atomic_write_json(READABILITY_STATE_FILE, {"strikes": 0})
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
def _resolve_integer_field(
|
|
93
|
-
all_payload_fields: dict[str, object], field_name: str, default_value: int
|
|
94
|
-
) -> int:
|
|
95
|
-
raw_value = all_payload_fields.get(field_name, default_value)
|
|
96
|
-
if isinstance(raw_value, int) and not isinstance(raw_value, bool):
|
|
97
|
-
return raw_value
|
|
98
|
-
return default_value
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
def _load_readability_thresholds() -> ReadabilityThresholds:
|
|
102
|
-
payload = _read_json_or_default(READABILITY_THRESHOLD_OVERRIDE_FILE, {})
|
|
103
|
-
return ReadabilityThresholds(
|
|
104
|
-
flesch_min=_resolve_integer_field(
|
|
105
|
-
payload, "flesch_min", DEFAULT_READABILITY_THRESHOLDS.flesch_min
|
|
106
|
-
),
|
|
107
|
-
max_sentence_words=_resolve_integer_field(
|
|
108
|
-
payload, "max_sentence_words", DEFAULT_READABILITY_THRESHOLDS.max_sentence_words
|
|
109
|
-
),
|
|
110
|
-
avg_sentence_words=_resolve_integer_field(
|
|
111
|
-
payload, "avg_sentence_words", DEFAULT_READABILITY_THRESHOLDS.avg_sentence_words
|
|
112
|
-
),
|
|
113
|
-
)
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def _read_loosens_used() -> int:
|
|
117
|
-
payload = _read_json_or_default(READABILITY_THRESHOLD_OVERRIDE_FILE, {})
|
|
118
|
-
raw_count = payload.get("loosens_used", 0)
|
|
119
|
-
if isinstance(raw_count, int) and not isinstance(raw_count, bool):
|
|
120
|
-
return max(raw_count, 0)
|
|
121
|
-
return 0
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
def _is_readability_enabled() -> bool:
|
|
125
|
-
payload = _read_json_or_default(READABILITY_ENABLED_STATE_FILE, {"enabled": True})
|
|
126
|
-
enabled_value = payload.get("enabled", True)
|
|
127
|
-
if isinstance(enabled_value, bool):
|
|
128
|
-
return enabled_value
|
|
129
|
-
return True
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def _set_readability_enabled(enabled: bool) -> None:
|
|
133
|
-
_atomic_write_json(READABILITY_ENABLED_STATE_FILE, {"enabled": enabled})
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def _count_syllables_in_word(word: str) -> int:
|
|
137
|
-
all_vowel_characters: frozenset[str] = frozenset("aeiouy")
|
|
138
|
-
cleaned_word = "".join(
|
|
139
|
-
each_character for each_character in word.lower() if each_character.isalpha()
|
|
140
|
-
)
|
|
141
|
-
if not cleaned_word:
|
|
142
|
-
return 0
|
|
143
|
-
syllable_count = 0
|
|
144
|
-
is_previous_character_vowel = False
|
|
145
|
-
for each_character in cleaned_word:
|
|
146
|
-
is_vowel = each_character in all_vowel_characters
|
|
147
|
-
if is_vowel and not is_previous_character_vowel:
|
|
148
|
-
syllable_count += 1
|
|
149
|
-
is_previous_character_vowel = is_vowel
|
|
150
|
-
if cleaned_word.endswith("e") and syllable_count > 1:
|
|
151
|
-
syllable_count -= 1
|
|
152
|
-
return max(syllable_count, 1)
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def _split_sentences(text: str) -> list[str]:
|
|
156
|
-
sentence_split_pattern = re.compile(r"[.!?]+\s+")
|
|
157
|
-
cleaned_text = text.strip()
|
|
158
|
-
if not cleaned_text:
|
|
159
|
-
return []
|
|
160
|
-
raw_pieces = sentence_split_pattern.split(cleaned_text)
|
|
161
|
-
all_sentences = [each_piece.strip() for each_piece in raw_pieces if each_piece.strip()]
|
|
162
|
-
return all_sentences
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
def _compute_flesch_reading_ease(text: str) -> float:
|
|
166
|
-
all_sentences = _split_sentences(text)
|
|
167
|
-
if not all_sentences:
|
|
168
|
-
return FLESCH_PERFECT_SCORE
|
|
169
|
-
all_words: list[str] = []
|
|
170
|
-
total_syllables = 0
|
|
171
|
-
for each_sentence in all_sentences:
|
|
172
|
-
sentence_words = [
|
|
173
|
-
each_token for each_token in re.split(r"\s+", each_sentence) if each_token
|
|
174
|
-
]
|
|
175
|
-
all_words.extend(sentence_words)
|
|
176
|
-
for each_word in sentence_words:
|
|
177
|
-
total_syllables += _count_syllables_in_word(each_word)
|
|
178
|
-
total_words = len(all_words)
|
|
179
|
-
if total_words == 0:
|
|
180
|
-
return FLESCH_PERFECT_SCORE
|
|
181
|
-
total_sentences = len(all_sentences)
|
|
182
|
-
return (
|
|
183
|
-
FLESCH_BASE_SCORE
|
|
184
|
-
- FLESCH_WORDS_PER_SENTENCE_COEFFICIENT * (total_words / total_sentences)
|
|
185
|
-
- FLESCH_SYLLABLES_PER_WORD_COEFFICIENT * (total_syllables / total_words)
|
|
186
|
-
)
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
def _extract_readability_target_text(body: str) -> str:
|
|
190
|
-
"""Return the ceremony-stripped prose window scored for readability.
|
|
191
|
-
|
|
192
|
-
Strips fenced code blocks, then builds a window from the body's intro
|
|
193
|
-
paragraph plus its first section's prose. The intro paragraph ends at the
|
|
194
|
-
earliest boundary among the first blank line and the first ATX header; when
|
|
195
|
-
neither boundary exists the whole body is the intro. The first section runs
|
|
196
|
-
from just after that first header to the next header (or end of body). The
|
|
197
|
-
intro and first section are joined with a blank line and returned with
|
|
198
|
-
Markdown ceremony stripped.
|
|
199
|
-
|
|
200
|
-
Args:
|
|
201
|
-
body: The raw PR body markdown text.
|
|
202
|
-
|
|
203
|
-
Returns:
|
|
204
|
-
The ceremony-stripped intro-paragraph plus first-section prose window
|
|
205
|
-
used for readability scoring.
|
|
206
|
-
"""
|
|
207
|
-
intro_paragraph = ""
|
|
208
|
-
body_without_fences = FENCED_CODE_BLOCK_PATTERN.sub("", body)
|
|
209
|
-
body_after_strip = body_without_fences.lstrip()
|
|
210
|
-
blank_line_position = body_after_strip.find("\n\n")
|
|
211
|
-
header_position_match = HEADING_LINE_PATTERN.search(body_after_strip)
|
|
212
|
-
header_position = header_position_match.start() if header_position_match else -1
|
|
213
|
-
|
|
214
|
-
if blank_line_position == -1 and header_position == -1:
|
|
215
|
-
intro_paragraph = body_after_strip
|
|
216
|
-
elif blank_line_position == -1:
|
|
217
|
-
intro_paragraph = body_after_strip[:header_position]
|
|
218
|
-
elif header_position == -1:
|
|
219
|
-
intro_paragraph = body_after_strip[:blank_line_position]
|
|
220
|
-
else:
|
|
221
|
-
first_boundary = min(blank_line_position, header_position)
|
|
222
|
-
intro_paragraph = body_after_strip[:first_boundary]
|
|
223
|
-
|
|
224
|
-
first_body_section = ""
|
|
225
|
-
if header_position_match is not None:
|
|
226
|
-
section_start = header_position_match.end()
|
|
227
|
-
remainder = body_after_strip[section_start:]
|
|
228
|
-
next_header_match = HEADING_LINE_PATTERN.search(remainder)
|
|
229
|
-
if next_header_match is not None:
|
|
230
|
-
first_body_section = remainder[: next_header_match.start()]
|
|
231
|
-
else:
|
|
232
|
-
first_body_section = remainder
|
|
233
|
-
|
|
234
|
-
combined_text = f"{intro_paragraph}\n\n{first_body_section}"
|
|
235
|
-
return strip_markdown_ceremony(combined_text)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
def _evaluate_readability_metrics(
|
|
239
|
-
target_text: str,
|
|
240
|
-
thresholds: ReadabilityThresholds,
|
|
241
|
-
) -> list[str]:
|
|
242
|
-
all_metric_violations: list[str] = []
|
|
243
|
-
all_sentences = _split_sentences(target_text)
|
|
244
|
-
if not all_sentences:
|
|
245
|
-
return all_metric_violations
|
|
246
|
-
word_counts_per_sentence: list[int] = []
|
|
247
|
-
for each_sentence in all_sentences:
|
|
248
|
-
sentence_words = [
|
|
249
|
-
each_token for each_token in re.split(r"\s+", each_sentence) if each_token
|
|
250
|
-
]
|
|
251
|
-
word_counts_per_sentence.append(len(sentence_words))
|
|
252
|
-
max_sentence_words = max(word_counts_per_sentence) if word_counts_per_sentence else 0
|
|
253
|
-
average_sentence_words = (
|
|
254
|
-
sum(word_counts_per_sentence) / len(word_counts_per_sentence)
|
|
255
|
-
if word_counts_per_sentence
|
|
256
|
-
else 0.0
|
|
257
|
-
)
|
|
258
|
-
if max_sentence_words > thresholds.max_sentence_words:
|
|
259
|
-
all_metric_violations.append(
|
|
260
|
-
f"Readability: longest sentence is {max_sentence_words} words "
|
|
261
|
-
f"(maximum {thresholds.max_sentence_words}); "
|
|
262
|
-
"split or rewrite the longest sentence"
|
|
263
|
-
)
|
|
264
|
-
if average_sentence_words > thresholds.avg_sentence_words:
|
|
265
|
-
all_metric_violations.append(
|
|
266
|
-
f"Readability: average sentence is {average_sentence_words:.1f} words "
|
|
267
|
-
f"(maximum {thresholds.avg_sentence_words}); "
|
|
268
|
-
"shorten or split your longest sentences"
|
|
269
|
-
)
|
|
270
|
-
flesch_score = _compute_flesch_reading_ease(target_text)
|
|
271
|
-
if flesch_score < thresholds.flesch_min:
|
|
272
|
-
all_metric_violations.append(
|
|
273
|
-
f"Readability: Flesch Reading Ease is {flesch_score:.1f} "
|
|
274
|
-
f"(minimum {thresholds.flesch_min}); use shorter words and sentences"
|
|
275
|
-
)
|
|
276
|
-
return all_metric_violations
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
def _build_readability_escape_hatch_message() -> str:
|
|
280
|
-
return (
|
|
281
|
-
"Readability strike threshold reached. Pick one: "
|
|
282
|
-
"(1) python <enforcer-path> --readability-loosen to widen thresholds 10%, "
|
|
283
|
-
"(2) python <enforcer-path> --readability-disable to skip the readability check, "
|
|
284
|
-
"(3) python <enforcer-path> --readability-reset to zero the strike counter, "
|
|
285
|
-
"(4) reply with the body plus the intended message to report a false positive."
|
|
286
|
-
)
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
def _apply_readability_loosen() -> str:
|
|
290
|
-
current_thresholds = _load_readability_thresholds()
|
|
291
|
-
loosens_used = _read_loosens_used()
|
|
292
|
-
|
|
293
|
-
if loosens_used >= READABILITY_LOOSEN_CAP:
|
|
294
|
-
return "cap_reached"
|
|
295
|
-
|
|
296
|
-
if current_thresholds.flesch_min <= READABILITY_MIN_FLESCH_FLOOR:
|
|
297
|
-
return "floor_reached"
|
|
298
|
-
|
|
299
|
-
if current_thresholds.max_sentence_words >= READABILITY_MAX_SENTENCE_WORDS_CEILING:
|
|
300
|
-
return "ceiling_reached"
|
|
301
|
-
|
|
302
|
-
if current_thresholds.avg_sentence_words >= READABILITY_AVG_SENTENCE_WORDS_CEILING:
|
|
303
|
-
return "ceiling_reached"
|
|
304
|
-
|
|
305
|
-
next_flesch = max(
|
|
306
|
-
READABILITY_MIN_FLESCH_FLOOR,
|
|
307
|
-
math.floor(current_thresholds.flesch_min * READABILITY_FLESCH_LOOSEN_FACTOR),
|
|
308
|
-
)
|
|
309
|
-
next_max_sentence = min(
|
|
310
|
-
READABILITY_MAX_SENTENCE_WORDS_CEILING,
|
|
311
|
-
math.ceil(current_thresholds.max_sentence_words * READABILITY_SENTENCE_WORDS_LOOSEN_FACTOR),
|
|
312
|
-
)
|
|
313
|
-
next_avg_sentence = min(
|
|
314
|
-
READABILITY_AVG_SENTENCE_WORDS_CEILING,
|
|
315
|
-
math.ceil(current_thresholds.avg_sentence_words * READABILITY_SENTENCE_WORDS_LOOSEN_FACTOR),
|
|
316
|
-
)
|
|
317
|
-
|
|
318
|
-
next_payload: dict[str, object] = {
|
|
319
|
-
"flesch_min": next_flesch,
|
|
320
|
-
"max_sentence_words": next_max_sentence,
|
|
321
|
-
"avg_sentence_words": next_avg_sentence,
|
|
322
|
-
"loosens_used": loosens_used + 1,
|
|
323
|
-
}
|
|
324
|
-
_atomic_write_json(READABILITY_THRESHOLD_OVERRIDE_FILE, next_payload)
|
|
325
|
-
return "ok"
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
def _apply_readability_reset() -> None:
|
|
329
|
-
_reset_strike_count()
|
|
330
|
-
_atomic_write_json(READABILITY_THRESHOLD_OVERRIDE_FILE, {"loosens_used": 0})
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
def _dispatch_cli_flag(
|
|
334
|
-
flag_token: str,
|
|
335
|
-
output_stream: TextIO,
|
|
336
|
-
error_stream: TextIO,
|
|
337
|
-
) -> None:
|
|
338
|
-
"""Handle a single readability-management CLI flag and exit the process."""
|
|
339
|
-
if flag_token == "--readability-loosen":
|
|
340
|
-
outcome = _apply_readability_loosen()
|
|
341
|
-
if outcome == "cap_reached":
|
|
342
|
-
error_stream.write(
|
|
343
|
-
"loosen cap reached; use --readability-disable or --readability-reset\n"
|
|
344
|
-
)
|
|
345
|
-
sys.exit(1)
|
|
346
|
-
if outcome in {"floor_reached", "ceiling_reached"}:
|
|
347
|
-
error_stream.write(
|
|
348
|
-
"thresholds already at floor/ceiling; use --readability-disable or --readability-reset\n"
|
|
349
|
-
)
|
|
350
|
-
sys.exit(1)
|
|
351
|
-
output_stream.write("readability thresholds loosened 10%\n")
|
|
352
|
-
sys.exit(0)
|
|
353
|
-
if flag_token == "--readability-reset":
|
|
354
|
-
_apply_readability_reset()
|
|
355
|
-
output_stream.write("readability strike counter and override thresholds reset\n")
|
|
356
|
-
sys.exit(0)
|
|
357
|
-
if flag_token == "--readability-disable":
|
|
358
|
-
_set_readability_enabled(False)
|
|
359
|
-
output_stream.write("readability check disabled\n")
|
|
360
|
-
sys.exit(0)
|
|
361
|
-
if flag_token == "--readability-enable":
|
|
362
|
-
_set_readability_enabled(True)
|
|
363
|
-
output_stream.write("readability check enabled\n")
|
|
364
|
-
sys.exit(0)
|
|
@@ -1,182 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""PreToolUse hook: block a reviewer-spawn trigger when that reviewer is down.
|
|
3
|
-
|
|
4
|
-
autoconverge already skips a reviewer step when its own pre-check reports
|
|
5
|
-
that reviewer unavailable. That skip only protects the trigger commands the
|
|
6
|
-
workflow generates and inspects itself. This hook is the backstop: it reads
|
|
7
|
-
the Bash command a tool call is about to run and blocks it when the matched
|
|
8
|
-
reviewer reports unavailable.
|
|
9
|
-
|
|
10
|
-
The gate only inspects a command carrying the literal sentinel marker
|
|
11
|
-
`CLAUDE_REVIEWER_GATE=autoconverge`. autoconverge prepends this marker to its
|
|
12
|
-
own trigger commands. A command without the marker is never inspected, so a
|
|
13
|
-
manual or unrelated `gh` call always passes through untouched.
|
|
14
|
-
|
|
15
|
-
A Copilot trigger is a command containing both `requested_reviewers` and
|
|
16
|
-
`copilot-pull-request-reviewer[bot]`. A Bugbot trigger is a command
|
|
17
|
-
containing `post_fix_reply.py` and a `--body "bugbot run"` argument.
|
|
18
|
-
|
|
19
|
-
For the matched reviewer, the gate runs
|
|
20
|
-
`reviewer_availability.py --reviewer copilot|bugbot` and denies the call only
|
|
21
|
-
when that script exits with the documented reviewer-down code (3). Any other
|
|
22
|
-
exit code is an unexpected script failure, not a down report, so the call is
|
|
23
|
-
allowed (fail open). The deny reason names the reviewer and carries
|
|
24
|
-
the script's output. The script path resolves relative to this hook's
|
|
25
|
-
install location by default, or from the
|
|
26
|
-
`REVIEWER_SPAWN_GATE_AVAILABILITY_SCRIPT_PATH` environment variable when set.
|
|
27
|
-
A missing script, a timeout, or a failure to launch it allows the call
|
|
28
|
-
(fail open), so a broken availability check never blocks a legitimate run.
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
import json
|
|
32
|
-
import os
|
|
33
|
-
import subprocess
|
|
34
|
-
import sys
|
|
35
|
-
from pathlib import Path
|
|
36
|
-
|
|
37
|
-
_hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
38
|
-
if _hooks_dir not in sys.path:
|
|
39
|
-
sys.path.insert(0, _hooks_dir)
|
|
40
|
-
|
|
41
|
-
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
42
|
-
from hooks_constants.reviewer_spawn_gate_constants import ( # noqa: E402
|
|
43
|
-
ALL_COPILOT_TRIGGER_MARKERS,
|
|
44
|
-
AVAILABILITY_DOWN_EXIT_CODE,
|
|
45
|
-
AVAILABILITY_REVIEWER_FLAG,
|
|
46
|
-
AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME,
|
|
47
|
-
AVAILABILITY_SCRIPT_RELATIVE_PATH,
|
|
48
|
-
AVAILABILITY_SCRIPT_TIMEOUT_SECONDS,
|
|
49
|
-
BASH_TOOL_NAME,
|
|
50
|
-
BUGBOT_REVIEWER_LABEL,
|
|
51
|
-
BUGBOT_REVIEWER_TOKEN,
|
|
52
|
-
BUGBOT_RUN_BODY_PATTERN,
|
|
53
|
-
BUGBOT_TRIGGER_SCRIPT_MARKER,
|
|
54
|
-
COPILOT_REVIEWER_LABEL,
|
|
55
|
-
COPILOT_REVIEWER_TOKEN,
|
|
56
|
-
DENY_REASON_TEMPLATE,
|
|
57
|
-
GATE_SENTINEL_MARKER,
|
|
58
|
-
)
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
def _matches_copilot_trigger(command: str) -> bool:
|
|
62
|
-
return all(each_marker in command for each_marker in ALL_COPILOT_TRIGGER_MARKERS)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
def _matches_bugbot_trigger(command: str) -> bool:
|
|
66
|
-
return BUGBOT_TRIGGER_SCRIPT_MARKER in command and bool(
|
|
67
|
-
BUGBOT_RUN_BODY_PATTERN.search(command)
|
|
68
|
-
)
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
def _reviewer_trigger(command: str) -> tuple[str, str] | None:
|
|
72
|
-
"""Return the reviewer token and label a scoped command triggers.
|
|
73
|
-
|
|
74
|
-
Args:
|
|
75
|
-
command: The Bash command text carrying the gate's sentinel marker.
|
|
76
|
-
|
|
77
|
-
Returns:
|
|
78
|
-
The matched reviewer's CLI token paired with its display label, or
|
|
79
|
-
None when the command matches neither the Copilot review-request
|
|
80
|
-
shape nor the Bugbot rerun-comment shape.
|
|
81
|
-
"""
|
|
82
|
-
if _matches_copilot_trigger(command):
|
|
83
|
-
return COPILOT_REVIEWER_TOKEN, COPILOT_REVIEWER_LABEL
|
|
84
|
-
if _matches_bugbot_trigger(command):
|
|
85
|
-
return BUGBOT_REVIEWER_TOKEN, BUGBOT_REVIEWER_LABEL
|
|
86
|
-
return None
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def _resolve_availability_script_path() -> Path:
|
|
90
|
-
override_path = os.environ.get(AVAILABILITY_SCRIPT_PATH_ENV_VAR_NAME, "")
|
|
91
|
-
if override_path:
|
|
92
|
-
return Path(override_path)
|
|
93
|
-
plugin_root = Path(__file__).resolve().parents[2]
|
|
94
|
-
return plugin_root / AVAILABILITY_SCRIPT_RELATIVE_PATH
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
def _probe_reviewer_availability(reviewer_token: str) -> tuple[bool, str]:
|
|
98
|
-
"""Run the shared availability script for a reviewer and report the outcome.
|
|
99
|
-
|
|
100
|
-
Args:
|
|
101
|
-
reviewer_token: The reviewer's CLI token (copilot or bugbot) passed to
|
|
102
|
-
the availability script's --reviewer flag.
|
|
103
|
-
|
|
104
|
-
Returns:
|
|
105
|
-
A pair of (is_available, detail_text). is_available is False only
|
|
106
|
-
when the script exits with the documented reviewer-down code;
|
|
107
|
-
detail_text carries the script's combined stdout and stderr for the
|
|
108
|
-
deny message. A missing script, a timeout, an OS-level failure to
|
|
109
|
-
launch the script, or any exit code other than the documented down
|
|
110
|
-
code reports available (fail open), so an availability-check crash
|
|
111
|
-
never blocks a legitimate run.
|
|
112
|
-
"""
|
|
113
|
-
script_path = _resolve_availability_script_path()
|
|
114
|
-
if not script_path.is_file():
|
|
115
|
-
return True, f"availability script not found at {script_path}"
|
|
116
|
-
try:
|
|
117
|
-
completed_process = subprocess.run(
|
|
118
|
-
[sys.executable, str(script_path), AVAILABILITY_REVIEWER_FLAG, reviewer_token],
|
|
119
|
-
capture_output=True,
|
|
120
|
-
text=True,
|
|
121
|
-
encoding="utf-8",
|
|
122
|
-
errors="replace",
|
|
123
|
-
timeout=AVAILABILITY_SCRIPT_TIMEOUT_SECONDS,
|
|
124
|
-
check=False,
|
|
125
|
-
)
|
|
126
|
-
except (OSError, subprocess.TimeoutExpired) as availability_error:
|
|
127
|
-
return True, f"availability script failed to run: {availability_error}"
|
|
128
|
-
availability_detail_text = (completed_process.stdout + completed_process.stderr).strip()
|
|
129
|
-
is_reviewer_down = completed_process.returncode == AVAILABILITY_DOWN_EXIT_CODE
|
|
130
|
-
return not is_reviewer_down, availability_detail_text
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def main() -> None:
|
|
134
|
-
try:
|
|
135
|
-
hook_input = json.load(sys.stdin)
|
|
136
|
-
except json.JSONDecodeError:
|
|
137
|
-
sys.exit(0)
|
|
138
|
-
if not isinstance(hook_input, dict):
|
|
139
|
-
sys.exit(0)
|
|
140
|
-
|
|
141
|
-
tool_name = hook_input.get("tool_name", "")
|
|
142
|
-
if tool_name != BASH_TOOL_NAME:
|
|
143
|
-
sys.exit(0)
|
|
144
|
-
|
|
145
|
-
tool_input = hook_input.get("tool_input", {})
|
|
146
|
-
command = tool_input.get("command", "") if isinstance(tool_input, dict) else ""
|
|
147
|
-
if not command or GATE_SENTINEL_MARKER not in command:
|
|
148
|
-
sys.exit(0)
|
|
149
|
-
|
|
150
|
-
reviewer_trigger = _reviewer_trigger(command)
|
|
151
|
-
if reviewer_trigger is None:
|
|
152
|
-
sys.exit(0)
|
|
153
|
-
reviewer_token, reviewer_label = reviewer_trigger
|
|
154
|
-
|
|
155
|
-
is_available, availability_detail = _probe_reviewer_availability(reviewer_token)
|
|
156
|
-
if is_available:
|
|
157
|
-
sys.exit(0)
|
|
158
|
-
|
|
159
|
-
deny_reason = DENY_REASON_TEMPLATE.format(
|
|
160
|
-
reviewer_label=reviewer_label, availability_detail=availability_detail
|
|
161
|
-
)
|
|
162
|
-
deny_payload = {
|
|
163
|
-
"hookSpecificOutput": {
|
|
164
|
-
"hookEventName": "PreToolUse",
|
|
165
|
-
"permissionDecision": "deny",
|
|
166
|
-
"permissionDecisionReason": deny_reason,
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
log_hook_block(
|
|
170
|
-
calling_hook_name="reviewer_spawn_gate.py",
|
|
171
|
-
hook_event="PreToolUse",
|
|
172
|
-
block_reason=deny_reason,
|
|
173
|
-
tool_name=tool_name,
|
|
174
|
-
offending_input_preview=command,
|
|
175
|
-
)
|
|
176
|
-
print(json.dumps(deny_payload))
|
|
177
|
-
sys.stdout.flush()
|
|
178
|
-
sys.exit(0)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
if __name__ == "__main__":
|
|
182
|
-
main()
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
"""Regression tests for the code-review enforcement constants bootstrap.
|
|
2
|
-
|
|
3
|
-
These prove the shared constants module resolves to the sibling
|
|
4
|
-
``config/code_review_enforcement_constants.py`` file even when a foreign
|
|
5
|
-
``config`` package sits ahead of ``blocking`` on ``sys.path``, and that a
|
|
6
|
-
registration a caller placed first is left untouched.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
import subprocess
|
|
10
|
-
from collections.abc import Callable
|
|
11
|
-
|
|
12
|
-
RunUnderConfigShadow = Callable[[str], subprocess.CompletedProcess[str]]
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
def test_register_binds_dotted_name_to_sibling_constants_file(
|
|
16
|
-
run_under_config_shadow: RunUnderConfigShadow,
|
|
17
|
-
) -> None:
|
|
18
|
-
completed_probe = run_under_config_shadow(
|
|
19
|
-
"import code_review_enforcement_config_bootstrap as bootstrap\n"
|
|
20
|
-
"bootstrap.register_code_review_enforcement_constants()\n"
|
|
21
|
-
"import config.code_review_enforcement_constants as loaded\n"
|
|
22
|
-
"print(loaded.__file__)\n"
|
|
23
|
-
"print(loaded.STAMP_DIRECTORY_NAME)\n"
|
|
24
|
-
)
|
|
25
|
-
assert completed_probe.returncode == 0, completed_probe.stderr
|
|
26
|
-
assert "IS_DECOY_CONFIG" not in completed_probe.stdout
|
|
27
|
-
resolved_file_line, stamp_directory_line = completed_probe.stdout.splitlines()[:2]
|
|
28
|
-
assert resolved_file_line.replace("\\", "/").endswith(
|
|
29
|
-
"blocking/config/code_review_enforcement_constants.py"
|
|
30
|
-
)
|
|
31
|
-
assert stamp_directory_line == "code-review-stamps"
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def test_register_binds_push_required_effort(
|
|
35
|
-
run_under_config_shadow: RunUnderConfigShadow,
|
|
36
|
-
) -> None:
|
|
37
|
-
completed_probe = run_under_config_shadow(
|
|
38
|
-
"import code_review_enforcement_config_bootstrap as bootstrap\n"
|
|
39
|
-
"bootstrap.register_code_review_enforcement_constants()\n"
|
|
40
|
-
"import config.code_review_enforcement_constants as loaded\n"
|
|
41
|
-
"print(loaded.PUSH_REQUIRED_EFFORT)\n"
|
|
42
|
-
)
|
|
43
|
-
assert completed_probe.returncode == 0, completed_probe.stderr
|
|
44
|
-
assert completed_probe.stdout.strip().splitlines()[-1] == "low"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
def test_register_leaves_an_already_cached_module_untouched(
|
|
48
|
-
run_under_config_shadow: RunUnderConfigShadow,
|
|
49
|
-
) -> None:
|
|
50
|
-
completed_probe = run_under_config_shadow(
|
|
51
|
-
"import sys\n"
|
|
52
|
-
"import types\n"
|
|
53
|
-
"sentinel = types.ModuleType('config.code_review_enforcement_constants')\n"
|
|
54
|
-
"sentinel.ORIGIN = 'caller-seeded'\n"
|
|
55
|
-
"sys.modules['config.code_review_enforcement_constants'] = sentinel\n"
|
|
56
|
-
"import code_review_enforcement_config_bootstrap as bootstrap\n"
|
|
57
|
-
"bootstrap.register_code_review_enforcement_constants()\n"
|
|
58
|
-
"import config.code_review_enforcement_constants as loaded\n"
|
|
59
|
-
"print(getattr(loaded, 'ORIGIN', 'displaced'))\n"
|
|
60
|
-
)
|
|
61
|
-
assert completed_probe.returncode == 0, completed_probe.stderr
|
|
62
|
-
assert completed_probe.stdout.strip().splitlines()[-1] == "caller-seeded"
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
"""Behavioral tests for the shared code-review gate deny scaffold.
|
|
2
|
-
|
|
3
|
-
The tests drive the two shared helpers directly: the payload builder's shape and
|
|
4
|
-
the log-and-emit helper's stdout, so both code-review gates inherit one verified
|
|
5
|
-
deny path.
|
|
6
|
-
"""
|
|
7
|
-
|
|
8
|
-
from __future__ import annotations
|
|
9
|
-
|
|
10
|
-
import importlib.util
|
|
11
|
-
import json
|
|
12
|
-
import sys
|
|
13
|
-
from pathlib import Path
|
|
14
|
-
from types import ModuleType
|
|
15
|
-
|
|
16
|
-
import pytest
|
|
17
|
-
|
|
18
|
-
_HOOK_DIR = Path(__file__).resolve().parent
|
|
19
|
-
if str(_HOOK_DIR) not in sys.path:
|
|
20
|
-
sys.path.insert(0, str(_HOOK_DIR))
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
def _load_module(module_name: str) -> ModuleType:
|
|
24
|
-
module_spec = importlib.util.spec_from_file_location(
|
|
25
|
-
module_name, _HOOK_DIR / f"{module_name}.py"
|
|
26
|
-
)
|
|
27
|
-
assert module_spec is not None
|
|
28
|
-
assert module_spec.loader is not None
|
|
29
|
-
loaded_module = importlib.util.module_from_spec(module_spec)
|
|
30
|
-
module_spec.loader.exec_module(loaded_module)
|
|
31
|
-
return loaded_module
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
deny_module = _load_module("code_review_gate_deny")
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
def test_build_payload_carries_reason_and_deny_decision() -> None:
|
|
38
|
-
deny_payload = deny_module.build_code_review_deny_payload("blocked reason")
|
|
39
|
-
hook_output = deny_payload["hookSpecificOutput"]
|
|
40
|
-
assert hook_output["permissionDecision"] == "deny"
|
|
41
|
-
assert hook_output["permissionDecisionReason"] == "blocked reason"
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
def test_log_and_emit_code_review_deny_writes_payload_to_stdout(
|
|
45
|
-
capsys: pytest.CaptureFixture[str],
|
|
46
|
-
monkeypatch: pytest.MonkeyPatch,
|
|
47
|
-
tmp_path: Path,
|
|
48
|
-
) -> None:
|
|
49
|
-
monkeypatch.setattr(Path, "home", lambda: tmp_path)
|
|
50
|
-
deny_module.log_and_emit_code_review_deny("blocked reason", "Bash", "code_review_push_gate")
|
|
51
|
-
emitted_payload = json.loads(capsys.readouterr().out)
|
|
52
|
-
hook_output = emitted_payload["hookSpecificOutput"]
|
|
53
|
-
assert hook_output["permissionDecision"] == "deny"
|
|
54
|
-
assert hook_output["permissionDecisionReason"] == "blocked reason"
|