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,483 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""PreToolUse hook: pre-flight gate for the code-verifier subagent spawn.
|
|
3
|
-
|
|
4
|
-
The hook fires on an ``Agent`` or ``Task`` tool call whose ``subagent_type`` is
|
|
5
|
-
``code-verifier``. Before that verification spawn runs, the hook checks the
|
|
6
|
-
branch for two committability problems against the resolved base ref: a real
|
|
7
|
-
merge conflict (a non-mutating trial-merge of HEAD against the base ref) and a
|
|
8
|
-
CODE_RULES violation on a line added in the working tree since the merge base
|
|
9
|
-
(committed on the branch or uncommitted). When either fires, the hook denies
|
|
10
|
-
the spawn with a reason addressed to the spawning agent that names the
|
|
11
|
-
conflicting files and the violating file:line, so that agent fixes them and
|
|
12
|
-
re-spawns. A CODE_RULES engine import/load failure is fail-closed (deny with a
|
|
13
|
-
named load-failure section). Environmental problems — a non-repo cwd, an
|
|
14
|
-
absent base ref, a git failure, an unreadable file alone, or a timeout — fail
|
|
15
|
-
OPEN because the authoritative fail-closed CODE_RULES enforcement already runs
|
|
16
|
-
at Write time and at commit time. The hook never network-fetches and never
|
|
17
|
-
mutates the index or working tree.
|
|
18
|
-
"""
|
|
19
|
-
|
|
20
|
-
from __future__ import annotations
|
|
21
|
-
|
|
22
|
-
import contextlib
|
|
23
|
-
import io
|
|
24
|
-
import json
|
|
25
|
-
import re
|
|
26
|
-
import subprocess
|
|
27
|
-
import sys
|
|
28
|
-
from pathlib import Path
|
|
29
|
-
from typing import TextIO
|
|
30
|
-
|
|
31
|
-
_blocking_dir = str(Path(__file__).resolve().parent)
|
|
32
|
-
if _blocking_dir not in sys.path:
|
|
33
|
-
sys.path.insert(0, _blocking_dir)
|
|
34
|
-
|
|
35
|
-
_hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
36
|
-
if _hooks_dir not in sys.path:
|
|
37
|
-
sys.path.append(_hooks_dir)
|
|
38
|
-
|
|
39
|
-
from verification_verdict_store import ( # noqa: E402
|
|
40
|
-
candidate_base_references,
|
|
41
|
-
resolve_merge_base,
|
|
42
|
-
resolve_repo_root,
|
|
43
|
-
run_git,
|
|
44
|
-
untracked_file_paths,
|
|
45
|
-
)
|
|
46
|
-
|
|
47
|
-
from hooks_constants.code_verifier_spawn_preflight_gate_constants import ( # noqa: E402
|
|
48
|
-
ALL_CODE_VERIFIER_SPAWN_TOOL_NAMES,
|
|
49
|
-
ALL_MERGE_HEAD_PROBE_FLAGS,
|
|
50
|
-
ALL_MERGE_TREE_COMMAND_FLAGS,
|
|
51
|
-
ALL_NAME_ONLY_WORKTREE_DIFF_FLAGS,
|
|
52
|
-
ALL_UNIFIED_ZERO_DIFF_FLAGS,
|
|
53
|
-
ALL_UNMERGED_PATHS_DIFF_FLAGS,
|
|
54
|
-
CODE_RULES_SECTION_HEADER,
|
|
55
|
-
CODE_VERIFIER_SUBAGENT_TYPE,
|
|
56
|
-
DENY_REASON_LEAD,
|
|
57
|
-
ENGINE_LOAD_FAILURE_SECTION,
|
|
58
|
-
GATE_SCRIPTS_RELATIVE_PATH,
|
|
59
|
-
MERGE_CONFLICT_SECTION_HEADER,
|
|
60
|
-
MERGE_TREE_CLEAN_EXIT_CODE,
|
|
61
|
-
MERGE_TREE_CONFLICT_EXIT_CODE,
|
|
62
|
-
MERGE_TREE_TIMEOUT_SECONDS,
|
|
63
|
-
)
|
|
64
|
-
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
65
|
-
|
|
66
|
-
_scripts_dir = str(Path(__file__).resolve().parents[2] / GATE_SCRIPTS_RELATIVE_PATH)
|
|
67
|
-
if _scripts_dir not in sys.path:
|
|
68
|
-
sys.path.insert(0, _scripts_dir)
|
|
69
|
-
|
|
70
|
-
from code_rules_gate import ( # noqa: E402
|
|
71
|
-
ValidateContentCallable,
|
|
72
|
-
_collect_partitioned_violations,
|
|
73
|
-
_report_partitioned_violations,
|
|
74
|
-
is_code_path,
|
|
75
|
-
load_validate_content,
|
|
76
|
-
parse_added_line_numbers,
|
|
77
|
-
whole_file_line_set,
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def _should_run(payload_by_field: dict[str, object]) -> bool:
|
|
82
|
-
"""Return True only for a code-verifier Agent or Task spawn.
|
|
83
|
-
|
|
84
|
-
Args:
|
|
85
|
-
payload_by_field: The full PreToolUse hook payload (already
|
|
86
|
-
JSON-parsed), keyed by top-level field name.
|
|
87
|
-
|
|
88
|
-
Returns:
|
|
89
|
-
True when the tool is Agent or Task and ``tool_input.subagent_type``
|
|
90
|
-
is ``code-verifier``; False for every other shape.
|
|
91
|
-
"""
|
|
92
|
-
if payload_by_field.get("tool_name", "") not in ALL_CODE_VERIFIER_SPAWN_TOOL_NAMES:
|
|
93
|
-
return False
|
|
94
|
-
tool_input = payload_by_field.get("tool_input", {})
|
|
95
|
-
if not isinstance(tool_input, dict):
|
|
96
|
-
return False
|
|
97
|
-
return tool_input.get("subagent_type", "") == CODE_VERIFIER_SUBAGENT_TYPE
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def _resolve_repo_root_and_base(working_directory: str | None) -> tuple[str, str, str] | None:
|
|
101
|
-
"""Resolve the repo root, merge-base sha, and chosen base ref.
|
|
102
|
-
|
|
103
|
-
Args:
|
|
104
|
-
working_directory: The spawn's working directory from the payload, or
|
|
105
|
-
None when the payload carries no ``cwd``.
|
|
106
|
-
|
|
107
|
-
Returns:
|
|
108
|
-
A ``(repo_root, merge_base_sha, base_ref)`` triple, or None when the
|
|
109
|
-
directory is not a work tree or no base ref resolves on disk — the
|
|
110
|
-
caller fails OPEN on None.
|
|
111
|
-
"""
|
|
112
|
-
start_directory = working_directory if working_directory else str(Path.cwd())
|
|
113
|
-
repo_root = resolve_repo_root(start_directory)
|
|
114
|
-
if repo_root is None:
|
|
115
|
-
return None
|
|
116
|
-
merge_base_sha = resolve_merge_base(repo_root)
|
|
117
|
-
if merge_base_sha is None:
|
|
118
|
-
return None
|
|
119
|
-
for each_reference in candidate_base_references(repo_root):
|
|
120
|
-
if run_git(repo_root, "merge-base", "HEAD", each_reference) is not None:
|
|
121
|
-
return repo_root, merge_base_sha, each_reference
|
|
122
|
-
return None
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
def _run_trial_merge(repo_root: str, base_ref: str) -> tuple[int, str] | None:
|
|
126
|
-
"""Run the non-mutating trial-merge and return its exit code and stdout.
|
|
127
|
-
|
|
128
|
-
Args:
|
|
129
|
-
repo_root: The repository top-level directory.
|
|
130
|
-
base_ref: The base ref to trial-merge HEAD against.
|
|
131
|
-
|
|
132
|
-
Returns:
|
|
133
|
-
A ``(returncode, stdout)`` pair, or None when the command is missing,
|
|
134
|
-
times out, or raises an OS-level error — the caller fails OPEN on None.
|
|
135
|
-
"""
|
|
136
|
-
try:
|
|
137
|
-
completed_process = subprocess.run(
|
|
138
|
-
["git", "-C", repo_root, *ALL_MERGE_TREE_COMMAND_FLAGS, base_ref, "HEAD"],
|
|
139
|
-
capture_output=True,
|
|
140
|
-
text=True,
|
|
141
|
-
encoding="utf-8",
|
|
142
|
-
errors="replace",
|
|
143
|
-
timeout=MERGE_TREE_TIMEOUT_SECONDS,
|
|
144
|
-
check=False,
|
|
145
|
-
)
|
|
146
|
-
except (OSError, subprocess.TimeoutExpired):
|
|
147
|
-
return None
|
|
148
|
-
return completed_process.returncode, completed_process.stdout
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
def _merge_in_progress(repo_root: str) -> bool:
|
|
152
|
-
"""Decide whether an unfinished merge is recorded in the work tree.
|
|
153
|
-
|
|
154
|
-
Args:
|
|
155
|
-
repo_root: The repository top-level directory.
|
|
156
|
-
|
|
157
|
-
Returns:
|
|
158
|
-
True when ``MERGE_HEAD`` resolves — a merge is started but not yet
|
|
159
|
-
committed; False when no merge is in progress or git fails.
|
|
160
|
-
"""
|
|
161
|
-
return run_git(repo_root, *ALL_MERGE_HEAD_PROBE_FLAGS) is not None
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
def _staged_merge_conflicts(repo_root: str) -> list[str] | None:
|
|
165
|
-
"""Return the unmerged paths of an in-progress merge from the index.
|
|
166
|
-
|
|
167
|
-
A merge whose conflicts are fully resolved and staged leaves zero unmerged
|
|
168
|
-
index entries, so the resolved state is committable and this returns an
|
|
169
|
-
empty list. A merge with conflicts still open returns those paths, so the
|
|
170
|
-
caller keeps denying until the resolution is staged.
|
|
171
|
-
|
|
172
|
-
Args:
|
|
173
|
-
repo_root: The repository top-level directory.
|
|
174
|
-
|
|
175
|
-
Returns:
|
|
176
|
-
The still-unmerged paths (empty when the resolution is fully staged),
|
|
177
|
-
or None when git fails — the caller fails OPEN on None.
|
|
178
|
-
"""
|
|
179
|
-
unmerged_text = run_git(repo_root, *ALL_UNMERGED_PATHS_DIFF_FLAGS)
|
|
180
|
-
if unmerged_text is None:
|
|
181
|
-
return None
|
|
182
|
-
return [each_line for each_line in unmerged_text.splitlines() if each_line]
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
def _conflicting_files(repo_root: str, base_ref: str) -> list[str] | None:
|
|
186
|
-
"""Return the files that conflict when HEAD trial-merges against the base.
|
|
187
|
-
|
|
188
|
-
While a merge is in progress the branch tip has not yet recorded the
|
|
189
|
-
resolution, so the trial-merge of the base against HEAD would re-report
|
|
190
|
-
conflicts the staged index already fixed. In that state the index is the
|
|
191
|
-
authority: an empty unmerged set is a committable resolution, and any
|
|
192
|
-
remaining unmerged path is a live conflict.
|
|
193
|
-
|
|
194
|
-
Args:
|
|
195
|
-
repo_root: The repository top-level directory.
|
|
196
|
-
base_ref: The base ref to trial-merge HEAD against.
|
|
197
|
-
|
|
198
|
-
Returns:
|
|
199
|
-
The conflicting file paths on a conflict exit, an empty list on a
|
|
200
|
-
clean merge, or None on any infrastructure failure (command missing,
|
|
201
|
-
timeout, or an exit code that is neither clean nor conflict) — the
|
|
202
|
-
caller fails OPEN on None.
|
|
203
|
-
"""
|
|
204
|
-
if _merge_in_progress(repo_root):
|
|
205
|
-
return _staged_merge_conflicts(repo_root)
|
|
206
|
-
merge_outcome = _run_trial_merge(repo_root, base_ref)
|
|
207
|
-
if merge_outcome is None:
|
|
208
|
-
return None
|
|
209
|
-
return_code, merge_stdout = merge_outcome
|
|
210
|
-
if return_code == MERGE_TREE_CLEAN_EXIT_CODE:
|
|
211
|
-
return []
|
|
212
|
-
if return_code != MERGE_TREE_CONFLICT_EXIT_CODE:
|
|
213
|
-
return None
|
|
214
|
-
return _parse_conflicting_paths(merge_stdout)
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def _parse_conflicting_paths(merge_stdout: str) -> list[str]:
|
|
218
|
-
"""Extract conflicting paths from trial-merge stdout.
|
|
219
|
-
|
|
220
|
-
Args:
|
|
221
|
-
merge_stdout: The stdout of a conflict-exit trial-merge: the written
|
|
222
|
-
tree-OID line, then one conflicting path per line, then a blank
|
|
223
|
-
line and informational text.
|
|
224
|
-
|
|
225
|
-
Returns:
|
|
226
|
-
The conflicting file paths — the lines after the tree-OID up to the
|
|
227
|
-
first blank line.
|
|
228
|
-
"""
|
|
229
|
-
all_lines = merge_stdout.splitlines()
|
|
230
|
-
conflicting_paths: list[str] = []
|
|
231
|
-
for each_line in all_lines[1:]:
|
|
232
|
-
if not each_line.strip():
|
|
233
|
-
break
|
|
234
|
-
conflicting_paths.append(each_line)
|
|
235
|
-
return conflicting_paths
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
def _working_tree_added_lines_by_path(
|
|
239
|
-
repo_root: str, merge_base_sha: str
|
|
240
|
-
) -> tuple[list[Path], dict[Path, set[int]]] | None:
|
|
241
|
-
"""Build the code-file surface and its working-tree-vs-merge-base added lines.
|
|
242
|
-
|
|
243
|
-
Args:
|
|
244
|
-
repo_root: The repository top-level directory.
|
|
245
|
-
merge_base_sha: The merge-base sha the surface diffs against.
|
|
246
|
-
|
|
247
|
-
Returns:
|
|
248
|
-
A ``(file_paths, added_lines_by_path)`` pair keyed by resolved
|
|
249
|
-
absolute path, or None when a surface git query fails — the caller
|
|
250
|
-
fails OPEN on None.
|
|
251
|
-
"""
|
|
252
|
-
tracked_changed_text = run_git(repo_root, *ALL_NAME_ONLY_WORKTREE_DIFF_FLAGS, merge_base_sha)
|
|
253
|
-
if tracked_changed_text is None:
|
|
254
|
-
return None
|
|
255
|
-
untracked_paths = untracked_file_paths(repo_root)
|
|
256
|
-
if untracked_paths is None:
|
|
257
|
-
return None
|
|
258
|
-
repo_root_path = Path(repo_root)
|
|
259
|
-
file_paths: list[Path] = []
|
|
260
|
-
added_lines_by_path: dict[Path, set[int]] = {}
|
|
261
|
-
for each_relative in tracked_changed_text.splitlines():
|
|
262
|
-
if not each_relative or not is_code_path(Path(each_relative)):
|
|
263
|
-
continue
|
|
264
|
-
resolved_path = (repo_root_path / each_relative).resolve()
|
|
265
|
-
file_paths.append(resolved_path)
|
|
266
|
-
added_lines_by_path[resolved_path] = _tracked_file_added_lines(
|
|
267
|
-
repo_root, merge_base_sha, each_relative
|
|
268
|
-
)
|
|
269
|
-
for each_relative in untracked_paths:
|
|
270
|
-
if not is_code_path(Path(each_relative)):
|
|
271
|
-
continue
|
|
272
|
-
resolved_path = (repo_root_path / each_relative).resolve()
|
|
273
|
-
file_paths.append(resolved_path)
|
|
274
|
-
added_lines_by_path[resolved_path] = whole_file_line_set(resolved_path)
|
|
275
|
-
return file_paths, added_lines_by_path
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
def _tracked_file_added_lines(repo_root: str, merge_base_sha: str, relative_path: str) -> set[int]:
|
|
279
|
-
"""Return the working-tree-added line numbers for one tracked file.
|
|
280
|
-
|
|
281
|
-
Args:
|
|
282
|
-
repo_root: The repository top-level directory.
|
|
283
|
-
merge_base_sha: The merge-base sha the diff runs against.
|
|
284
|
-
relative_path: The repo-relative path of the tracked changed file.
|
|
285
|
-
|
|
286
|
-
Returns:
|
|
287
|
-
The 1-indexed line numbers added vs the merge base in the working
|
|
288
|
-
tree, or an empty set when the per-file diff fails.
|
|
289
|
-
"""
|
|
290
|
-
unified_diff_text = run_git(
|
|
291
|
-
repo_root, *ALL_UNIFIED_ZERO_DIFF_FLAGS, merge_base_sha, "--", relative_path
|
|
292
|
-
)
|
|
293
|
-
if unified_diff_text is None:
|
|
294
|
-
return set()
|
|
295
|
-
return parse_added_line_numbers(unified_diff_text)
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
def _code_rules_report(
|
|
299
|
-
repo_root: str, all_file_paths: list[Path], all_added_lines_by_path: dict[Path, set[int]]
|
|
300
|
-
) -> str | None:
|
|
301
|
-
"""Run the CODE_RULES engine and return a full deny section, or None.
|
|
302
|
-
|
|
303
|
-
Args:
|
|
304
|
-
repo_root: The repository top-level directory.
|
|
305
|
-
all_file_paths: The resolved code-file paths to inspect.
|
|
306
|
-
all_added_lines_by_path: Per-file working-tree-added line numbers keyed
|
|
307
|
-
by resolved absolute path.
|
|
308
|
-
|
|
309
|
-
Returns:
|
|
310
|
-
A full deny section when a blocking violation lands on an added line or
|
|
311
|
-
the CODE_RULES engine fails to import/load (fail-closed). None when the
|
|
312
|
-
surface is clean, only an unreadable changed file caused a non-zero gate
|
|
313
|
-
exit, or an environmental engine error arises — those non-block outcomes
|
|
314
|
-
fail OPEN. The harness hook timeout in hooks.json is the wall-clock
|
|
315
|
-
bound on a runaway engine.
|
|
316
|
-
"""
|
|
317
|
-
if not all_file_paths:
|
|
318
|
-
return None
|
|
319
|
-
try:
|
|
320
|
-
validate_content = load_validate_content()
|
|
321
|
-
except SystemExit:
|
|
322
|
-
return ENGINE_LOAD_FAILURE_SECTION
|
|
323
|
-
except (
|
|
324
|
-
ImportError,
|
|
325
|
-
SyntaxError,
|
|
326
|
-
AttributeError,
|
|
327
|
-
RuntimeError,
|
|
328
|
-
TypeError,
|
|
329
|
-
NameError,
|
|
330
|
-
OSError,
|
|
331
|
-
re.error,
|
|
332
|
-
):
|
|
333
|
-
return ENGINE_LOAD_FAILURE_SECTION
|
|
334
|
-
try:
|
|
335
|
-
blocking_present, captured_report = _run_gate_capturing_stderr(
|
|
336
|
-
validate_content, all_file_paths, Path(repo_root), all_added_lines_by_path
|
|
337
|
-
)
|
|
338
|
-
except OSError:
|
|
339
|
-
return None
|
|
340
|
-
if not blocking_present:
|
|
341
|
-
return None
|
|
342
|
-
return f"{CODE_RULES_SECTION_HEADER}\n{captured_report.strip()}"
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
def _run_gate_capturing_stderr(
|
|
346
|
-
validate_content: ValidateContentCallable,
|
|
347
|
-
all_file_paths: list[Path],
|
|
348
|
-
repository_root: Path,
|
|
349
|
-
all_added_lines_by_path: dict[Path, set[int]],
|
|
350
|
-
) -> tuple[bool, str]:
|
|
351
|
-
"""Run the gate, reporting whether a blocking violation was actually found.
|
|
352
|
-
|
|
353
|
-
Args:
|
|
354
|
-
validate_content: The enforcer ``validate_content`` callable.
|
|
355
|
-
all_file_paths: The resolved code-file paths to inspect.
|
|
356
|
-
repository_root: The repository root path the gate resolves against.
|
|
357
|
-
all_added_lines_by_path: Per-file working-tree-added line numbers keyed
|
|
358
|
-
by resolved absolute path.
|
|
359
|
-
|
|
360
|
-
Returns:
|
|
361
|
-
A ``(blocking_present, captured_report)`` pair. ``blocking_present`` is
|
|
362
|
-
True only when at least one blocking violation landed on an added line;
|
|
363
|
-
an unreadable changed file alone (which exits the gate non-zero) leaves
|
|
364
|
-
it False, so the caller fails OPEN. ``captured_report`` is the gate's
|
|
365
|
-
grouped stderr report.
|
|
366
|
-
"""
|
|
367
|
-
blocking_by_file, advisory_by_file, skipped_unreadable_count = (
|
|
368
|
-
_collect_partitioned_violations(
|
|
369
|
-
validate_content,
|
|
370
|
-
all_file_paths,
|
|
371
|
-
repository_root,
|
|
372
|
-
all_added_lines_by_path,
|
|
373
|
-
False,
|
|
374
|
-
)
|
|
375
|
-
)
|
|
376
|
-
captured_stderr = io.StringIO()
|
|
377
|
-
with contextlib.redirect_stderr(captured_stderr):
|
|
378
|
-
_report_partitioned_violations(
|
|
379
|
-
blocking_by_file,
|
|
380
|
-
advisory_by_file,
|
|
381
|
-
repository_root,
|
|
382
|
-
False,
|
|
383
|
-
skipped_unreadable_count,
|
|
384
|
-
)
|
|
385
|
-
return bool(blocking_by_file), captured_stderr.getvalue()
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
def _build_deny_reason(
|
|
389
|
-
all_conflicting_files: list[str] | None, base_ref: str, code_rules_report: str | None
|
|
390
|
-
) -> str | None:
|
|
391
|
-
"""Assemble the spawner-addressed deny reason from the two check results.
|
|
392
|
-
|
|
393
|
-
Args:
|
|
394
|
-
all_conflicting_files: The conflicting file paths from the conflict
|
|
395
|
-
check, an empty list when clean, or None when that check failed open.
|
|
396
|
-
base_ref: The base ref named in the conflict section header.
|
|
397
|
-
code_rules_report: A full CODE_RULES deny section (violations or engine
|
|
398
|
-
load failure), or None when that check found nothing or failed open.
|
|
399
|
-
|
|
400
|
-
Returns:
|
|
401
|
-
The full deny reason when either check fired, or None when neither
|
|
402
|
-
produced an issue.
|
|
403
|
-
"""
|
|
404
|
-
reason_sections: list[str] = []
|
|
405
|
-
if all_conflicting_files:
|
|
406
|
-
conflict_lines = "\n".join(f" {each_path}" for each_path in all_conflicting_files)
|
|
407
|
-
conflict_header = MERGE_CONFLICT_SECTION_HEADER.format(base_ref=base_ref)
|
|
408
|
-
reason_sections.append(f"{conflict_header}\n{conflict_lines}")
|
|
409
|
-
if code_rules_report:
|
|
410
|
-
reason_sections.append(code_rules_report.strip())
|
|
411
|
-
if not reason_sections:
|
|
412
|
-
return None
|
|
413
|
-
return DENY_REASON_LEAD + "\n\n" + "\n\n".join(reason_sections)
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
def _emit_deny_payload(output_stream: TextIO, reason: str) -> None:
|
|
417
|
-
"""Write the PreToolUse deny payload to the provided stream.
|
|
418
|
-
|
|
419
|
-
Args:
|
|
420
|
-
output_stream: Writable text stream — production code passes
|
|
421
|
-
``sys.stdout``; tests pass a ``StringIO`` to capture the JSON.
|
|
422
|
-
reason: The ``permissionDecisionReason`` text for the deny payload.
|
|
423
|
-
"""
|
|
424
|
-
deny_payload = {
|
|
425
|
-
"hookSpecificOutput": {
|
|
426
|
-
"hookEventName": "PreToolUse",
|
|
427
|
-
"permissionDecision": "deny",
|
|
428
|
-
"permissionDecisionReason": reason,
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
log_hook_block(
|
|
432
|
-
calling_hook_name="code_verifier_spawn_preflight_gate.py",
|
|
433
|
-
hook_event="PreToolUse",
|
|
434
|
-
block_reason=reason,
|
|
435
|
-
)
|
|
436
|
-
output_stream.write(json.dumps(deny_payload) + "\n")
|
|
437
|
-
output_stream.flush()
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
def _preflight_deny_reason(payload_by_field: dict[str, object]) -> str | None:
|
|
441
|
-
"""Run both pre-flight checks and return a deny reason, or None to allow.
|
|
442
|
-
|
|
443
|
-
Args:
|
|
444
|
-
payload_by_field: The full PreToolUse hook payload, keyed by top-level
|
|
445
|
-
field name.
|
|
446
|
-
|
|
447
|
-
Returns:
|
|
448
|
-
The deny reason when a check fired, or None when both checks pass or
|
|
449
|
-
fail open.
|
|
450
|
-
"""
|
|
451
|
-
working_directory = payload_by_field.get("cwd")
|
|
452
|
-
resolution = _resolve_repo_root_and_base(
|
|
453
|
-
working_directory if isinstance(working_directory, str) else None
|
|
454
|
-
)
|
|
455
|
-
if resolution is None:
|
|
456
|
-
return None
|
|
457
|
-
repo_root, merge_base_sha, base_ref = resolution
|
|
458
|
-
conflicting_files = _conflicting_files(repo_root, base_ref)
|
|
459
|
-
surface = _working_tree_added_lines_by_path(repo_root, merge_base_sha)
|
|
460
|
-
code_rules_report = None
|
|
461
|
-
if surface is not None:
|
|
462
|
-
file_paths, added_lines_by_path = surface
|
|
463
|
-
code_rules_report = _code_rules_report(repo_root, file_paths, added_lines_by_path)
|
|
464
|
-
return _build_deny_reason(conflicting_files, base_ref, code_rules_report)
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
def main() -> None:
|
|
468
|
-
try:
|
|
469
|
-
hook_payload = json.load(sys.stdin)
|
|
470
|
-
except json.JSONDecodeError:
|
|
471
|
-
sys.exit(0)
|
|
472
|
-
if not isinstance(hook_payload, dict):
|
|
473
|
-
sys.exit(0)
|
|
474
|
-
if not _should_run(hook_payload):
|
|
475
|
-
sys.exit(0)
|
|
476
|
-
deny_reason = _preflight_deny_reason(hook_payload)
|
|
477
|
-
if deny_reason is not None:
|
|
478
|
-
_emit_deny_payload(sys.stdout, deny_reason)
|
|
479
|
-
sys.exit(0)
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
if __name__ == "__main__":
|
|
483
|
-
main()
|
|
@@ -1,143 +0,0 @@
|
|
|
1
|
-
"""Single source of truth for the code-review enforcement gate family.
|
|
2
|
-
|
|
3
|
-
::
|
|
4
|
-
|
|
5
|
-
CLAUDE_CODE_REVIEW_ENFORCEMENT=1 -> CODE_REVIEW_ENFORCEMENT_ENABLED True
|
|
6
|
-
(unset) -> CODE_REVIEW_ENFORCEMENT_ENABLED False
|
|
7
|
-
effort_meets_threshold("xhigh", PUSH_REQUIRED_EFFORT) -> True
|
|
8
|
-
effort_meets_threshold("high", PR_CREATE_REQUIRED_EFFORT) -> False
|
|
9
|
-
|
|
10
|
-
Every gate reads its thresholds and messages from here, so the effort a push
|
|
11
|
-
needs and the effort a pull request needs never drift apart. The master flag
|
|
12
|
-
tracks the environment, so a machine opts in through its own environment and
|
|
13
|
-
the choice outlives a reinstall that rewrites this shipped file.
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
from __future__ import annotations
|
|
17
|
-
|
|
18
|
-
import os
|
|
19
|
-
|
|
20
|
-
CODE_REVIEW_ENFORCEMENT_ENV_VAR = "CLAUDE_CODE_REVIEW_ENFORCEMENT"
|
|
21
|
-
ALL_ENFORCEMENT_ENABLED_ENV_VALUES = frozenset({"1", "true", "yes", "on"})
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def code_review_enforcement_enabled_in_environment() -> bool:
|
|
25
|
-
"""Read whether this machine turns the code-review gates on.
|
|
26
|
-
|
|
27
|
-
::
|
|
28
|
-
|
|
29
|
-
CLAUDE_CODE_REVIEW_ENFORCEMENT=1 -> True
|
|
30
|
-
CLAUDE_CODE_REVIEW_ENFORCEMENT=" On " -> True
|
|
31
|
-
CLAUDE_CODE_REVIEW_ENFORCEMENT=0 -> False
|
|
32
|
-
(variable unset) -> False
|
|
33
|
-
|
|
34
|
-
A machine opts in through its own environment, so an install that rewrites
|
|
35
|
-
this shipped file leaves the choice standing. Any value outside the enabled
|
|
36
|
-
set, and an unset variable, read as off, so the gate family stays quiet
|
|
37
|
-
until someone asks for it.
|
|
38
|
-
|
|
39
|
-
Returns:
|
|
40
|
-
True when the variable holds an enabled value, False otherwise.
|
|
41
|
-
"""
|
|
42
|
-
raw_environment_setting = os.environ.get(CODE_REVIEW_ENFORCEMENT_ENV_VAR, "")
|
|
43
|
-
return raw_environment_setting.strip().lower() in ALL_ENFORCEMENT_ENABLED_ENV_VALUES
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
CODE_REVIEW_ENFORCEMENT_ENABLED = code_review_enforcement_enabled_in_environment()
|
|
47
|
-
STAMP_DIRECTORY_NAME = "code-review-stamps"
|
|
48
|
-
ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER = ("low", "medium", "high", "xhigh", "max")
|
|
49
|
-
PUSH_REQUIRED_EFFORT = "low"
|
|
50
|
-
PR_CREATE_REQUIRED_EFFORT = "xhigh"
|
|
51
|
-
STAMP_KEY_EFFORT = "effort"
|
|
52
|
-
STAMP_KEY_MANIFEST_SHA256 = "manifest_sha256"
|
|
53
|
-
STAMP_KEY_RECORDED_AT_EPOCH = "recorded_at_epoch"
|
|
54
|
-
CODE_REVIEW_BYPASS_MARKER = "# code-review-skip"
|
|
55
|
-
GATED_PUSH_SUBCOMMANDS = frozenset({"push"})
|
|
56
|
-
ALL_GATED_SHELL_TOOL_NAMES = ("Bash", "PowerShell")
|
|
57
|
-
ALL_WRITE_EDIT_TOOL_NAMES = ("Write", "Edit", "MultiEdit")
|
|
58
|
-
MCP_CREATE_PULL_REQUEST_TOOL_NAME = "mcp__plugin_github_github__create_pull_request"
|
|
59
|
-
HASH_PREVIEW_LENGTH = 16
|
|
60
|
-
PRE_TOOL_USE_HOOK_EVENT_NAME = "PreToolUse"
|
|
61
|
-
DENY_PERMISSION_DECISION = "deny"
|
|
62
|
-
PUSH_GATE_HOOK_MODULE_NAME = "code_review_push_gate.py"
|
|
63
|
-
PR_CREATE_GATE_HOOK_MODULE_NAME = "code_review_pr_create_gate.py"
|
|
64
|
-
STAMP_WRITE_BLOCKER_HOOK_MODULE_NAME = "code_review_stamp_directory_write_blocker.py"
|
|
65
|
-
SANCTIONED_STAMP_MINTER_FLAG = "--record-stamp"
|
|
66
|
-
PUSH_GATE_CORRECTIVE_MESSAGE = (
|
|
67
|
-
"BLOCKED: [CODE_REVIEW_PUSH_GATE] This branch surface has no clean "
|
|
68
|
-
"code-review stamp at effort 'low' or higher. Run "
|
|
69
|
-
"`python invoke_code_review.py --record-stamp --cwd <dir> "
|
|
70
|
-
"--session-model <alias> low` (or a higher effort) so a clean review "
|
|
71
|
-
"mints the stamp before `git push`; any file change after minting "
|
|
72
|
-
"invalidates it. Append `# code-review-skip` as a trailing shell comment "
|
|
73
|
-
"only when the verified-commit gate's verify-skip rule for a same-surface "
|
|
74
|
-
"re-land applies. Exempt automatically: docs/image files, pytest test "
|
|
75
|
-
"files, and Python files whose docstring- and comment-stripped AST is "
|
|
76
|
-
"unchanged."
|
|
77
|
-
)
|
|
78
|
-
PR_CREATE_GATE_CORRECTIVE_MESSAGE = (
|
|
79
|
-
"BLOCKED: [CODE_REVIEW_PR_CREATE_GATE] This branch surface has no clean "
|
|
80
|
-
"code-review stamp at effort 'xhigh' or higher. Run "
|
|
81
|
-
"`python invoke_code_review.py --record-stamp --cwd <dir> "
|
|
82
|
-
"--session-model <alias> xhigh` (or max) so a clean review mints the "
|
|
83
|
-
"stamp before `gh pr create` or the MCP create_pull_request tool. Any "
|
|
84
|
-
"file change after minting invalidates the stamp."
|
|
85
|
-
)
|
|
86
|
-
STAMP_DIRECTORY_GUARD_MESSAGE = (
|
|
87
|
-
"BLOCKED: [CODE_REVIEW_STAMP_DIRECTORY_GUARD] Access to the code-review "
|
|
88
|
-
"stamp directory (~/.claude/code-review-stamps/) is denied. Only "
|
|
89
|
-
"invoke_code_review.py --record-stamp mints stamp files; a shell or "
|
|
90
|
-
"Write/Edit forge here would defeat the push and PR-create gates. "
|
|
91
|
-
"Earn a stamp by running a clean /code-review through the invoker."
|
|
92
|
-
)
|
|
93
|
-
RELATIVE_STAMP_DIRECTORY_PATTERN = r"(?:^|(?<=[\s;&|(='\"]))code-review-stamps[\\/]"
|
|
94
|
-
STAMP_DIRECTORY_CHANGE_TARGET_PATTERN = r"[ \t]+['\"]?code-review-stamps[\\/]?['\"]?(?=[\s;&|]|$)"
|
|
95
|
-
STAMP_FILE_REFERENCE_BOUNDARY_PREFIX_PATTERN = r"(?:^|(?<=[\s;&|(='\"\\/]))"
|
|
96
|
-
STAMP_FILE_ROOT_KEY_JSON_SUFFIX_PATTERN = r"[\\/][0-9a-f]{%d}\.json"
|
|
97
|
-
GH_PR_CREATE_INVOCATION_PATTERN = (
|
|
98
|
-
r"(?:^|[;&|\n`({]|\$\()[ \t]*"
|
|
99
|
-
r"(?:(?:if|then|else|elif|while|until|do|!)[ \t]+)*"
|
|
100
|
-
r"(?:[A-Za-z_][A-Za-z0-9_]*=\S+[ \t]+)*"
|
|
101
|
-
r"gh(?:[ \t]+(?:--[A-Za-z][\w-]*(?:=\S+)?|-[A-Za-z])(?:[ \t]+(?!-)\S+)?)*"
|
|
102
|
-
r"[ \t]+pr[ \t]+create\b"
|
|
103
|
-
)
|
|
104
|
-
ALL_STAMP_PATH_SEGMENT_NAMES = (".claude", "code-review-stamps")
|
|
105
|
-
ALL_STAMP_PATH_SEGMENT_BODIES = ("claude", "code-review-stamps")
|
|
106
|
-
STAMP_STORE_MODULE_NAME = "code_review_stamp_store"
|
|
107
|
-
STAMP_MINT_FUNCTION_NAME = "record_clean_stamp"
|
|
108
|
-
ALL_STAMP_STORE_FORGE_PATTERNS = (
|
|
109
|
-
rf"\bimport\s+{STAMP_STORE_MODULE_NAME}\b",
|
|
110
|
-
rf"\bfrom\s+{STAMP_STORE_MODULE_NAME}\s+import\b",
|
|
111
|
-
rf"\b{STAMP_MINT_FUNCTION_NAME}\s*\(",
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def effort_meets_threshold(candidate_effort: str, required_effort: str) -> bool:
|
|
116
|
-
"""Decide whether a recorded effort satisfies a required threshold.
|
|
117
|
-
|
|
118
|
-
::
|
|
119
|
-
|
|
120
|
-
effort_meets_threshold("xhigh", "low") -> True xhigh outranks low
|
|
121
|
-
effort_meets_threshold("low", "xhigh") -> False low sits below xhigh
|
|
122
|
-
effort_meets_threshold("ultra", "low") -> False ultra is unranked
|
|
123
|
-
|
|
124
|
-
A candidate satisfies the threshold when it sits at or above the required
|
|
125
|
-
token in the ascending order. A token outside the known order — ``ultra``
|
|
126
|
-
among them — satisfies nothing, so an unrecognized stored effort clears no
|
|
127
|
-
gate.
|
|
128
|
-
|
|
129
|
-
Args:
|
|
130
|
-
candidate_effort: The effort a recorded stamp was earned at.
|
|
131
|
-
required_effort: The effort the gate demands for the action.
|
|
132
|
-
|
|
133
|
-
Returns:
|
|
134
|
-
True when both tokens are known and the candidate ranks at or above the
|
|
135
|
-
required token; False otherwise.
|
|
136
|
-
"""
|
|
137
|
-
if candidate_effort not in ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER:
|
|
138
|
-
return False
|
|
139
|
-
if required_effort not in ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER:
|
|
140
|
-
return False
|
|
141
|
-
candidate_rank = ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER.index(candidate_effort)
|
|
142
|
-
required_rank = ALL_EFFORT_TOKENS_IN_ASCENDING_ORDER.index(required_effort)
|
|
143
|
-
return candidate_rank >= required_rank
|