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.
Files changed (309) hide show
  1. package/CLAUDE.md +26 -59
  2. package/_shared/pr-loop/scripts/_claude_permissions_common.py +84 -0
  3. package/_shared/pr-loop/scripts/code_rules_gate.py +6 -3
  4. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +12 -2
  5. package/_shared/pr-loop/scripts/code_rules_gate_parts/baseline_import_isolation.py +309 -0
  6. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_regression.py +540 -0
  7. package/_shared/pr-loop/scripts/code_rules_gate_parts/staged_test_running.py +206 -70
  8. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/__init__.py +1 -0
  9. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/_repo_test_helpers.py +76 -0
  10. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_baseline_import_isolation.py +248 -0
  11. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_regression.py +309 -0
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/tests/test_staged_test_running.py +91 -58
  13. package/_shared/pr-loop/scripts/grant_project_claude_permissions.py +306 -306
  14. package/_shared/pr-loop/scripts/pr_loop_shared_constants/claude_permissions_constants.py +44 -0
  15. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +202 -0
  16. package/_shared/pr-loop/scripts/pr_loop_shared_constants/copilot_quota_constants.py +24 -24
  17. package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py +107 -107
  18. package/_shared/pr-loop/scripts/revoke_project_claude_permissions.py +290 -48
  19. package/_shared/pr-loop/scripts/tests/test_claude_permissions_common.py +42 -2
  20. package/_shared/pr-loop/scripts/tests/test_claude_permissions_constants.py +36 -0
  21. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +100 -1
  22. package/_shared/pr-loop/scripts/tests/test_fix_hookspath.py +497 -497
  23. package/_shared/pr-loop/scripts/tests/test_revoke_project_claude_permissions.py +311 -2
  24. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep.py +301 -301
  25. package/_shared/pr-loop/scripts/tests/test_stale_worktree_rule_sweep_constants.py +85 -85
  26. package/_shared/pr-loop/worker-spawn.md +1 -1
  27. package/agents/CLAUDE.md +3 -2
  28. package/agents/caveman.md +0 -1
  29. package/agents/clasp-deployment-orchestrator.md +0 -1
  30. package/agents/clean-coder.md +0 -1
  31. package/agents/code-advisor.md +0 -1
  32. package/agents/code-quality-agent.md +1 -2
  33. package/agents/code-verifier.md +36 -8
  34. package/agents/deep-research.md +0 -1
  35. package/agents/docs-agent.md +0 -1
  36. package/agents/git-commit-crafter.md +0 -1
  37. package/agents/issue-tracker.md +42 -0
  38. package/agents/plan-packet-validator.md +0 -1
  39. package/agents/pr-description-writer.md +0 -1
  40. package/agents/test_agent_frontmatter.py +67 -18
  41. package/audit-rubrics/category_rubrics/category-o-docstring-vs-impl-drift.md +143 -141
  42. package/bin/CLAUDE.md +68 -5
  43. package/bin/codex-compat.mjs +104 -0
  44. package/bin/codex-compat.test.mjs +51 -0
  45. package/bin/ever-shipped-skills.mjs +1 -0
  46. package/bin/install-constants.mjs +88 -0
  47. package/bin/install.mjs +1138 -114
  48. package/bin/install.prune.test.mjs +869 -19
  49. package/bin/install.test.mjs +906 -2
  50. package/codex-capability-map.json +13 -0
  51. package/commands/implement.md +1 -1
  52. package/commands/right-size.md +1 -1
  53. package/docs/CLAUDE.md +1 -0
  54. package/docs/CODE_RULES.md +2 -0
  55. package/docs/codex-compatibility.md +25 -0
  56. package/docs/host-pool-health-monitor.md +102 -0
  57. package/docs/nas-ssh-invocation.md +96 -12
  58. package/docs/references/CLAUDE.md +4 -2
  59. package/docs/references/advisor-tool.md +13 -0
  60. package/docs/references/code-review-enforcement.md +35 -0
  61. package/docs/references/team-advisor-skill.md +14 -0
  62. package/hooks/blocking/CLAUDE.md +4 -0
  63. package/hooks/blocking/code_review_pr_create_gate.py +7 -3
  64. package/hooks/blocking/code_review_push_gate.py +9 -4
  65. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +8 -0
  66. package/hooks/blocking/config/__init__.py +5 -5
  67. package/hooks/blocking/config/code_review_enforcement_constants.py +40 -7
  68. package/hooks/blocking/config/test_code_review_enforcement_constants.py +58 -0
  69. package/hooks/blocking/config/verified_commit_constants.py +160 -159
  70. package/hooks/blocking/eli11_reply_enforcer.py +479 -0
  71. package/hooks/blocking/gh_body_arg_blocker.py +1 -1
  72. package/hooks/blocking/nas_ssh_binary_enforcer.py +8 -46
  73. package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +256 -0
  74. package/hooks/blocking/pre_tool_use_dispatcher.py +24 -24
  75. package/hooks/blocking/shell_substitution_blocker.py +129 -0
  76. package/hooks/blocking/state_description_blocker.py +1 -1
  77. package/hooks/blocking/stop_dispatcher.py +1 -1
  78. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +2 -3
  79. package/hooks/blocking/test_code_review_pr_create_gate.py +14 -0
  80. package/hooks/blocking/test_code_review_push_gate.py +16 -0
  81. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +19 -0
  82. package/hooks/blocking/test_eli11_reply_enforcer.py +457 -0
  83. package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +231 -0
  84. package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -1
  85. package/hooks/blocking/test_shell_substitution_blocker.py +124 -0
  86. package/hooks/blocking/test_stop_dispatcher.py +23 -0
  87. package/hooks/blocking/test_unscoped_search_blocker.py +102 -0
  88. package/hooks/blocking/test_verdict_directory_write_blocker.py +804 -808
  89. package/hooks/blocking/test_verification_verdict_store.py +54 -0
  90. package/hooks/blocking/test_verified_commit_gate.py +581 -581
  91. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +131 -131
  92. package/hooks/blocking/unscoped_search_blocker.py +391 -0
  93. package/hooks/blocking/verdict_directory_write_blocker.py +687 -687
  94. package/hooks/blocking/verification_verdict_store.py +1039 -1036
  95. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +167 -167
  96. package/hooks/blocking/verifier_verdict_minter.py +280 -280
  97. package/hooks/git-hooks/CLAUDE.md +3 -0
  98. package/hooks/git-hooks/conftest.py +30 -0
  99. package/hooks/git-hooks/gate_utils.py +2 -2
  100. package/hooks/git-hooks/git_hooks_constants/__init__.py +41 -2
  101. package/hooks/git-hooks/pre_push.py +75 -4
  102. package/hooks/git-hooks/pre_push_base_reference.py +166 -0
  103. package/hooks/git-hooks/test_config.py +0 -15
  104. package/hooks/git-hooks/test_gate_utils.py +3 -15
  105. package/hooks/git-hooks/test_pre_commit.py +1 -15
  106. package/hooks/git-hooks/test_pre_push.py +257 -23
  107. package/hooks/git-hooks/test_pre_push_base_reference.py +339 -0
  108. package/hooks/hooks.json +10 -12
  109. package/hooks/hooks_constants/CLAUDE.md +7 -2
  110. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +4 -4
  111. package/hooks/hooks_constants/eli11_reply_enforcer_constants.py +101 -0
  112. package/hooks/hooks_constants/enter_worktree_prefetch_constants.py +18 -18
  113. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +2 -8
  114. package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +48 -0
  115. package/hooks/hooks_constants/ruff_integration_constants.py +16 -0
  116. package/hooks/hooks_constants/shell_command_segments.py +82 -0
  117. package/hooks/hooks_constants/shell_substitution_blocker_constants.py +67 -0
  118. package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -0
  119. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +5 -6
  120. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +1 -0
  121. package/hooks/hooks_constants/unscoped_search_blocker_constants.py +153 -0
  122. package/hooks/lifecycle/enter_worktree_origin_prefetch.py +163 -146
  123. package/hooks/lifecycle/test_enter_worktree_origin_prefetch.py +185 -178
  124. package/hooks/pyproject.toml +1 -0
  125. package/hooks/validators/CLAUDE.md +1 -0
  126. package/hooks/validators/config/__init__.py +0 -0
  127. package/hooks/validators/config/directory_exemption_constants.py +183 -0
  128. package/hooks/validators/config/test_directory_exemption_constants.py +21 -0
  129. package/hooks/validators/conftest.py +4 -0
  130. package/hooks/validators/ruff_integration.py +49 -5
  131. package/hooks/validators/run_all_validators.py +206 -9
  132. package/hooks/validators/test_directory_exemption_constants.py +185 -0
  133. package/hooks/validators/test_python_antipattern_checks.py +110 -5
  134. package/hooks/validators/test_ruff_integration.py +92 -1
  135. package/hooks/validators/test_run_all_validators.py +115 -68
  136. package/hooks/validators/test_run_all_validators_pretooluse.py +159 -1
  137. package/package.json +13 -3
  138. package/rules/CLAUDE.md +17 -22
  139. package/rules/agent-spawn-protocol.md +6 -6
  140. package/rules/anti-corollary-tests.md +1 -1
  141. package/rules/bdd.md +1 -1
  142. package/rules/cleanup-temp-files.md +10 -4
  143. package/rules/code-standards.md +7 -0
  144. package/rules/conservative-action.md +1 -5
  145. package/rules/context7.md +0 -4
  146. package/rules/destructive-commands.md +47 -0
  147. package/rules/doc-inventory-integrity.md +48 -0
  148. package/rules/doc-prose-cuts.md +58 -0
  149. package/rules/docstring-prose-matches-implementation.md +53 -44
  150. package/rules/durable-post-artifacts.md +0 -4
  151. package/rules/eli11-replies.md +31 -0
  152. package/rules/explore-thoroughly.md +4 -4
  153. package/rules/falsify-before-green.md +68 -0
  154. package/rules/file-global-constants.md +1 -1
  155. package/rules/filesystem-search.md +51 -0
  156. package/rules/gh-cli-conventions.md +27 -0
  157. package/rules/git-workflow.md +26 -0
  158. package/rules/hedging-claims.md +9 -0
  159. package/rules/long-horizon-autonomy.md +0 -4
  160. package/rules/measurement-denominators.md +48 -0
  161. package/rules/nas-ssh-invocation.md +23 -5
  162. package/rules/parallel-tools.md +2 -2
  163. package/rules/plain-illustrative-docstrings.md +3 -7
  164. package/rules/plain-language.md +2 -0
  165. package/rules/proof-of-work-pr-comments.md +0 -4
  166. package/rules/re-stage-before-commit.md +2 -0
  167. package/rules/research-mode.md +10 -0
  168. package/rules/shell-invocation.md +21 -0
  169. package/rules/testing.md +4 -0
  170. package/rules/verified-commit-gate-skip.md +3 -27
  171. package/rules/verify-before-asking.md +5 -0
  172. package/rules/windows-filesystem-safe.md +1 -1
  173. package/rules/workers-done-before-complete.md +4 -0
  174. package/scripts/CLAUDE.md +1 -0
  175. package/scripts/Capture-PoolHealth.ps1 +410 -0
  176. package/scripts/Migrate-ShellPolicy.ps1 +1 -1
  177. package/scripts/_code_review_test_support.py +404 -0
  178. package/scripts/claude_chain_runner.py +141 -1
  179. package/scripts/codex_capability_bridge.py +171 -0
  180. package/scripts/codex_compat_materializer.py +1087 -0
  181. package/scripts/codex_compat_watcher.py +502 -0
  182. package/scripts/conftest.py +16 -1
  183. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
  184. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +9 -0
  185. package/scripts/dev_env_scripts_constants/code_review_constants.py +37 -0
  186. package/scripts/invoke_code_review.py +11 -4
  187. package/scripts/resolve_worker_spawn.py +626 -626
  188. package/scripts/spawn_grok_batch.py +672 -672
  189. package/scripts/sync_to_cursor/rules.py +0 -10
  190. package/scripts/test_claude_chain_runner.py +131 -0
  191. package/scripts/test_invoke_code_review.py +85 -908
  192. package/scripts/test_invoke_code_review_chain.py +70 -0
  193. package/scripts/test_invoke_code_review_cli.py +192 -0
  194. package/scripts/test_invoke_code_review_contract.py +256 -0
  195. package/scripts/test_invoke_code_review_git.py +123 -0
  196. package/scripts/test_invoke_code_review_mode.py +99 -0
  197. package/scripts/test_resolve_worker_spawn.py +1014 -1014
  198. package/scripts/tests/test_code_review_constants.py +80 -0
  199. package/scripts/tests/test_codex_capability_bridge.py +91 -0
  200. package/scripts/tests/test_codex_compat_materializer.py +632 -0
  201. package/scripts/tests/test_codex_compat_watcher.py +599 -0
  202. package/scripts/tests/test_sync_to_cursor.py +0 -1
  203. package/skills/CLAUDE.md +2 -0
  204. package/skills/auditing-claude-config/SKILL.md +114 -114
  205. package/skills/autoconverge/SKILL.md +427 -427
  206. package/skills/autoconverge/reference/convergence.md +24 -3
  207. package/skills/autoconverge/workflow/CLAUDE.md +1 -0
  208. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +3 -3
  209. package/skills/autoconverge/workflow/converge.contract.test.mjs +1263 -1263
  210. package/skills/autoconverge/workflow/converge.mjs +168 -1
  211. package/skills/autoconverge/workflow/converge.p2-advance.test.mjs +202 -0
  212. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +2 -2
  213. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +2 -2
  214. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +2 -2
  215. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +2 -2
  216. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +2 -2
  217. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +2 -2
  218. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +2 -2
  219. package/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +2 -2
  220. package/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +265 -265
  221. package/skills/bugteam/reference/copilot-gap-analysis.md +1 -1
  222. package/skills/closeout/SKILL.md +33 -50
  223. package/skills/codex-review/scripts/codex_review_scripts_constants/run_constants.py +8 -0
  224. package/skills/codex-review/scripts/run_codex_review.py +233 -1
  225. package/skills/codex-review/scripts/test_run_codex_review.py +189 -0
  226. package/skills/condensing-instructions/SKILL.md +72 -0
  227. package/skills/copilot-review/SKILL.md +119 -119
  228. package/skills/e-code-review/SKILL.md +52 -0
  229. package/skills/e-code-review/reference/fix.md +54 -0
  230. package/skills/e-code-review/reference/loop.md +43 -0
  231. package/skills/e-code-review/reference/low.md +57 -0
  232. package/skills/e-code-review/reference/medium.md +153 -0
  233. package/skills/e-code-review/reference/xhigh.md +182 -0
  234. package/skills/e-simplify/SKILL.md +97 -0
  235. package/skills/fresh-branch/CLAUDE.md +1 -1
  236. package/skills/fresh-branch/SKILL.md +5 -6
  237. package/skills/fresh-branch/scripts/create_fresh_branch.py +42 -24
  238. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +1 -3
  239. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +30 -126
  240. package/skills/issue-tracker/SKILL.md +92 -0
  241. package/skills/issue-tracker/reference/epic-and-sub-issue-model.md +55 -0
  242. package/skills/issue-tracker/reference/handoff-schema.md +64 -0
  243. package/skills/issue-tracker/reference/operation-matrix.md +41 -0
  244. package/skills/orchestrator/SKILL.md +177 -22
  245. package/skills/orchestrator/scripts/status_gate.py +625 -0
  246. package/skills/orchestrator/scripts/status_gate_constants/__init__.py +1 -0
  247. package/skills/orchestrator/scripts/status_gate_constants/config/__init__.py +1 -0
  248. package/skills/orchestrator/scripts/status_gate_constants/config/constants.py +47 -0
  249. package/skills/orchestrator/scripts/test_status_gate.py +439 -0
  250. package/skills/orchestrator-refresh/SKILL.md +129 -35
  251. package/skills/plan-to-pr/SKILL.md +155 -0
  252. package/skills/plan-to-pr/reference/final-validation-tasks.md +15 -0
  253. package/skills/plan-to-pr/reference/model-routing.md +36 -0
  254. package/skills/plan-to-pr/reference/packet-contract.md +43 -0
  255. package/skills/plan-to-pr/reference/packet-schema.json +57 -0
  256. package/skills/plan-to-pr/reference/process-inventory.md +22 -0
  257. package/skills/plan-to-pr/reference/review-loop.md +33 -0
  258. package/skills/plan-to-pr/reference/run-record.schema.json +27 -0
  259. package/skills/plan-to-pr/reference/self-audit-tasks.md +15 -0
  260. package/skills/plan-to-pr/reference/task-seeds.md +14 -0
  261. package/skills/plan-to-pr/reference/task-ticket.md +38 -0
  262. package/skills/plan-to-pr/scripts/config/__init__.py +1 -0
  263. package/skills/plan-to-pr/scripts/config/constants.py +193 -0
  264. package/skills/plan-to-pr/scripts/create_packet.py +173 -0
  265. package/skills/plan-to-pr/scripts/test_create_packet.py +102 -0
  266. package/skills/plan-to-pr/scripts/test_validate_packet.py +256 -0
  267. package/skills/plan-to-pr/scripts/test_validate_protocol.py +135 -0
  268. package/skills/plan-to-pr/scripts/test_validate_run.py +158 -0
  269. package/skills/plan-to-pr/scripts/validate_packet.py +655 -0
  270. package/skills/plan-to-pr/scripts/validate_protocol.py +622 -0
  271. package/skills/plan-to-pr/scripts/validate_run.py +173 -0
  272. package/skills/plan-to-pr/test_skill_contract.py +207 -0
  273. package/skills/plan-to-pr/test_task_ticket_contract.py +151 -0
  274. package/skills/pr-converge/SKILL.md +472 -469
  275. package/skills/pr-converge/reference/examples.md +3 -3
  276. package/skills/pr-converge/reference/fix-protocol.md +1 -1
  277. package/skills/pr-converge/reference/ground-rules.md +7 -4
  278. package/skills/pr-converge/reference/multi-pr-orchestration.md +4 -1
  279. package/skills/pr-converge/reference/per-tick.md +5 -5
  280. package/skills/pr-converge/reference/progress-checklist.md +1 -1
  281. package/skills/pr-converge/scripts/check_convergence_gates.py +279 -279
  282. package/skills/pr-converge/scripts/test_check_convergence_codex.py +507 -507
  283. package/skills/pr-converge/scripts/test_check_convergence_gates.py +84 -84
  284. package/skills/pr-converge/test_step5_host_branch.py +1 -1
  285. package/skills/pr-fix-protocol/SKILL.md +1 -1
  286. package/skills/privacy-hygiene/SKILL.md +68 -68
  287. package/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
  288. package/skills/prototype/workflows/promotion.md +1 -1
  289. package/skills/release-notes-html/SKILL.md +164 -0
  290. package/skills/session-log/SKILL.md +1 -1
  291. package/skills/task-build/CLAUDE.md +8 -7
  292. package/skills/task-build/SKILL.md +16 -8
  293. package/skills/task-build/reference/tool-routing.md +19 -0
  294. package/rules/claude-md-orphan-file.md +0 -28
  295. package/rules/cleanup-command-forms.md +0 -23
  296. package/rules/code-reviews.md +0 -11
  297. package/rules/env-var-table-code-drift.md +0 -10
  298. package/rules/gh-body-file.md +0 -5
  299. package/rules/gh-paginate.md +0 -3
  300. package/rules/hook-prose-matches-detector.md +0 -15
  301. package/rules/no-historical-clutter.md +0 -26
  302. package/rules/no-inline-destructive-literals.md +0 -9
  303. package/rules/no-justification-noise.md +0 -61
  304. package/rules/package-inventory-stale-entry.md +0 -25
  305. package/rules/right-sized-engineering.md +0 -28
  306. package/rules/self-contained-docs.md +0 -17
  307. package/rules/shell-invocation-policy.md +0 -5
  308. package/rules/tdd.md +0 -7
  309. 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
- GATE_SCRIPT_RELATIVE_PATH: tuple[str, ...] = (
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 remote's default branch symbolic ref.
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 gate_utils import is_safe_regular_file, resolve_gate_script_path
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
- code_rules_exit_code = invoke_gate(gate_script_path, base_reference)
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: