claude-dev-env 2.4.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +26 -59
- package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
- package/_shared/pr-loop/scripts/code_rules_gate.py +6 -3
- package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +12 -2
- package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +309 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +540 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +206 -70
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/__init__.py +1 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/_repo_test_helpers.py +76 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_baseline_import_isolation.py +248 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_regression.py +309 -0
- package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +91 -58
- package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +202 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
- package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
- package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
- package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
- package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
- package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
- package/_shared/pr-loop/worker-spawn.md +1 -1
- package/agents/CLAUDE.md +3 -2
- package/agents/caveman.md +0 -1
- package/agents/clasp-deployment-orchestrator.md +0 -1
- package/agents/clean-coder.md +0 -1
- package/agents/code-advisor.md +0 -1
- package/agents/code-quality-agent.md +1 -2
- package/agents/code-verifier.md +36 -8
- package/agents/deep-research.md +0 -1
- package/agents/docs-agent.md +0 -1
- package/agents/git-commit-crafter.md +0 -1
- package/agents/issue-tracker.md +42 -0
- package/agents/plan-packet-validator.md +0 -1
- package/agents/pr-description-writer.md +0 -1
- package/agents/test_agent_frontmatter.py +67 -18
- package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +143 -141
- package/bin/CLAUDE.md +68 -5
- package/bin/codex-compat.mjs +104 -0
- package/bin/codex-compat.test.mjs +51 -0
- package/bin/ever-shipped-skills.mjs +1 -0
- package/bin/install-constants.mjs +88 -0
- package/bin/install.mjs +1138 -114
- package/bin/install.prune.test.mjs +869 -19
- package/bin/install.test.mjs +906 -2
- package/codex-capability-map.json +13 -0
- package/commands/implement.md +1 -1
- package/commands/right-size.md +1 -1
- package/docs/CLAUDE.md +1 -0
- package/docs/CODE_RULES.md +2 -0
- package/docs/codex-compatibility.md +25 -0
- package/docs/host-pool-health-monitor.md +102 -0
- package/docs/nas-ssh-invocation.md +96 -12
- package/docs/references/CLAUDE.md +4 -2
- package/docs/references/advisor-tool.md +13 -0
- package/docs/references/code-review-enforcement.md +35 -0
- package/docs/references/team-advisor-skill.md +14 -0
- package/hooks/blocking/CLAUDE.md +4 -0
- package/hooks/blocking/code_review_pr_create_gate.py +7 -3
- package/hooks/blocking/code_review_push_gate.py +9 -4
- package/hooks/blocking/code_review_stamp_directory_write_blocker.py +8 -0
- package/hooks/blocking/config/__init__.py +5 -5
- package/hooks/blocking/config/code_review_enforcement_constants.py +40 -7
- package/hooks/blocking/config/test_code_review_enforcement_constants.py +58 -0
- package/hooks/blocking/config/verified_commit_constants.py +160 -159
- package/hooks/blocking/eli11_reply_enforcer.py +479 -0
- package/hooks/blocking/gh_body_arg_blocker.py +1 -1
- package/hooks/blocking/nas_ssh_binary_enforcer.py +8 -46
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
- package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
- package/hooks/blocking/shell_substitution_blocker.py +129 -0
- package/hooks/blocking/state_description_blocker.py +1 -1
- package/hooks/blocking/stop_dispatcher.py +1 -1
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +2 -3
- package/hooks/blocking/test_code_review_pr_create_gate.py +14 -0
- package/hooks/blocking/test_code_review_push_gate.py +16 -0
- package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +19 -0
- package/hooks/blocking/test_eli11_reply_enforcer.py +457 -0
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
- package/hooks/blocking/test_shell_substitution_blocker.py +124 -0
- package/hooks/blocking/test_stop_dispatcher.py +23 -0
- package/hooks/blocking/test_unscoped_search_blocker.py +102 -0
- package/hooks/blocking/test_verdict_directory_write_blocker.py +804 -808
- package/hooks/blocking/test_verification_verdict_store.py +54 -0
- package/hooks/blocking/test_verified_commit_gate.py +581 -581
- package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
- package/hooks/blocking/unscoped_search_blocker.py +391 -0
- package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
- package/hooks/blocking/verification_verdict_store.py +1039 -1036
- package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
- package/hooks/blocking/verifier_verdict_minter.py +280 -280
- package/hooks/git-hooks/CLAUDE.md +3 -0
- package/hooks/git-hooks/conftest.py +30 -0
- package/hooks/git-hooks/gate_utils.py +2 -2
- package/hooks/git-hooks/git_hooks_constants/__init__.py +41 -2
- package/hooks/git-hooks/pre_push.py +75 -4
- package/hooks/git-hooks/pre_push_base_reference.py +166 -0
- package/hooks/git-hooks/test_config.py +0 -15
- package/hooks/git-hooks/test_gate_utils.py +3 -15
- package/hooks/git-hooks/test_pre_commit.py +1 -15
- package/hooks/git-hooks/test_pre_push.py +257 -23
- package/hooks/git-hooks/test_pre_push_base_reference.py +339 -0
- package/hooks/hooks.json +10 -12
- package/hooks/hooks_constants/CLAUDE.md +7 -2
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +4 -4
- package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +101 -0
- package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +2 -8
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
- package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
- package/hooks/hooks_constants/shell_command_segments.py +82 -0
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +67 -0
- package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +5 -6
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +1 -0
- package/hooks/hooks_constants/unscoped_search_blocker_constants.py +153 -0
- package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
- package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
- package/hooks/pyproject.toml +1 -0
- package/hooks/validators/CLAUDE.md +1 -0
- package/hooks/validators/config/__init__.py +0 -0
- package/hooks/validators/config/directory_exemption_constants.py +183 -0
- package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
- package/hooks/validators/conftest.py +4 -0
- package/hooks/validators/ruff_integration.py +49 -5
- package/hooks/validators/run_all_validators.py +206 -9
- package/hooks/validators/test_directory_exemption_constants.py +185 -0
- package/hooks/validators/test_python_antipattern_checks.py +110 -5
- package/hooks/validators/test_ruff_integration.py +92 -1
- package/hooks/validators/test_run_all_validators.py +115 -68
- package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
- package/package.json +13 -3
- package/rules/CLAUDE.md +17 -22
- package/rules/agent-spawn-protocol.md +6 -6
- package/rules/anti-corollary-tests.md +1 -1
- package/rules/bdd.md +1 -1
- package/rules/cleanup-temp-files.md +10 -4
- package/rules/code-standards.md +7 -0
- package/rules/conservative-action.md +1 -5
- package/rules/context7.md +0 -4
- package/rules/destructive-commands.md +47 -0
- package/rules/doc-inventory-integrity.md +48 -0
- package/rules/doc-prose-cuts.md +58 -0
- package/rules/docstring-prose-matches-implementation.md +53 -44
- package/rules/durable-post-artifacts.md +0 -4
- package/rules/eli11-replies.md +31 -0
- package/rules/explore-thoroughly.md +4 -4
- package/rules/falsify-before-green.md +68 -0
- package/rules/file-global-constants.md +1 -1
- package/rules/filesystem-search.md +51 -0
- package/rules/gh-cli-conventions.md +27 -0
- package/rules/git-workflow.md +26 -0
- package/rules/hedging-claims.md +9 -0
- package/rules/long-horizon-autonomy.md +0 -4
- package/rules/measurement-denominators.md +48 -0
- package/rules/nas-ssh-invocation.md +23 -5
- package/rules/parallel-tools.md +2 -2
- package/rules/plain-illustrative-docstrings.md +3 -7
- package/rules/plain-language.md +2 -0
- package/rules/proof-of-work-pr-comments.md +0 -4
- package/rules/re-stage-before-commit.md +2 -0
- package/rules/research-mode.md +10 -0
- package/rules/shell-invocation.md +21 -0
- package/rules/testing.md +4 -0
- package/rules/verified-commit-gate-skip.md +3 -27
- package/rules/verify-before-asking.md +5 -0
- package/rules/windows-filesystem-safe.md +1 -1
- package/rules/workers-done-before-complete.md +4 -0
- package/scripts/CLAUDE.md +1 -0
- package/scripts/Capture-PoolHealth.ps1 +410 -0
- package/scripts/Migrate-ShellPolicy.ps1 +1 -1
- package/scripts/_code_review_test_support.py +404 -0
- package/scripts/claude_chain_runner.py +141 -1
- package/scripts/codex_capability_bridge.py +171 -0
- package/scripts/codex_compat_materializer.py +1087 -0
- package/scripts/codex_compat_watcher.py +502 -0
- package/scripts/conftest.py +16 -1
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
- package/scripts/dev_env_scripts_constants/claude_chain_constants.py +9 -0
- package/scripts/dev_env_scripts_constants/code_review_constants.py +37 -0
- package/scripts/invoke_code_review.py +11 -4
- package/scripts/resolve_worker_spawn.py +626 -626
- package/scripts/spawn_grok_batch.py +672 -672
- package/scripts/sync_to_cursor/rules.py +0 -10
- package/scripts/test_claude_chain_runner.py +131 -0
- package/scripts/test_invoke_code_review.py +85 -908
- package/scripts/test_invoke_code_review_chain.py +70 -0
- package/scripts/test_invoke_code_review_cli.py +192 -0
- package/scripts/test_invoke_code_review_contract.py +256 -0
- package/scripts/test_invoke_code_review_git.py +123 -0
- package/scripts/test_invoke_code_review_mode.py +99 -0
- package/scripts/test_resolve_worker_spawn.py +1014 -1014
- package/scripts/tests/test_code_review_constants.py +80 -0
- package/scripts/tests/test_codex_capability_bridge.py +91 -0
- package/scripts/tests/test_codex_compat_materializer.py +632 -0
- package/scripts/tests/test_codex_compat_watcher.py +599 -0
- package/scripts/tests/test_sync_to_cursor.py +0 -1
- package/skills/CLAUDE.md +2 -0
- package/skills/auditing-claude-config/SKILL.md +114 -114
- package/skills/autoconverge/SKILL.md +427 -427
- package/skills/autoconverge/reference/convergence.md +24 -3
- package/skills/autoconverge/workflow/CLAUDE.md +1 -0
- package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +3 -3
- package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1263
- package/skills/autoconverge/workflow/converge.mjs +168 -1
- package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
- package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
- package/skills/bugteam/reference/copilot-gap-analysis.md +1 -1
- package/skills/closeout/SKILL.md +33 -50
- package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
- package/skills/codex-review/scripts/run_codex_review.py +233 -1
- package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
- package/skills/condensing-instructions/SKILL.md +72 -0
- package/skills/copilot-review/SKILL.md +119 -119
- package/skills/e-code-review/SKILL.md +52 -0
- package/skills/e-code-review/reference/fix.md +54 -0
- package/skills/e-code-review/reference/loop.md +43 -0
- package/skills/e-code-review/reference/low.md +57 -0
- package/skills/e-code-review/reference/medium.md +153 -0
- package/skills/e-code-review/reference/xhigh.md +182 -0
- package/skills/e-simplify/SKILL.md +97 -0
- package/skills/fresh-branch/CLAUDE.md +1 -1
- package/skills/fresh-branch/SKILL.md +5 -6
- package/skills/fresh-branch/scripts/create_fresh_branch.py +42 -24
- package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -3
- package/skills/fresh-branch/scripts/test_create_fresh_branch.py +30 -126
- package/skills/issue-tracker/SKILL.md +92 -0
- package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
- package/skills/issue-tracker/reference/handoff-schema.md +64 -0
- package/skills/issue-tracker/reference/operation-matrix.md +41 -0
- package/skills/orchestrator/SKILL.md +177 -22
- package/skills/orchestrator/scripts/status_gate.py +625 -0
- package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
- package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
- package/skills/orchestrator/scripts/test_status_gate.py +439 -0
- package/skills/orchestrator-refresh/SKILL.md +129 -35
- package/skills/plan-to-pr/SKILL.md +155 -0
- package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
- package/skills/plan-to-pr/reference/model-routing.md +36 -0
- package/skills/plan-to-pr/reference/packet-contract.md +43 -0
- package/skills/plan-to-pr/reference/packet-schema.json +57 -0
- package/skills/plan-to-pr/reference/process-inventory.md +22 -0
- package/skills/plan-to-pr/reference/review-loop.md +33 -0
- package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
- package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
- package/skills/plan-to-pr/reference/task-seeds.md +14 -0
- package/skills/plan-to-pr/reference/task-ticket.md +38 -0
- package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
- package/skills/plan-to-pr/scripts/config/constants.py +193 -0
- package/skills/plan-to-pr/scripts/create_packet.py +173 -0
- package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
- package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
- package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
- package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
- package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
- package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
- package/skills/plan-to-pr/scripts/validate_run.py +173 -0
- package/skills/plan-to-pr/test_skill_contract.py +207 -0
- package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
- package/skills/pr-converge/SKILL.md +472 -469
- package/skills/pr-converge/reference/examples.md +3 -3
- package/skills/pr-converge/reference/fix-protocol.md +1 -1
- package/skills/pr-converge/reference/ground-rules.md +7 -4
- package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
- package/skills/pr-converge/reference/per-tick.md +5 -5
- package/skills/pr-converge/reference/progress-checklist.md +1 -1
- package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
- package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
- package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
- package/skills/pr-converge/test_step5_host_branch.py +1 -1
- package/skills/pr-fix-protocol/SKILL.md +1 -1
- package/skills/privacy-hygiene/SKILL.md +68 -68
- package/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
- package/skills/prototype/workflows/promotion.md +1 -1
- package/skills/release-notes-html/SKILL.md +164 -0
- package/skills/session-log/SKILL.md +1 -1
- package/skills/task-build/CLAUDE.md +8 -7
- package/skills/task-build/SKILL.md +16 -8
- package/skills/task-build/reference/tool-routing.md +19 -0
- package/rules/claude-md-orphan-file.md +0 -28
- package/rules/cleanup-command-forms.md +0 -23
- package/rules/code-reviews.md +0 -11
- package/rules/env-var-table-code-drift.md +0 -10
- package/rules/gh-body-file.md +0 -5
- package/rules/gh-paginate.md +0 -3
- package/rules/hook-prose-matches-detector.md +0 -15
- package/rules/no-historical-clutter.md +0 -26
- package/rules/no-inline-destructive-literals.md +0 -9
- package/rules/no-justification-noise.md +0 -61
- package/rules/package-inventory-stale-entry.md +0 -25
- package/rules/right-sized-engineering.md +0 -28
- package/rules/self-contained-docs.md +0 -17
- package/rules/shell-invocation-policy.md +0 -5
- package/rules/tdd.md +0 -7
- package/skills/closeout/reference/issue-body-templates.md +0 -108
|
@@ -9,7 +9,6 @@ import ...`` resolve against this file both inside the repo and under
|
|
|
9
9
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
|
-
|
|
13
12
|
STAGED_SCOPE_ARGUMENT: str = "--staged"
|
|
14
13
|
BASE_REFERENCE_ARGUMENT: str = "--base"
|
|
15
14
|
DEFAULT_REMOTE_BASE_REFERENCE: str = "origin/HEAD"
|
|
@@ -21,7 +20,7 @@ REMOTE_REFERENCE_FIELD_INDEX: int = 2
|
|
|
21
20
|
GATE_PATH_OVERRIDE_ENV_VAR: str = "CODE_RULES_GATE_PATH"
|
|
22
21
|
CLAUDE_HOME_ENV_VAR: str = "CLAUDE_HOME"
|
|
23
22
|
CLAUDE_HOME_DEFAULT_SUBDIRECTORY: str = ".claude"
|
|
24
|
-
|
|
23
|
+
ALL_GATE_SCRIPT_RELATIVE_PATH: tuple[str, ...] = (
|
|
25
24
|
"_shared",
|
|
26
25
|
"pr-loop",
|
|
27
26
|
"scripts",
|
|
@@ -65,3 +64,43 @@ PROTECTED_BRANCH_PUSH_BLOCK_MESSAGE: str = (
|
|
|
65
64
|
"To push the feature branch to its own ref, name the destination: "
|
|
66
65
|
"git push origin {local_branch}:refs/heads/{local_branch}"
|
|
67
66
|
)
|
|
67
|
+
GIT_EXECUTABLE_NAME: str = "git"
|
|
68
|
+
GIT_COMMAND_SUCCESS_EXIT_CODE: int = 0
|
|
69
|
+
GIT_OUTPUT_ENCODING_NAME: str = "utf-8"
|
|
70
|
+
GIT_OUTPUT_DECODE_ERRORS_POLICY: str = "replace"
|
|
71
|
+
GIT_COMMAND_UNAVAILABLE_MESSAGE: str = (
|
|
72
|
+
"claude-dev-env pre-push: could not run git ({error}), aborting"
|
|
73
|
+
)
|
|
74
|
+
GIT_REV_PARSE_SUBCOMMAND: str = "rev-parse"
|
|
75
|
+
GIT_REV_PARSE_VERIFY_FLAG: str = "--verify"
|
|
76
|
+
GIT_QUIET_FLAG: str = "--quiet"
|
|
77
|
+
GIT_SYMBOLIC_REFERENCE_SUBCOMMAND: str = "symbolic-ref"
|
|
78
|
+
GIT_COMMAND_TIMEOUT_SECONDS: int = 30
|
|
79
|
+
GIT_FOR_EACH_REF_SUBCOMMAND: str = "for-each-ref"
|
|
80
|
+
GIT_REFERENCE_SHORT_NAME_FORMAT_ARGUMENT: str = "--format=%(refname:short)"
|
|
81
|
+
COMMIT_OBJECT_NAME_SUFFIX: str = "^{commit}"
|
|
82
|
+
REMOTE_REFERENCE_NAME_PREFIX: str = "refs/remotes/"
|
|
83
|
+
REMOTE_HEAD_SYMBOLIC_REFERENCE_TEMPLATE: str = "refs/remotes/{remote}/HEAD"
|
|
84
|
+
REMOTE_HEAD_BRANCH_NAME: str = "HEAD"
|
|
85
|
+
REMOTE_BRANCH_SHORT_NAME_TEMPLATE: str = "{remote}/{branch}"
|
|
86
|
+
DEFAULT_REMOTE_NAME: str = "origin"
|
|
87
|
+
REMOTE_NAME_ARGUMENT_INDEX: int = 1
|
|
88
|
+
ALL_REJECTED_REMOTE_NAME_CHARACTERS: tuple[str, ...] = (
|
|
89
|
+
":",
|
|
90
|
+
"/",
|
|
91
|
+
"*",
|
|
92
|
+
"?",
|
|
93
|
+
"[",
|
|
94
|
+
"\\",
|
|
95
|
+
)
|
|
96
|
+
ALL_FALLBACK_REMOTE_DEFAULT_BRANCH_NAMES: tuple[str, ...] = (
|
|
97
|
+
"main",
|
|
98
|
+
"master",
|
|
99
|
+
"trunk",
|
|
100
|
+
"develop",
|
|
101
|
+
)
|
|
102
|
+
UNRESOLVABLE_BASE_REFERENCE_MESSAGE: str = (
|
|
103
|
+
"claude-dev-env pre-push: no usable gate base -- {reference} names no commit and "
|
|
104
|
+
"the default branch of remote {remote!r} could not be read.\n"
|
|
105
|
+
"Set the remote head so the gate has a base: git remote set-head {remote} --auto"
|
|
106
|
+
)
|
|
@@ -18,14 +18,17 @@ installed; deletions and same-name pushes pass.
|
|
|
18
18
|
Gate base: the first non-zero remote-sha is used as the gate `--base`, so
|
|
19
19
|
violations are scoped to commits that are not already on the remote. When
|
|
20
20
|
every remote object name is zero (new branch) or stdin is empty, the gate
|
|
21
|
-
falls back to the
|
|
21
|
+
falls back to the default branch of the remote git names in argv: its
|
|
22
|
+
symbolic ref first, then the first candidate default branch that exists
|
|
23
|
+
under that remote.
|
|
22
24
|
|
|
23
25
|
Exit codes:
|
|
24
26
|
0 - the push destination is allowed and its commits pass the gate (or
|
|
25
27
|
the gate is not installed).
|
|
26
28
|
1 - the push would land a non-protected local branch onto a protected
|
|
27
29
|
remote branch, or a commit introduces a blocking violation.
|
|
28
|
-
2 - unexpected invocation failure (e.g., subprocess could not launch)
|
|
30
|
+
2 - unexpected invocation failure (e.g., subprocess could not launch), or
|
|
31
|
+
no gate base that git resolves.
|
|
29
32
|
"""
|
|
30
33
|
|
|
31
34
|
from __future__ import annotations
|
|
@@ -36,6 +39,7 @@ import subprocess
|
|
|
36
39
|
import sys
|
|
37
40
|
from pathlib import Path
|
|
38
41
|
|
|
42
|
+
from gate_utils import is_safe_regular_file, resolve_gate_script_path
|
|
39
43
|
from git_hooks_constants import (
|
|
40
44
|
ALL_PROTECTED_BRANCH_PUSH_NAMES,
|
|
41
45
|
ALL_ZEROS_OBJECT_NAME_CHARACTER,
|
|
@@ -48,6 +52,11 @@ from git_hooks_constants import (
|
|
|
48
52
|
CODE_REVIEW_STAMP_BLOCK_EXIT_CODE,
|
|
49
53
|
DEFAULT_REMOTE_BASE_REFERENCE,
|
|
50
54
|
GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE,
|
|
55
|
+
GIT_COMMAND_TIMEOUT_SECONDS,
|
|
56
|
+
GIT_COMMAND_UNAVAILABLE_MESSAGE,
|
|
57
|
+
GIT_EXECUTABLE_NAME,
|
|
58
|
+
GIT_OUTPUT_DECODE_ERRORS_POLICY,
|
|
59
|
+
GIT_OUTPUT_ENCODING_NAME,
|
|
51
60
|
INVOKE_GATE_FAILURE_MESSAGE,
|
|
52
61
|
LOCAL_BRANCH_REFERENCE_PREFIX,
|
|
53
62
|
LOCAL_REFERENCE_FIELD_INDEX,
|
|
@@ -63,7 +72,60 @@ from git_hooks_constants import (
|
|
|
63
72
|
STDIN_READ_FAILURE_MESSAGE,
|
|
64
73
|
STDIN_REMOTE_OBJECT_FIELD_INDEX,
|
|
65
74
|
)
|
|
66
|
-
from
|
|
75
|
+
from pre_push_base_reference import (
|
|
76
|
+
resolve_remote_name_from_arguments,
|
|
77
|
+
resolve_usable_base_reference,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _report_unavailable_git(launch_error: Exception) -> int:
|
|
82
|
+
"""Report a git that would not run, and hand back the exit code to use."""
|
|
83
|
+
git_command_unavailable_message = GIT_COMMAND_UNAVAILABLE_MESSAGE
|
|
84
|
+
sys.stderr.write(
|
|
85
|
+
git_command_unavailable_message.format(error=launch_error) + "\n"
|
|
86
|
+
)
|
|
87
|
+
return GATE_INFRASTRUCTURE_FAILURE_EXIT_CODE
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class GitCommandUnavailable(RuntimeError):
|
|
91
|
+
"""Git itself could not run: it failed to launch, or it timed out.
|
|
92
|
+
|
|
93
|
+
A git that runs and reports nothing is ordinary absence, which stays a
|
|
94
|
+
plain return. Separating the two keeps a broken toolchain from reading
|
|
95
|
+
as a repository that simply has no default branch.
|
|
96
|
+
"""
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def run_git_text_command(all_command_arguments: list[str]) -> tuple[int, str]:
|
|
100
|
+
"""Ask git a question and read back its answer.
|
|
101
|
+
|
|
102
|
+
Output decodes with a replacing policy, so a reference whose bytes are
|
|
103
|
+
invalid in this encoding becomes a marked string and matches nothing.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
all_command_arguments: The git arguments following the executable name.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
The exit code paired with the trimmed standard output.
|
|
110
|
+
|
|
111
|
+
Raises:
|
|
112
|
+
GitCommandUnavailable: Git failed to launch or exceeded its timeout.
|
|
113
|
+
"""
|
|
114
|
+
git_executable_name = GIT_EXECUTABLE_NAME
|
|
115
|
+
git_command_timeout_seconds = GIT_COMMAND_TIMEOUT_SECONDS
|
|
116
|
+
try:
|
|
117
|
+
completion = subprocess.run(
|
|
118
|
+
[git_executable_name, *all_command_arguments],
|
|
119
|
+
check=False,
|
|
120
|
+
capture_output=True,
|
|
121
|
+
timeout=git_command_timeout_seconds,
|
|
122
|
+
)
|
|
123
|
+
except (OSError, subprocess.SubprocessError) as launch_error:
|
|
124
|
+
raise GitCommandUnavailable(str(launch_error)) from launch_error
|
|
125
|
+
decoded_output = completion.stdout.decode(
|
|
126
|
+
GIT_OUTPUT_ENCODING_NAME, errors=GIT_OUTPUT_DECODE_ERRORS_POLICY
|
|
127
|
+
)
|
|
128
|
+
return completion.returncode, decoded_output.strip()
|
|
67
129
|
|
|
68
130
|
|
|
69
131
|
def is_all_zeros_object_name(object_name: str) -> bool:
|
|
@@ -282,7 +344,16 @@ def main() -> int:
|
|
|
282
344
|
if base_reference == no_parseable_stdin_lines_sentinel:
|
|
283
345
|
print(no_parseable_stdin_lines_message, file=sys.stderr)
|
|
284
346
|
return gate_infrastructure_failure_exit_code
|
|
285
|
-
|
|
347
|
+
remote_name = resolve_remote_name_from_arguments(sys.argv)
|
|
348
|
+
try:
|
|
349
|
+
usable_base_reference = resolve_usable_base_reference(
|
|
350
|
+
base_reference, remote_name, run_git_text_command
|
|
351
|
+
)
|
|
352
|
+
except GitCommandUnavailable as unavailable_error:
|
|
353
|
+
return _report_unavailable_git(unavailable_error)
|
|
354
|
+
if usable_base_reference is None:
|
|
355
|
+
return gate_infrastructure_failure_exit_code
|
|
356
|
+
code_rules_exit_code = invoke_gate(gate_script_path, usable_base_reference)
|
|
286
357
|
if code_rules_exit_code != 0:
|
|
287
358
|
return code_rules_exit_code
|
|
288
359
|
return code_review_stamp_block_exit_code()
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Resolve the gate base that the pre-push hook hands to the CODE_RULES gate.
|
|
3
|
+
|
|
4
|
+
Git names the pushed remote in the hook's first argument, so every reference
|
|
5
|
+
built here keys on that name: the remote's symbolic head first, then the
|
|
6
|
+
candidate default branches that exist under the same remote.
|
|
7
|
+
|
|
8
|
+
The caller passes its own git seam, so a specification drives these functions
|
|
9
|
+
with a table of references in place of a repository.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
from collections.abc import Callable
|
|
16
|
+
|
|
17
|
+
from git_hooks_constants import (
|
|
18
|
+
ALL_FALLBACK_REMOTE_DEFAULT_BRANCH_NAMES,
|
|
19
|
+
ALL_REJECTED_REMOTE_NAME_CHARACTERS,
|
|
20
|
+
COMMIT_OBJECT_NAME_SUFFIX,
|
|
21
|
+
DEFAULT_REMOTE_BASE_REFERENCE,
|
|
22
|
+
DEFAULT_REMOTE_NAME,
|
|
23
|
+
GIT_COMMAND_SUCCESS_EXIT_CODE,
|
|
24
|
+
GIT_FOR_EACH_REF_SUBCOMMAND,
|
|
25
|
+
GIT_QUIET_FLAG,
|
|
26
|
+
GIT_REFERENCE_SHORT_NAME_FORMAT_ARGUMENT,
|
|
27
|
+
GIT_REV_PARSE_SUBCOMMAND,
|
|
28
|
+
GIT_REV_PARSE_VERIFY_FLAG,
|
|
29
|
+
GIT_SYMBOLIC_REFERENCE_SUBCOMMAND,
|
|
30
|
+
REMOTE_BRANCH_SHORT_NAME_TEMPLATE,
|
|
31
|
+
REMOTE_HEAD_BRANCH_NAME,
|
|
32
|
+
REMOTE_HEAD_SYMBOLIC_REFERENCE_TEMPLATE,
|
|
33
|
+
REMOTE_NAME_ARGUMENT_INDEX,
|
|
34
|
+
REMOTE_REFERENCE_NAME_PREFIX,
|
|
35
|
+
UNRESOLVABLE_BASE_REFERENCE_MESSAGE,
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
AskGit = Callable[[list[str]], tuple[int, str]]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def resolve_remote_name_from_arguments(all_command_line_arguments: list[str]) -> str:
|
|
42
|
+
"""Name the remote this push targets from the arguments git passed.
|
|
43
|
+
|
|
44
|
+
::
|
|
45
|
+
|
|
46
|
+
["pre-push", "upstream", "https://host/owner.git"] -> "upstream"
|
|
47
|
+
["pre-push", "https://host/owner.git", ...] -> "origin"
|
|
48
|
+
|
|
49
|
+
Git passes a remote name for a named remote and a URL for an ad-hoc push.
|
|
50
|
+
A URL carries a colon or a slash, which a remote name never does, so a URL
|
|
51
|
+
falls back to the default remote. A name carrying a glob character falls
|
|
52
|
+
back the same way, so no wildcard reaches the reference patterns built here.
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
all_command_line_arguments: The hook's own argument list.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
The remote name that every reference here is built from.
|
|
59
|
+
"""
|
|
60
|
+
remote_name_argument_index = REMOTE_NAME_ARGUMENT_INDEX
|
|
61
|
+
if len(all_command_line_arguments) <= remote_name_argument_index:
|
|
62
|
+
return DEFAULT_REMOTE_NAME
|
|
63
|
+
pushed_argument = all_command_line_arguments[remote_name_argument_index].strip()
|
|
64
|
+
if not pushed_argument:
|
|
65
|
+
return DEFAULT_REMOTE_NAME
|
|
66
|
+
if any(
|
|
67
|
+
each_character in pushed_argument
|
|
68
|
+
for each_character in ALL_REJECTED_REMOTE_NAME_CHARACTERS
|
|
69
|
+
):
|
|
70
|
+
return DEFAULT_REMOTE_NAME
|
|
71
|
+
return pushed_argument
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _names_a_commit(reference_name: str, ask_git: AskGit) -> bool:
|
|
75
|
+
"""Report whether git resolves a reference name to a commit."""
|
|
76
|
+
exit_code, _ = ask_git(
|
|
77
|
+
[
|
|
78
|
+
GIT_REV_PARSE_SUBCOMMAND,
|
|
79
|
+
GIT_REV_PARSE_VERIFY_FLAG,
|
|
80
|
+
GIT_QUIET_FLAG,
|
|
81
|
+
reference_name + COMMIT_OBJECT_NAME_SUFFIX,
|
|
82
|
+
]
|
|
83
|
+
)
|
|
84
|
+
return exit_code == GIT_COMMAND_SUCCESS_EXIT_CODE
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _first_existing_candidate_branch(remote_name: str, ask_git: AskGit) -> str | None:
|
|
88
|
+
"""Name the first candidate default branch that exists under a remote.
|
|
89
|
+
|
|
90
|
+
Git lists matching references sorted by reference name, so the candidate
|
|
91
|
+
order below decides which listed name wins.
|
|
92
|
+
"""
|
|
93
|
+
all_candidate_short_names = [
|
|
94
|
+
REMOTE_BRANCH_SHORT_NAME_TEMPLATE.format(remote=remote_name, branch=each_branch)
|
|
95
|
+
for each_branch in ALL_FALLBACK_REMOTE_DEFAULT_BRANCH_NAMES
|
|
96
|
+
]
|
|
97
|
+
_, listed_short_names = ask_git(
|
|
98
|
+
[
|
|
99
|
+
GIT_FOR_EACH_REF_SUBCOMMAND,
|
|
100
|
+
GIT_REFERENCE_SHORT_NAME_FORMAT_ARGUMENT,
|
|
101
|
+
*(
|
|
102
|
+
REMOTE_REFERENCE_NAME_PREFIX + each_short_name
|
|
103
|
+
for each_short_name in all_candidate_short_names
|
|
104
|
+
),
|
|
105
|
+
]
|
|
106
|
+
)
|
|
107
|
+
all_existing_short_names = set(listed_short_names.split())
|
|
108
|
+
for each_candidate_short_name in all_candidate_short_names:
|
|
109
|
+
if each_candidate_short_name in all_existing_short_names:
|
|
110
|
+
return each_candidate_short_name
|
|
111
|
+
return None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _remote_default_branch_reference(remote_name: str, ask_git: AskGit) -> str | None:
|
|
115
|
+
"""Name a remote's default branch from its symbolic ref, then candidates."""
|
|
116
|
+
exit_code, symbolic_target = ask_git(
|
|
117
|
+
[
|
|
118
|
+
GIT_SYMBOLIC_REFERENCE_SUBCOMMAND,
|
|
119
|
+
GIT_QUIET_FLAG,
|
|
120
|
+
REMOTE_HEAD_SYMBOLIC_REFERENCE_TEMPLATE.format(remote=remote_name),
|
|
121
|
+
]
|
|
122
|
+
)
|
|
123
|
+
if exit_code == GIT_COMMAND_SUCCESS_EXIT_CODE and symbolic_target:
|
|
124
|
+
return symbolic_target.removeprefix(REMOTE_REFERENCE_NAME_PREFIX)
|
|
125
|
+
return _first_existing_candidate_branch(remote_name, ask_git)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _report_unresolvable_base(base_reference: str, remote_name: str) -> None:
|
|
129
|
+
"""Tell the pusher which remote head to set so the gate gains a base."""
|
|
130
|
+
sys.stderr.write(
|
|
131
|
+
UNRESOLVABLE_BASE_REFERENCE_MESSAGE.format(reference=base_reference, remote=remote_name)
|
|
132
|
+
+ "\n"
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def resolve_usable_base_reference(
|
|
137
|
+
base_reference: str, remote_name: str, ask_git: AskGit
|
|
138
|
+
) -> str | None:
|
|
139
|
+
"""Turn the gate base into a name git can actually resolve.
|
|
140
|
+
|
|
141
|
+
::
|
|
142
|
+
|
|
143
|
+
"origin/HEAD" on a clone that has it -> "origin/HEAD"
|
|
144
|
+
pushed to "upstream" on such a clone -> "upstream/HEAD"
|
|
145
|
+
"origin/HEAD" on a clone that lacks it -> "origin/main"
|
|
146
|
+
a commit name git does not know -> None
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
base_reference: The gate base drawn from the push's stdin lines.
|
|
150
|
+
remote_name: The remote git named in the hook's arguments.
|
|
151
|
+
ask_git: The seam that runs a git command and reads its output.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
A reference git resolves, or None when no such reference is found.
|
|
155
|
+
"""
|
|
156
|
+
if base_reference != DEFAULT_REMOTE_BASE_REFERENCE:
|
|
157
|
+
return base_reference
|
|
158
|
+
remote_head_reference = REMOTE_BRANCH_SHORT_NAME_TEMPLATE.format(
|
|
159
|
+
remote=remote_name, branch=REMOTE_HEAD_BRANCH_NAME
|
|
160
|
+
)
|
|
161
|
+
if _names_a_commit(remote_head_reference, ask_git):
|
|
162
|
+
return remote_head_reference
|
|
163
|
+
default_branch_reference = _remote_default_branch_reference(remote_name, ask_git)
|
|
164
|
+
if default_branch_reference is None:
|
|
165
|
+
_report_unresolvable_base(remote_head_reference, remote_name)
|
|
166
|
+
return default_branch_reference
|
|
@@ -1,20 +1,5 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import importlib
|
|
4
|
-
import sys
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
SCRIPT_DIRECTORY = Path(__file__).resolve().parent
|
|
9
|
-
_git_hooks_directory_string = str(SCRIPT_DIRECTORY)
|
|
10
|
-
while _git_hooks_directory_string in sys.path:
|
|
11
|
-
sys.path.remove(_git_hooks_directory_string)
|
|
12
|
-
sys.path.insert(0, _git_hooks_directory_string)
|
|
13
|
-
for each_module_name in list(sys.modules):
|
|
14
|
-
if each_module_name == "config" or each_module_name.startswith("config."):
|
|
15
|
-
del sys.modules[each_module_name]
|
|
16
|
-
importlib.invalidate_caches()
|
|
17
|
-
|
|
18
3
|
import git_hooks_constants
|
|
19
4
|
|
|
20
5
|
|
|
@@ -1,23 +1,11 @@
|
|
|
1
|
+
"""Specifications for the gate-script path resolution and its trust check."""
|
|
2
|
+
|
|
1
3
|
from __future__ import annotations
|
|
2
4
|
|
|
3
|
-
import importlib
|
|
4
|
-
import sys
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
import pytest
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
SCRIPT_DIRECTORY = Path(__file__).resolve().parent
|
|
11
|
-
_git_hooks_directory_string = str(SCRIPT_DIRECTORY)
|
|
12
|
-
while _git_hooks_directory_string in sys.path:
|
|
13
|
-
sys.path.remove(_git_hooks_directory_string)
|
|
14
|
-
sys.path.insert(0, _git_hooks_directory_string)
|
|
15
|
-
for each_module_name in list(sys.modules):
|
|
16
|
-
if each_module_name == "config" or each_module_name.startswith("config."):
|
|
17
|
-
del sys.modules[each_module_name]
|
|
18
|
-
importlib.invalidate_caches()
|
|
19
|
-
|
|
20
7
|
import gate_utils
|
|
8
|
+
import pytest
|
|
21
9
|
|
|
22
10
|
|
|
23
11
|
def test_resolve_gate_script_path_uses_override_env_var_when_set(
|
|
@@ -1,23 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import importlib
|
|
4
|
-
import sys
|
|
5
3
|
from pathlib import Path
|
|
6
4
|
|
|
7
|
-
import pytest
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
SCRIPT_DIRECTORY = Path(__file__).resolve().parent
|
|
11
|
-
_git_hooks_directory_string = str(SCRIPT_DIRECTORY)
|
|
12
|
-
while _git_hooks_directory_string in sys.path:
|
|
13
|
-
sys.path.remove(_git_hooks_directory_string)
|
|
14
|
-
sys.path.insert(0, _git_hooks_directory_string)
|
|
15
|
-
for each_module_name in list(sys.modules):
|
|
16
|
-
if each_module_name == "config" or each_module_name.startswith("config."):
|
|
17
|
-
del sys.modules[each_module_name]
|
|
18
|
-
importlib.invalidate_caches()
|
|
19
|
-
|
|
20
5
|
import pre_commit
|
|
6
|
+
import pytest
|
|
21
7
|
|
|
22
8
|
|
|
23
9
|
def make_gate_script_returning(exit_code: int, target_path: Path) -> Path:
|