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,310 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""PreToolUse hook: block gh pr ready until convergence pre-conditions pass.
|
|
3
|
-
|
|
4
|
-
Runs check_convergence.py against the PR and denies the gh pr ready
|
|
5
|
-
call if any condition fails. The agent sees exactly which conditions
|
|
6
|
-
failed and can address them before retrying.
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
from __future__ import annotations
|
|
10
|
-
|
|
11
|
-
import json
|
|
12
|
-
import re
|
|
13
|
-
import subprocess
|
|
14
|
-
import sys
|
|
15
|
-
from pathlib import Path
|
|
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_pr_number import ( # noqa: E402
|
|
22
|
-
_extract_pr_number_from_command,
|
|
23
|
-
)
|
|
24
|
-
from hooks_constants.convergence_gate_blocker_constants import ( # noqa: E402
|
|
25
|
-
ALL_GH_PR_VIEW_NUMBER_COMMAND,
|
|
26
|
-
BASH_LINE_CONTINUATION_PATTERN,
|
|
27
|
-
COMMAND_SEPARATOR_PATTERN,
|
|
28
|
-
GH_PR_READY_ANCHOR_PATTERN,
|
|
29
|
-
GH_REPO_FLAG,
|
|
30
|
-
GIT_URL_SUFFIX,
|
|
31
|
-
PR_URL_OWNER_REPO_NUMBER_PATTERN,
|
|
32
|
-
REPO_OVERRIDE_FLAG_PATTERN,
|
|
33
|
-
REPO_SLUG_TEMPLATE,
|
|
34
|
-
)
|
|
35
|
-
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
def _ready_command_segment(command: str) -> str:
|
|
39
|
-
"""Return the ``gh pr ready`` invocation, clipped at the next command separator.
|
|
40
|
-
|
|
41
|
-
::
|
|
42
|
-
|
|
43
|
-
gh pr ready 161 && gh pr comment 999 --repo other-owner/other-repo
|
|
44
|
-
^^^^^^^^^^^^^^^^ clipped -> returned segment
|
|
45
|
-
|
|
46
|
-
Scanning only this segment keeps a ``--repo`` flag or PR URL that belongs to
|
|
47
|
-
a chained command from binding the gate to the wrong PR. A backslash-newline
|
|
48
|
-
continuation is folded to a space before the separator search, so a
|
|
49
|
-
``--repo`` flag written on a continued line stays inside the segment.
|
|
50
|
-
"""
|
|
51
|
-
ready_match = re.search(GH_PR_READY_ANCHOR_PATTERN, command)
|
|
52
|
-
if ready_match is None:
|
|
53
|
-
return command
|
|
54
|
-
ready_tail = command[ready_match.start() :]
|
|
55
|
-
ready_tail = re.sub(BASH_LINE_CONTINUATION_PATTERN, " ", ready_tail)
|
|
56
|
-
separator_match = re.search(COMMAND_SEPARATOR_PATTERN, ready_tail)
|
|
57
|
-
if separator_match is None:
|
|
58
|
-
return ready_tail
|
|
59
|
-
return ready_tail[: separator_match.start()]
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def _parse_pr_url(command: str) -> tuple[str, str, int] | None:
|
|
63
|
-
"""Return (owner, repo, pr_number) from a full PR URL in the command."""
|
|
64
|
-
url_match = re.search(PR_URL_OWNER_REPO_NUMBER_PATTERN, command)
|
|
65
|
-
if url_match is None:
|
|
66
|
-
return None
|
|
67
|
-
return url_match.group("owner"), url_match.group("repo"), int(url_match.group("number"))
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def _parse_repo_flag(command: str) -> tuple[str, str] | None:
|
|
71
|
-
"""Return (owner, repo) from a --repo/-R flag in the command."""
|
|
72
|
-
flag_match = re.search(REPO_OVERRIDE_FLAG_PATTERN, command)
|
|
73
|
-
if flag_match is None:
|
|
74
|
-
return None
|
|
75
|
-
return flag_match.group("owner"), flag_match.group("repo").removesuffix(GIT_URL_SUFFIX)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def _resolve_named_identity(
|
|
79
|
-
command: str, cwd: str | None
|
|
80
|
-
) -> tuple[tuple[str, str] | None, int] | None:
|
|
81
|
-
"""Resolve the repository the command names and its PR number.
|
|
82
|
-
|
|
83
|
-
A full PR URL yields both the (owner, repo) pair and the number. A
|
|
84
|
-
``--repo``/``-R`` flag yields the pair while the number resolves from the
|
|
85
|
-
command's positional argument or the named repository's current-branch PR.
|
|
86
|
-
With neither present, the pair is None — leaving gh bound to the current
|
|
87
|
-
directory's repository — and the number resolves the same way. Every parse
|
|
88
|
-
runs over the ``gh pr ready`` segment alone, clipped at the next command
|
|
89
|
-
separator, so a flag or URL belonging to a chained command cannot bind the
|
|
90
|
-
gate to the wrong PR.
|
|
91
|
-
|
|
92
|
-
Args:
|
|
93
|
-
command: The raw shell command captured by the hook.
|
|
94
|
-
cwd: The working directory gh runs in when the number resolves from the
|
|
95
|
-
current branch, or None for the process default.
|
|
96
|
-
|
|
97
|
-
Returns:
|
|
98
|
-
The (owner, repo) pair the command names — None when it names none —
|
|
99
|
-
paired with the resolved PR number, or None when no number resolves.
|
|
100
|
-
"""
|
|
101
|
-
ready_segment = _ready_command_segment(command)
|
|
102
|
-
pr_url_identity = _parse_pr_url(ready_segment)
|
|
103
|
-
if pr_url_identity is not None:
|
|
104
|
-
url_owner, url_repo, url_number = pr_url_identity
|
|
105
|
-
return (url_owner, url_repo), url_number
|
|
106
|
-
all_target_repo = _parse_repo_flag(ready_segment)
|
|
107
|
-
from_owner, from_repo = all_target_repo if all_target_repo is not None else (None, None)
|
|
108
|
-
resolved_pr_number = _resolve_pr_number(ready_segment, cwd, from_owner, from_repo)
|
|
109
|
-
if resolved_pr_number is None:
|
|
110
|
-
return None
|
|
111
|
-
return all_target_repo, resolved_pr_number
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
def _resolve_target_identity(command: str, cwd: str | None) -> tuple[str, str, int] | None:
|
|
115
|
-
"""Resolve the (owner, repo, pr_number) the gate keys its evidence to.
|
|
116
|
-
|
|
117
|
-
Delegates the repository-and-number resolution to the shared core, then
|
|
118
|
-
falls back to the cwd repository for the number when the command names no
|
|
119
|
-
repository of its own.
|
|
120
|
-
"""
|
|
121
|
-
named_identity = _resolve_named_identity(command, cwd)
|
|
122
|
-
if named_identity is None:
|
|
123
|
-
return None
|
|
124
|
-
all_target_repo, pr_number = named_identity
|
|
125
|
-
if all_target_repo is not None:
|
|
126
|
-
owner, repo = all_target_repo
|
|
127
|
-
return owner, repo, pr_number
|
|
128
|
-
|
|
129
|
-
cwd_identity = _resolve_owner_repo(cwd)
|
|
130
|
-
if cwd_identity is None:
|
|
131
|
-
return None
|
|
132
|
-
cwd_owner, cwd_repo = cwd_identity
|
|
133
|
-
return cwd_owner, cwd_repo, pr_number
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def _repo_flag_arguments(all_target_repo: tuple[str, str] | None) -> list[str]:
|
|
137
|
-
"""Build the ``--repo owner/repo`` arguments for a gh subcommand.
|
|
138
|
-
|
|
139
|
-
Args:
|
|
140
|
-
all_target_repo: The (owner, repo) pair the command names, or None when
|
|
141
|
-
the command names no repository.
|
|
142
|
-
|
|
143
|
-
Returns:
|
|
144
|
-
A ``[--repo, owner/repo]`` argument pair when a repository is named, or
|
|
145
|
-
an empty list when it is None — leaving gh bound to the current
|
|
146
|
-
directory's repository.
|
|
147
|
-
"""
|
|
148
|
-
if all_target_repo is None:
|
|
149
|
-
return []
|
|
150
|
-
owner, repo = all_target_repo
|
|
151
|
-
return [GH_REPO_FLAG, REPO_SLUG_TEMPLATE.format(owner=owner, repo=repo)]
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
def _resolve_current_branch_pr_number(
|
|
155
|
-
all_target_repo: tuple[str, str] | None, cwd: str | None
|
|
156
|
-
) -> int | None:
|
|
157
|
-
"""Resolve the current-branch PR number through ``gh pr view``.
|
|
158
|
-
|
|
159
|
-
Args:
|
|
160
|
-
all_target_repo: The (owner, repo) pair the command names, or None to
|
|
161
|
-
read the current directory's repository.
|
|
162
|
-
cwd: The working directory gh runs in, or None for the process default.
|
|
163
|
-
|
|
164
|
-
Returns:
|
|
165
|
-
The PR number gh reports, or None when gh is missing, exits non-zero,
|
|
166
|
-
or prints no integer — every failure path returns None so the caller
|
|
167
|
-
fails open.
|
|
168
|
-
"""
|
|
169
|
-
all_view_arguments = [
|
|
170
|
-
*ALL_GH_PR_VIEW_NUMBER_COMMAND,
|
|
171
|
-
*_repo_flag_arguments(all_target_repo),
|
|
172
|
-
]
|
|
173
|
-
try:
|
|
174
|
-
completed_process = subprocess.run(
|
|
175
|
-
all_view_arguments,
|
|
176
|
-
capture_output=True,
|
|
177
|
-
text=True,
|
|
178
|
-
cwd=cwd or None,
|
|
179
|
-
check=False,
|
|
180
|
-
)
|
|
181
|
-
except OSError:
|
|
182
|
-
return None
|
|
183
|
-
if completed_process.returncode != 0:
|
|
184
|
-
return None
|
|
185
|
-
try:
|
|
186
|
-
return int(completed_process.stdout.strip())
|
|
187
|
-
except (ValueError, TypeError):
|
|
188
|
-
return None
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
def _resolve_pr_number(
|
|
192
|
-
command: str,
|
|
193
|
-
cwd: str | None,
|
|
194
|
-
from_owner: str | None,
|
|
195
|
-
from_repo: str | None,
|
|
196
|
-
) -> int | None:
|
|
197
|
-
direct_number = _extract_pr_number_from_command(command)
|
|
198
|
-
if direct_number is not None:
|
|
199
|
-
return direct_number
|
|
200
|
-
if from_owner is not None and from_repo is not None:
|
|
201
|
-
all_target_repo: tuple[str, str] | None = (from_owner, from_repo)
|
|
202
|
-
else:
|
|
203
|
-
all_target_repo = None
|
|
204
|
-
return _resolve_current_branch_pr_number(all_target_repo, cwd)
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
def _resolve_owner_repo(cwd: str | None) -> tuple[str, str] | None:
|
|
208
|
-
try:
|
|
209
|
-
completed_process = subprocess.run(
|
|
210
|
-
["gh", "repo", "view", "--json", "owner,name", "--jq", ".owner.login,.name"],
|
|
211
|
-
capture_output=True,
|
|
212
|
-
text=True,
|
|
213
|
-
cwd=cwd or None,
|
|
214
|
-
check=False,
|
|
215
|
-
)
|
|
216
|
-
except OSError:
|
|
217
|
-
return None
|
|
218
|
-
if completed_process.returncode != 0:
|
|
219
|
-
return None
|
|
220
|
-
parts = completed_process.stdout.strip().splitlines()
|
|
221
|
-
if len(parts) <= 1:
|
|
222
|
-
match = re.match(
|
|
223
|
-
r"https://github\.com/([^/]+)/([^/]+?)(?:\.git)?$",
|
|
224
|
-
completed_process.stdout.strip(),
|
|
225
|
-
)
|
|
226
|
-
if match:
|
|
227
|
-
return match.group(1), match.group(2)
|
|
228
|
-
return None
|
|
229
|
-
return parts[0], parts[1]
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def _run_convergence_check(
|
|
233
|
-
script: str, owner: str, repo: str, pr_number: int, cwd: str | None
|
|
234
|
-
) -> subprocess.CompletedProcess[str]:
|
|
235
|
-
return subprocess.run(
|
|
236
|
-
[
|
|
237
|
-
sys.executable,
|
|
238
|
-
script,
|
|
239
|
-
"--owner",
|
|
240
|
-
owner,
|
|
241
|
-
"--repo",
|
|
242
|
-
repo,
|
|
243
|
-
"--pr-number",
|
|
244
|
-
str(pr_number),
|
|
245
|
-
],
|
|
246
|
-
capture_output=True,
|
|
247
|
-
text=True,
|
|
248
|
-
cwd=cwd or None,
|
|
249
|
-
check=False,
|
|
250
|
-
)
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
def main() -> None:
|
|
254
|
-
check_convergence_script = str(
|
|
255
|
-
Path.home() / ".claude/skills/pr-converge/scripts/check_convergence.py"
|
|
256
|
-
)
|
|
257
|
-
|
|
258
|
-
if not Path(check_convergence_script).is_file():
|
|
259
|
-
sys.exit(0)
|
|
260
|
-
|
|
261
|
-
try:
|
|
262
|
-
hook_input = json.load(sys.stdin)
|
|
263
|
-
except json.JSONDecodeError:
|
|
264
|
-
sys.exit(0)
|
|
265
|
-
|
|
266
|
-
tool_name = hook_input.get("tool_name", "")
|
|
267
|
-
if tool_name != "Bash":
|
|
268
|
-
sys.exit(0)
|
|
269
|
-
|
|
270
|
-
command = hook_input.get("tool_input", {}).get("command", "")
|
|
271
|
-
gh_pr_ready_pattern = re.compile(GH_PR_READY_ANCHOR_PATTERN)
|
|
272
|
-
if not gh_pr_ready_pattern.search(command):
|
|
273
|
-
sys.exit(0)
|
|
274
|
-
|
|
275
|
-
cwd = hook_input.get("cwd")
|
|
276
|
-
target_identity = _resolve_target_identity(command, cwd)
|
|
277
|
-
if target_identity is None:
|
|
278
|
-
sys.exit(0)
|
|
279
|
-
owner, repo, pr_number = target_identity
|
|
280
|
-
|
|
281
|
-
completed_process = _run_convergence_check(
|
|
282
|
-
check_convergence_script, owner, repo, pr_number, cwd
|
|
283
|
-
)
|
|
284
|
-
|
|
285
|
-
if completed_process.returncode in (0, 2):
|
|
286
|
-
sys.exit(0)
|
|
287
|
-
|
|
288
|
-
block_reason = (
|
|
289
|
-
"Convergence check failed — PR is not ready to mark ready:\n\n" + completed_process.stdout
|
|
290
|
-
)
|
|
291
|
-
deny_payload = {
|
|
292
|
-
"hookSpecificOutput": {
|
|
293
|
-
"hookEventName": "PreToolUse",
|
|
294
|
-
"permissionDecision": "deny",
|
|
295
|
-
"permissionDecisionReason": block_reason,
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
log_hook_block(
|
|
299
|
-
calling_hook_name="convergence_gate_blocker.py",
|
|
300
|
-
hook_event="PreToolUse",
|
|
301
|
-
block_reason=block_reason,
|
|
302
|
-
tool_name="Bash",
|
|
303
|
-
)
|
|
304
|
-
print(json.dumps(deny_payload))
|
|
305
|
-
sys.stdout.flush()
|
|
306
|
-
sys.exit(0)
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
if __name__ == "__main__":
|
|
310
|
-
main()
|
|
@@ -1,170 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env python3
|
|
2
|
-
"""PreToolUse hook: enforce formal bugteam Skill at Step 5 BUGTEAM ticks.
|
|
3
|
-
|
|
4
|
-
The pr-converge loop's Step 5 BUGTEAM contract requires the formal
|
|
5
|
-
``Skill({skill: "bugteam", args: "<PR URL>"})`` invocation per tick.
|
|
6
|
-
Substituting an ad-hoc ``Agent({subagent_type: "clean-coder", ...})`` audit
|
|
7
|
-
call returns a "converged" verdict without writing the artifact that
|
|
8
|
-
``check_convergence.py``'s ``bugteam_clean_at`` gate reads, so the loop later
|
|
9
|
-
hits ``gh pr ready`` and fails structurally with no formal review on the PR.
|
|
10
|
-
|
|
11
|
-
The companion tracker hook
|
|
12
|
-
(``pr_converge_bugteam_skill_tracker.py``) records every formal Skill
|
|
13
|
-
invocation. This enforcer reads the recorded HEAD and tick and denies any
|
|
14
|
-
clean-coder audit-shaped Agent call that has not first been preceded by the
|
|
15
|
-
formal Skill at the same HEAD and tick.
|
|
16
|
-
|
|
17
|
-
``qbug`` is NOT an accepted substitute; only the ``bugteam`` skill records
|
|
18
|
-
the gate artifact, and the tracker deliberately ignores ``qbug`` invocations.
|
|
19
|
-
"""
|
|
20
|
-
|
|
21
|
-
from __future__ import annotations
|
|
22
|
-
|
|
23
|
-
import json
|
|
24
|
-
import sys
|
|
25
|
-
from pathlib import Path
|
|
26
|
-
from typing import TextIO
|
|
27
|
-
|
|
28
|
-
_hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
29
|
-
if _hooks_dir not in sys.path:
|
|
30
|
-
sys.path.insert(0, _hooks_dir)
|
|
31
|
-
|
|
32
|
-
from hooks_constants.pr_converge_bugteam_enforcer_constants import ( # noqa: E402
|
|
33
|
-
AGENT_TOOL_NAME,
|
|
34
|
-
ALL_AUDIT_PROMPT_SUBSTRINGS,
|
|
35
|
-
BUGTEAM_PHASE,
|
|
36
|
-
CLEAN_CODER_SUBAGENT_TYPE,
|
|
37
|
-
ENFORCER_CORRECTIVE_MESSAGE,
|
|
38
|
-
STATE_FIELD_BUGTEAM_SKILL_INVOKED_AT_HEAD,
|
|
39
|
-
STATE_FIELD_BUGTEAM_SKILL_INVOKED_AT_TICK,
|
|
40
|
-
STATE_FIELD_CURRENT_HEAD,
|
|
41
|
-
STATE_FIELD_PHASE,
|
|
42
|
-
STATE_FIELD_TICK_COUNT,
|
|
43
|
-
)
|
|
44
|
-
from hooks_constants.pr_converge_bugteam_enforcer_state import ( # noqa: E402
|
|
45
|
-
load_state_dictionary,
|
|
46
|
-
resolve_state_path,
|
|
47
|
-
)
|
|
48
|
-
from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
|
|
49
|
-
from hooks_constants.pre_tool_use_stdin import ( # noqa: E402
|
|
50
|
-
read_hook_input_dictionary_from_stdin,
|
|
51
|
-
)
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
def _prompt_is_audit_shaped(agent_prompt: str) -> bool:
|
|
55
|
-
"""Return True when the Agent prompt looks like an audit substitute.
|
|
56
|
-
|
|
57
|
-
Args:
|
|
58
|
-
agent_prompt: The ``prompt`` field of the Agent tool_input.
|
|
59
|
-
|
|
60
|
-
Returns:
|
|
61
|
-
True when any audit-shaped substring appears in the prompt
|
|
62
|
-
(case-insensitive); False for fix-only or unrelated prompts.
|
|
63
|
-
"""
|
|
64
|
-
lowercased_prompt = agent_prompt.lower()
|
|
65
|
-
return any(
|
|
66
|
-
each_substring in lowercased_prompt for each_substring in ALL_AUDIT_PROMPT_SUBSTRINGS
|
|
67
|
-
)
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
def _has_formal_skill_fired_this_tick(state_by_field: dict[str, object]) -> bool:
|
|
71
|
-
"""Return True when the bugteam Skill registered at current HEAD and tick.
|
|
72
|
-
|
|
73
|
-
Args:
|
|
74
|
-
state_by_field: Parsed pr-converge state.json mapping each field name
|
|
75
|
-
to its recorded value.
|
|
76
|
-
|
|
77
|
-
Returns:
|
|
78
|
-
True when both ``bugteam_skill_invoked_at_head`` matches
|
|
79
|
-
``current_head`` and ``bugteam_skill_invoked_at_tick`` matches
|
|
80
|
-
``tick_count``; False when either is missing, stale, or carries a
|
|
81
|
-
type that violates state-schema.md (head must be ``str``, tick must
|
|
82
|
-
be a non-bool ``int``) — type-invalid values fail closed so the
|
|
83
|
-
enforcer rejects corrupted state.
|
|
84
|
-
"""
|
|
85
|
-
invoked_head = state_by_field.get(STATE_FIELD_BUGTEAM_SKILL_INVOKED_AT_HEAD)
|
|
86
|
-
current_head = state_by_field.get(STATE_FIELD_CURRENT_HEAD)
|
|
87
|
-
invoked_tick = state_by_field.get(STATE_FIELD_BUGTEAM_SKILL_INVOKED_AT_TICK)
|
|
88
|
-
current_tick = state_by_field.get(STATE_FIELD_TICK_COUNT)
|
|
89
|
-
if not isinstance(invoked_head, str) or not isinstance(current_head, str):
|
|
90
|
-
return False
|
|
91
|
-
if invoked_head != current_head:
|
|
92
|
-
return False
|
|
93
|
-
if isinstance(invoked_tick, bool) or isinstance(current_tick, bool):
|
|
94
|
-
return False
|
|
95
|
-
if not isinstance(invoked_tick, int) or not isinstance(current_tick, int):
|
|
96
|
-
return False
|
|
97
|
-
return invoked_tick == current_tick
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
def _should_block(payload_by_field: dict[str, object]) -> bool:
|
|
101
|
-
"""Return True when the Agent call is a BUGTEAM-phase Skill substitution.
|
|
102
|
-
|
|
103
|
-
Args:
|
|
104
|
-
payload_by_field: The full PreToolUse hook payload (already JSON-parsed),
|
|
105
|
-
keyed by top-level field name.
|
|
106
|
-
|
|
107
|
-
Returns:
|
|
108
|
-
True when every gating condition holds: tool is Agent, subagent_type
|
|
109
|
-
is clean-coder, prompt is audit-shaped, pr-converge state.json exists
|
|
110
|
-
with phase BUGTEAM, and the formal bugteam Skill has NOT been
|
|
111
|
-
recorded at the current HEAD and tick. False otherwise.
|
|
112
|
-
"""
|
|
113
|
-
if payload_by_field.get("tool_name", "") != AGENT_TOOL_NAME:
|
|
114
|
-
return False
|
|
115
|
-
tool_input = payload_by_field.get("tool_input", {})
|
|
116
|
-
if not isinstance(tool_input, dict):
|
|
117
|
-
return False
|
|
118
|
-
if tool_input.get("subagent_type", "") != CLEAN_CODER_SUBAGENT_TYPE:
|
|
119
|
-
return False
|
|
120
|
-
agent_prompt = tool_input.get("prompt", "")
|
|
121
|
-
if not isinstance(agent_prompt, str):
|
|
122
|
-
return False
|
|
123
|
-
if not _prompt_is_audit_shaped(agent_prompt):
|
|
124
|
-
return False
|
|
125
|
-
state_path = resolve_state_path()
|
|
126
|
-
if state_path is None:
|
|
127
|
-
return False
|
|
128
|
-
parsed_state = load_state_dictionary(state_path)
|
|
129
|
-
if parsed_state is None:
|
|
130
|
-
return False
|
|
131
|
-
if parsed_state.get(STATE_FIELD_PHASE) != BUGTEAM_PHASE:
|
|
132
|
-
return False
|
|
133
|
-
return not _has_formal_skill_fired_this_tick(parsed_state)
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def _emit_deny_payload(output_stream: TextIO) -> None:
|
|
137
|
-
"""Write the PreToolUse deny payload to the provided stream.
|
|
138
|
-
|
|
139
|
-
Args:
|
|
140
|
-
output_stream: Writable text stream — production code passes
|
|
141
|
-
``sys.stdout``; tests pass a ``StringIO`` to capture the JSON.
|
|
142
|
-
"""
|
|
143
|
-
deny_payload = {
|
|
144
|
-
"hookSpecificOutput": {
|
|
145
|
-
"hookEventName": "PreToolUse",
|
|
146
|
-
"permissionDecision": "deny",
|
|
147
|
-
"permissionDecisionReason": ENFORCER_CORRECTIVE_MESSAGE,
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
log_hook_block(
|
|
151
|
-
calling_hook_name="pr_converge_bugteam_enforcer.py",
|
|
152
|
-
hook_event="PreToolUse",
|
|
153
|
-
block_reason=ENFORCER_CORRECTIVE_MESSAGE,
|
|
154
|
-
)
|
|
155
|
-
output_stream.write(json.dumps(deny_payload) + "\n")
|
|
156
|
-
output_stream.flush()
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
def main() -> None:
|
|
160
|
-
hook_payload = read_hook_input_dictionary_from_stdin()
|
|
161
|
-
if hook_payload is None:
|
|
162
|
-
sys.exit(0)
|
|
163
|
-
if not _should_block(hook_payload):
|
|
164
|
-
sys.exit(0)
|
|
165
|
-
_emit_deny_payload(sys.stdout)
|
|
166
|
-
sys.exit(0)
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if __name__ == "__main__":
|
|
170
|
-
main()
|
|
@@ -1,148 +0,0 @@
|
|
|
1
|
-
"""Audit PR body markdown for prose substance, shape, and structural rules.
|
|
2
|
-
|
|
3
|
-
Strips Markdown ceremony to measure substantive prose, classifies the body as
|
|
4
|
-
trivial, standard, or heavy, enumerates section headers, prepares the prose
|
|
5
|
-
scanned for vague language, and flags self-closing references to the PR's own
|
|
6
|
-
number and the discouraged "This PR ..." opening. Vague-language enforcement
|
|
7
|
-
runs in validate_pr_body in pr_description_enforcer.py.
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
import re
|
|
11
|
-
import sys
|
|
12
|
-
from pathlib import Path
|
|
13
|
-
|
|
14
|
-
_hooks_dir = str(Path(__file__).resolve().parent.parent)
|
|
15
|
-
if _hooks_dir not in sys.path:
|
|
16
|
-
sys.path.insert(0, _hooks_dir)
|
|
17
|
-
|
|
18
|
-
from hooks_constants.pr_description_enforcer_constants import ( # noqa: E402
|
|
19
|
-
BLOCKQUOTE_LINE_PATTERN,
|
|
20
|
-
BLOCKQUOTE_MARKER_PATTERN,
|
|
21
|
-
BOLD_PAIR_PATTERN,
|
|
22
|
-
BULLET_MARKER_PATTERN,
|
|
23
|
-
FENCED_CODE_BLOCK_PATTERN,
|
|
24
|
-
HEADING_LINE_PATTERN,
|
|
25
|
-
HEAVY_MIN_BODY_CHARS_FOR_CLASSIFICATION,
|
|
26
|
-
HEAVY_SHAPE,
|
|
27
|
-
INLINE_CODE_PATTERN,
|
|
28
|
-
LINK_TEXT_PATTERN,
|
|
29
|
-
SELF_REFERENCE_PATTERN_TEMPLATE,
|
|
30
|
-
STANDARD_SHAPE,
|
|
31
|
-
TABLE_ROW_LINE_PATTERN,
|
|
32
|
-
THIS_PR_OPENING_PATTERN,
|
|
33
|
-
TRIVIAL_BODY_CHAR_THRESHOLD,
|
|
34
|
-
TRIVIAL_SHAPE,
|
|
35
|
-
WHITESPACE_RUN_PATTERN,
|
|
36
|
-
)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def strip_markdown_ceremony(body: str) -> str:
|
|
40
|
-
"""Return the body with Markdown ceremony stripped to leave underlying prose.
|
|
41
|
-
|
|
42
|
-
Removes fenced code, inline code, heading lines, blockquote markers,
|
|
43
|
-
bullet list markers, bold/emphasis markers, and Markdown link targets.
|
|
44
|
-
Whitespace is preserved so callers can collapse or measure it as needed.
|
|
45
|
-
"""
|
|
46
|
-
body_without_fences = FENCED_CODE_BLOCK_PATTERN.sub("", body)
|
|
47
|
-
body_without_inline_code = INLINE_CODE_PATTERN.sub("", body_without_fences)
|
|
48
|
-
body_without_blockquotes = BLOCKQUOTE_MARKER_PATTERN.sub("", body_without_inline_code)
|
|
49
|
-
body_without_headings = HEADING_LINE_PATTERN.sub("", body_without_blockquotes)
|
|
50
|
-
body_without_bullets = BULLET_MARKER_PATTERN.sub("", body_without_headings)
|
|
51
|
-
body_without_bold = BOLD_PAIR_PATTERN.sub(r"\1", body_without_bullets)
|
|
52
|
-
body_without_emphasis = body_without_bold.replace("*", "")
|
|
53
|
-
body_without_links = LINK_TEXT_PATTERN.sub(r"\1", body_without_emphasis)
|
|
54
|
-
return body_without_links
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def _count_substantive_prose_chars(body: str) -> int:
|
|
58
|
-
"""Return the count of prose characters after stripping Markdown ceremony.
|
|
59
|
-
|
|
60
|
-
Collapses internal whitespace so a body of only headers and bullets --
|
|
61
|
-
no real WHY paragraph -- registers as effectively empty.
|
|
62
|
-
"""
|
|
63
|
-
stripped_body = strip_markdown_ceremony(body)
|
|
64
|
-
body_collapsed = WHITESPACE_RUN_PATTERN.sub(" ", stripped_body).strip()
|
|
65
|
-
return len(body_collapsed)
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def _extract_vague_scan_text(body: str) -> str:
|
|
69
|
-
"""Return the prose to scan for vague language, with non-prose regions removed.
|
|
70
|
-
|
|
71
|
-
Drops whole blockquote lines and whole pipe-delimited table rows, then strips
|
|
72
|
-
the same Markdown ceremony as the prose-count path -- which removes fenced
|
|
73
|
-
code, inline code, and whole heading lines. This exempts vague phrases that
|
|
74
|
-
appear only inside code fences, inline code, Markdown headings, quoted
|
|
75
|
-
reviewer text, or pipe-delimited example tables -- those are not the author's
|
|
76
|
-
own prose. A pipe-delimited row carries at least two pipes; a line with a
|
|
77
|
-
single leading pipe, or a borderless table row with no leading pipe, stays in
|
|
78
|
-
scope.
|
|
79
|
-
"""
|
|
80
|
-
without_blockquote_lines = BLOCKQUOTE_LINE_PATTERN.sub("", body)
|
|
81
|
-
without_table_rows = TABLE_ROW_LINE_PATTERN.sub("", without_blockquote_lines)
|
|
82
|
-
return strip_markdown_ceremony(without_table_rows)
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def _iter_section_headers(body: str) -> list[str]:
|
|
86
|
-
"""Return every ATX heading line in the body, preserving canonical form.
|
|
87
|
-
|
|
88
|
-
HEADING_LINE_PATTERN matches the leading hash run (one or more hash
|
|
89
|
-
characters at line start), so the result spans every ATX level.
|
|
90
|
-
Downstream callers in this module only test specific two-hash header
|
|
91
|
-
strings, so matching every heading level keeps the parser permissive
|
|
92
|
-
without changing behaviour for the canonical two-hash header shape.
|
|
93
|
-
|
|
94
|
-
Fenced code blocks are stripped first so example markdown nested inside ``` fences
|
|
95
|
-
(a PR body that demonstrates the Heavy shape, for instance) is not counted as a
|
|
96
|
-
structural header. This keeps the shape classifier and Heavy required-header check
|
|
97
|
-
aligned with `strip_markdown_ceremony`, which already strips fences before measuring.
|
|
98
|
-
"""
|
|
99
|
-
body_without_fences = FENCED_CODE_BLOCK_PATTERN.sub("", body)
|
|
100
|
-
all_headers: list[str] = []
|
|
101
|
-
for each_match in HEADING_LINE_PATTERN.finditer(body_without_fences):
|
|
102
|
-
header_text = each_match.group(0).strip()
|
|
103
|
-
all_headers.append(header_text)
|
|
104
|
-
return all_headers
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
def _compute_pr_body_shape(body: str) -> str:
|
|
108
|
-
"""Classify a PR body as `trivial`, `standard`, or `heavy` from content alone.
|
|
109
|
-
|
|
110
|
-
Uses substantive prose chars (post-Markdown-strip) rather than raw length so the
|
|
111
|
-
classifier and the ceremony-on-Trivial check both measure the same metric against
|
|
112
|
-
TRIVIAL_BODY_CHAR_THRESHOLD; otherwise a body can be classified Standard by shape
|
|
113
|
-
while simultaneously being flagged as Trivial-sized by the ceremony check.
|
|
114
|
-
"""
|
|
115
|
-
substantive_length = _count_substantive_prose_chars(body)
|
|
116
|
-
header_count = len(_iter_section_headers(body))
|
|
117
|
-
|
|
118
|
-
if substantive_length < TRIVIAL_BODY_CHAR_THRESHOLD and header_count == 0:
|
|
119
|
-
return TRIVIAL_SHAPE
|
|
120
|
-
|
|
121
|
-
if substantive_length >= HEAVY_MIN_BODY_CHARS_FOR_CLASSIFICATION:
|
|
122
|
-
return HEAVY_SHAPE
|
|
123
|
-
|
|
124
|
-
return STANDARD_SHAPE
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
def _body_contains_any_header(body: str, all_candidate_headers: frozenset[str]) -> bool:
|
|
128
|
-
body_headers_lower = {each_header.lower() for each_header in _iter_section_headers(body)}
|
|
129
|
-
for each_candidate in all_candidate_headers:
|
|
130
|
-
candidate_lower = each_candidate.lower()
|
|
131
|
-
for each_present in body_headers_lower:
|
|
132
|
-
if each_present == candidate_lower:
|
|
133
|
-
return True
|
|
134
|
-
if each_present.startswith(candidate_lower):
|
|
135
|
-
character_after_candidate = each_present[len(candidate_lower)]
|
|
136
|
-
if not (character_after_candidate.isalnum() or character_after_candidate == "_"):
|
|
137
|
-
return True
|
|
138
|
-
return False
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
def _matches_self_closing_reference(body: str, pr_number: int) -> bool:
|
|
142
|
-
pattern_source = SELF_REFERENCE_PATTERN_TEMPLATE.format(pr_number=pr_number)
|
|
143
|
-
compiled_pattern = re.compile(pattern_source, re.IGNORECASE)
|
|
144
|
-
return compiled_pattern.search(body) is not None
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
def _opens_with_this_pr_phrase(body: str) -> bool:
|
|
148
|
-
return THIS_PR_OPENING_PATTERN.search(body) is not None
|