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
package/CLAUDE.md CHANGED
@@ -1,84 +1,51 @@
1
- # Claude Development Assistant
1
+ # Development Assistant
2
2
 
3
- The user is short on time and appreciates brevity in replies. When you reply, always assume they'll only read your first few sentences and final sentences. Anything else is skimmed at best. Frame your replies accordingly.
3
+ ## Communication
4
4
 
5
- The user delegates execution to you and expects zero manual steps unless strictly necessary. Execute every command you can directly. Only instruct the user to do something manually when you are technically unable to do it yourself. When a task involves credentials or other sensitive input, display a minimal secure UI (e.g., a password dialog) to collect it rather than asking the user to paste it into chat or run the command themselves. When direction is ambiguous, use AskUserQuestion to clarify before acting.
5
+ Reply shape and length: follow `~/.claude/rules/eli11-replies.md`. Word choice: follow `~/.claude/rules/plain-language.md`. State claims affirmatively.
6
6
 
7
- ## Timeless Documentation (all `.md` files)
7
+ ## Security
8
8
 
9
- Docs describe the current state only. Full rule set and enforcement: `~/.claude/rules/no-historical-clutter.md` (hook `state-description-blocker`) and `~/.claude/rules/self-contained-docs.md`.
9
+ Collect credentials through secure UI only; never request secrets in chat.
10
10
 
11
- ## Private values stay out of the committed tree
11
+ A runtime value that is itself private — a host, an SSH user or port, an owner scope, an account ID — lives in git-ignored local configuration with a committed placeholder in its place. Source files never carry the real value.
12
12
 
13
- A value the code needs at run time that is itself private — a host, ssh user or port, owner scope, or id — lives in git-ignored local config with a committed placeholder, not in the source.
13
+ ## Advisors
14
14
 
15
- ## Choosing Edit vs Write
15
+ When the `advisor()` tool is available, read `~/.claude/docs/references/advisor-tool.md`. For complex tasks use the `/team-advisor` skill.
16
16
 
17
- `Edit` changes existing files; `Write` creates new ones. Default to `Edit` — reach for `Write` only for a genuinely new path. For a true full rewrite, delete the file first, then `Write`.
17
+ ## Files and workspaces
18
18
 
19
- ## Showing Files: Open Them, Don't Print the Path
19
+ Put all work in an isolated worktree under the repo's `.claude/worktrees/`.
20
20
 
21
- When I ask you to "show me", "open", "display", "let me see", or "pull up" a file an image, PDF, HTML page, document, anything — open it on my screen. Launch the viewer so each image window matches the asset's size:
21
+ Default to Edit for existing files; reach for Write only when the path is genuinely new.
22
22
 
23
- `Start-Process pwsh -WindowStyle Hidden -ArgumentList '-NoProfile','-File',"$HOME\.claude\scripts\Show-Asset.ps1",'<path 1>','<path 2>'`
23
+ ## Code and tests
24
24
 
25
- ## Test Philosophy
25
+ Tests must exercise real behavior, real data, and production paths.
26
26
 
27
- When writing tests, always write tests that actually test the behavior of the function against actual, real data and environments.
27
+ Keep changes within scope. Prefer durable systemic fixes for reusable behavior.
28
28
 
29
- When writing tests, always ensure you utilize the production code paths instead of duplicating explicitly for the test.
29
+ Do not rewrite entire files or rename public parameters without need.
30
30
 
31
- ## Research via Subagents
31
+ ## Reviews
32
32
 
33
- Delegate exploration whose raw content you won't directly edit or reuse. If you'd `Read` more than one file or `Grep` more than one pattern just to extract a fact, dispatch an `Explore` subagent.
33
+ Verify every sub-agent file list, count, description, and finding against the repository and diff.
34
34
 
35
- Ask the subagent for a specific answer: "return the file:line where X is defined." For multiple unrelated questions, fan out parallel subagents — issue several `Agent` calls in a single response.
35
+ Do not commit untracked files unless explicitly instructed.
36
36
 
37
- Reserve `Read`/`Grep`/`Glob` for files you will actually touch this turn. Compose subagent prompts via the protocol in `agent-spawn-protocol`.
37
+ ## Delegation
38
38
 
39
- ## Target Execution Workflow for Code Tasks
39
+ Request precise file-and-line answers from research subagents.
40
40
 
41
- Run every multi-step code task in three ordered phases:
41
+ ## Task tracking
42
42
 
43
- 1. **Coders** one coder agent per scoped assignment writes the code. A coder that hits a decision it can't reasonably solve consults the advisor (see beginning of this file).
44
- 2. **Commit-gate preflight** — before spawning `code-verifier`, the branch must be merge-clean vs base and CODE_RULES-clean on added working-tree lines (committed on the branch or uncommitted). The `code_verifier_spawn_preflight_gate` enforces this on Agent/Task spawns with `subagent_type` `code-verifier`; do not spawn the verifier until those gates are green.
45
- 3. **Verification** — the main session spawns the `code-verifier` agent in a fresh context, but you must first verify that their work is based on upstream's origin main (aka: the commit live on github). It derives and runs the checks itself rather than trusting coder reports: the task's named gates, tests against baselines recorded before the coders ran, and a two-way diff-vs-assignment reading (every task item maps to a hunk, every hunk maps to a task item, nothing missing). A finding must cite a failing command or a named task item. Before it emits the verdict, it puts the draft through one strongest-tier validation subagent — selected per the advisor protocol's host detection and tier ladder — that tries to refute it, and it re-checks and corrects any part the validator overturns. Source: the fresh-context review step in Claude Code best practices (https://code.claude.com/docs/en/best-practices) — the agent doing the work isn't the one grading it.
43
+ Track multi-step work with the `task-build` skill.
46
44
 
47
- Repair agents run only on reported findings; the verifier re-checks after each repair. Work lands (commit, push, draft PR) only on a clean verdict — enforced by the `verified_commit_gate` hook, which blocks `git commit`/`git push` unless a hook-minted verdict covers the current branch diff. One exemption is mechanical, not discretionary: a diff whose every changed file is non-code (docs, images), a pytest test file by name (`test_*.py`, `*_test.py`, or `conftest.py`), or a Python file whose docstring-stripped AST is unchanged (docstring, comment, or formatting-only edits). One escape hatch is manual and narrow: appending `# verify-skip` as a trailing shell comment (outside every quoted region) to the blocked commit or push command bypasses the gate for that one command — allowed only when the branch surface is the same code a code-verifier already passed clean and the gate is blocking on a verdict that doesn't cover it (an unminted verdict, staging churn, a reverted concurrent write); any real code change since the clean verdict runs a fresh verification instead. Full rule: `~/.claude/rules/verified-commit-gate-skip.md`.
45
+ ## Repository rule
48
46
 
49
- ## Sub-agent Output Validation
47
+ Before changing skill, rule, or hook installation in the claude-dev-env repo, read `docs/references/skill-install-system.md`.
50
48
 
51
- After any sub-agent returns a PR description, file list, or counts, verify each claim against the actual diff and repo state before using it. Flag and correct any invented paths, fabricated counts, or out-of-scope changes before they land in commits or PR bodies.
49
+ ## Definitions
52
50
 
53
- ## Task Tracking
54
-
55
- Track every task with the task tool, always — for all sessions and all tasks. Capture each task with `TaskCreate` as it arrives, mark it `in_progress` with `TaskUpdate` when you start, and `completed` when it is done. Run `/task-build` to gather any open tasks and add them to the list in one pass.
56
-
57
- ## Working in the claude-dev-env Repo
58
-
59
- When changing how skills, rules, or hooks install or sync in this repo (for example adding a skill), read `docs/references/skill-install-system.md` — it maps the install pipeline in `packages/claude-dev-env/bin/install.mjs`.
60
-
61
- ## Additional Non-overlapping Rules
62
-
63
- - **disambiguate_overloaded_terms:** When a word in the request has two different technical meanings — "conflict" (git-merge versus functional/behavioral), "sync" (fast-forward versus commit), and the like — confirm which one is meant via AskUserQuestion before analyzing or acting.
64
-
65
- ## Serena (Code Intelligence MCP)
66
-
67
- The `mcp__serena__*` tools expose LSP-level code intelligence for any activated project.
68
-
69
- ### CRITICAL: Call `initial_instructions` first
70
- Before any coding task, call the `initial_instructions` tool to load the Serena Instructions Manual.
71
-
72
- ### When to use Serena
73
- - **Symbol declaration** → `mcp__serena__find_declaration`
74
- - **All references to a symbol** → `mcp__serena__find_referencing_symbols`
75
- - **Implementations of an interface/class** → `mcp__serena__find_implementations`
76
- - **Rename across codebase** → `mcp__serena__rename_symbol`
77
- - **Targeted body replacement / insertion** → `replace_symbol_body`, `insert_after_symbol`, `insert_before_symbol`
78
- - **Safe symbol removal (no references)** → `mcp__serena__safe_delete_symbol`
79
- - **File diagnostics** → `mcp__serena__get_diagnostics_for_file`
80
-
81
- ### Tool hierarchy for code navigation
82
- 1. **Serena** — symbol-level navigation (declarations, references, implementations, rename)
83
- 2. **es.exe** — file-system search by name/path/extension/size/date (Everything CLI)
84
- 3. **Grep/Glob** — content and pattern matching
51
+ Warm agent: active within the past 59 minutes.
@@ -18,9 +18,11 @@ from pathlib import Path
18
18
  from typing import NoReturn
19
19
 
20
20
  from pr_loop_shared_constants.claude_permissions_constants import (
21
+ ALL_INERT_FILE_PERMISSION_TOOLS,
21
22
  ALL_TRUST_ENTRY_PROJECT_PATH_BOUNDARY_QUOTE_CHARACTERS,
22
23
  CLAUDE_SETTINGS_DIRECTORY_NAME,
23
24
  GIT_DIRECTORY_NAME,
25
+ HOME_PROJECT_PATH_ALIAS,
24
26
  TEXT_FILE_ENCODING as TEXT_FILE_ENCODING,
25
27
  UNIQUE_TEMPORARY_SUFFIX_BYTE_LENGTH,
26
28
  )
@@ -80,6 +82,88 @@ def get_current_project_path() -> str:
80
82
  return normalized_project_path
81
83
 
82
84
 
85
+ def permission_rule_tool_name(permission_rule: object) -> str | None:
86
+ """Return the tool name prefix of a Claude permission rule, or None.
87
+
88
+ ::
89
+
90
+ permission_rule_tool_name("Edit(/repo/.claude/**)") # ok: "Edit"
91
+ permission_rule_tool_name(42) # ok: None
92
+
93
+ Args:
94
+ permission_rule: A Claude settings allow/deny rule value.
95
+
96
+ Returns:
97
+ The tool name before the first ``(``, or None when the value is not a
98
+ well-formed rule string.
99
+ """
100
+ if not isinstance(permission_rule, str):
101
+ return None
102
+ opening_parenthesis_index = permission_rule.find("(")
103
+ if opening_parenthesis_index <= 0:
104
+ return None
105
+ return permission_rule[:opening_parenthesis_index]
106
+
107
+
108
+ def is_inert_file_permission_rule(permission_rule: object) -> bool:
109
+ """Return True when a rule uses Write/Glob/NotebookEdit (ignored for files).
110
+
111
+ ::
112
+
113
+ is_inert_file_permission_rule("Write(/wt/.claude/**)") # ok: True
114
+ is_inert_file_permission_rule("Edit(/repo/.claude/**)") # ok: False
115
+
116
+ Args:
117
+ permission_rule: A Claude settings allow/deny rule value.
118
+
119
+ Returns:
120
+ True when the rule's tool is in ALL_INERT_FILE_PERMISSION_TOOLS.
121
+ """
122
+ tool_name = permission_rule_tool_name(permission_rule)
123
+ if tool_name is None:
124
+ return False
125
+ return tool_name in ALL_INERT_FILE_PERMISSION_TOOLS
126
+
127
+
128
+ def all_project_path_aliases_for_reap(
129
+ project_path: str,
130
+ home_directory_path: str | None = None,
131
+ ) -> tuple[str, ...]:
132
+ """Return project path forms a revoke run should match when removing rules.
133
+
134
+ ::
135
+
136
+ all_project_path_aliases_for_reap("/repo") # ok: ("/repo",)
137
+ all_project_path_aliases_for_reap("/home/u", home_directory_path="/home/u")
138
+ # ok: ("/home/u", "$HOME")
139
+
140
+ Always includes the absolute POSIX project path. When that path is the
141
+ user home directory, also includes ``$HOME`` so grant shapes like
142
+ ``Edit($HOME/.claude/**)`` are reaped from home cwd.
143
+
144
+ Args:
145
+ project_path: POSIX-style project root path from get_current_project_path.
146
+ home_directory_path: Optional home path for tests; defaults to Path.home().
147
+
148
+ Returns:
149
+ Ordered unique path aliases used when building project-scoped remove rules.
150
+ """
151
+ normalized_project_path = project_path.replace("\\", "/").rstrip("/")
152
+ resolved_home_directory_path = (
153
+ home_directory_path
154
+ if home_directory_path is not None
155
+ else str(Path.home())
156
+ ).replace("\\", "/").rstrip("/")
157
+ all_path_aliases: list[str] = [normalized_project_path]
158
+ if normalized_project_path.lower() == resolved_home_directory_path.lower():
159
+ all_path_aliases.append(HOME_PROJECT_PATH_ALIAS)
160
+ all_unique_path_aliases: list[str] = []
161
+ for each_path_alias in all_path_aliases:
162
+ if each_path_alias not in all_unique_path_aliases:
163
+ all_unique_path_aliases.append(each_path_alias)
164
+ return tuple(all_unique_path_aliases)
165
+
166
+
83
167
  def build_permission_rule(tool_name: str, project_path: str) -> str:
84
168
  return f"{tool_name}({project_path}/.claude/**)"
85
169
 
@@ -32,6 +32,7 @@ try:
32
32
  gate_running,
33
33
  git_blob_readers,
34
34
  git_file_sets,
35
+ staged_test_regression,
35
36
  staged_test_running,
36
37
  violation_scoping,
37
38
  wrapper_plumb_check,
@@ -136,7 +137,7 @@ def _report_partitioned_violations(
136
137
  is_whole_file_scope,
137
138
  )
138
139
 
139
- run_staged_test_files = staged_test_running.run_staged_test_files
140
+ run_staged_test_files = staged_test_regression.run_staged_test_files
140
141
  _staged_test_file_paths = staged_test_running._staged_test_file_paths
141
142
  _resolve_owning_test_root = staged_test_running._resolve_owning_test_root
142
143
  _group_staged_tests_by_root = staged_test_running._group_staged_tests_by_root
@@ -296,8 +297,9 @@ def _run_diff_mode(
296
297
  """Validate the merge-base diff joined with the untracked files.
297
298
 
298
299
  Zero candidates means nothing was inspected (bad wiring looks the same),
299
- so that run refuses loudly. A set emptied only by the ``--only-under``
300
- scope flows through ``run_gate`` over zero files and exits clean.
300
+ so that run refuses loudly. A set emptied by the ``--only-under`` scope or
301
+ left with no code files after the code-path filter flows through
302
+ ``run_gate`` over zero files and exits clean.
301
303
  """
302
304
  all_candidate_paths = _deduplicate_paths(
303
305
  paths_from_git_diff(repository_root, arguments.base)
@@ -308,6 +310,7 @@ def _run_diff_mode(
308
310
  file_paths = filter_paths_under_prefixes(
309
311
  all_candidate_paths, repository_root, arguments.only_under
310
312
  )
313
+ file_paths = [each_path for each_path in file_paths if is_code_path(each_path)]
311
314
  scoped_added_lines = (
312
315
  added_lines_by_file(repository_root, arguments.base, file_paths) if file_paths else {}
313
316
  )
@@ -15,15 +15,25 @@ surface for the test suite.
15
15
  | `violation_scoping.py` | Recovers a violation's line span from the enforcer message and partitions violations into blocking versus advisory |
16
16
  | `wrapper_plumb_check.py` | Flags a public wrapper that drops a same-file delegate's optional keyword arguments; holds the code-path and test-path classifiers |
17
17
  | `gate_running.py` | Validates the eligible file set, reports the inspected-file count, and prints the partitioned violation report |
18
- | `staged_test_running.py` | Runs the staged Python test files, grouped by their owning pytest config, in command-line-length-safe batches |
18
+ | `staged_test_running.py` | Runs one staged-test group, grouped by its owning pytest config and by top-level directory when no config owns it, in command-line-length-safe pytest batches, optionally writing a JUnit XML report per batch |
19
+ | `staged_test_regression.py` | Discovers the staged test files, runs every group, and blocks only on a failure the staged change introduces — a group that fails is re-run against the HEAD baseline (a throwaway detached checkout made with `git worktree add --detach` under the OS temp root, deleted with `git worktree remove --force`) and only a failure absent there blocks |
20
+ | `baseline_import_isolation.py` | Points the baseline run's imports at the baseline worktree — rebases every repository import root onto the front of its `PYTHONPATH` — and installs the pytest plugin that reports which modules the run loaded out of the user's own tree, so a route that survives the rebase is measured and the baseline discarded |
19
21
  | `gate_arguments.py` | Parses the gate's command-line arguments |
20
22
  | `__init__.py` | Package marker |
21
23
 
24
+ ## Staged-test grouping boundaries
25
+
26
+ The top-level fallback in `staged_test_running.py` fires only in a repository
27
+ whose root holds no pytest config, and it splits top-level neighbors alone:
28
+
29
+ - Config-less packages that sit deeper under one shared top-level directory, such as `pkgs/alpha` and `pkgs/beta`, share that directory's single pytest session, so a module name they both expose still shadows.
30
+ - A `conftest.py` at the repository root stays unloaded for those groups, because each session works from the top-level directory and the root file falls outside the tree pytest treats as its root.
31
+
22
32
  ## Subdirectory
23
33
 
24
34
  | Entry | Description |
25
35
  |---|---|
26
- | `tests/` | pytest suite with one test module per module above |
36
+ | `tests/` | pytest suite with one test module per module above, plus `_repo_test_helpers.py` (shared real-git-repository builders) |
27
37
 
28
38
  ## Running tests
29
39
 
@@ -0,0 +1,309 @@
1
+ """Make the HEAD baseline run import from the baseline worktree, and prove that it did.
2
+
3
+ ::
4
+
5
+ repository_root = /repo (working tree, staged edits)
6
+ baseline_worktree = /tmp/base/tree (detached HEAD, clean)
7
+
8
+ ok: PYTHONPATH=/repo/packages -> PYTHONPATH=/tmp/base/tree/packages
9
+ ok: editable root /repo/src -> /tmp/base/tree/src prepended
10
+ flag: import hook ahead of the path scan still serving /repo/src/foo.py
11
+ -> reported by the leak plugin, baseline discarded
12
+
13
+ Rebasing the working directory alone does not move imports. An absolute route
14
+ into the user's own checkout keeps serving staged code to the baseline run.
15
+ Two shapes reach it: a ``PYTHONPATH`` entry under the repository root, and an
16
+ editable install pointing at it. The baseline then fails the way the staged run
17
+ failed, and the two failures cancel a real regression out.
18
+
19
+ Two halves close that. Every import root inside the repository is rewritten to
20
+ its baseline-worktree equivalent and put at the front of ``PYTHONPATH``. That
21
+ beats every ``.pth`` entry and every setuptools editable style. Then the
22
+ baseline pytest session loads a plugin that records which modules it imported
23
+ out of the user's tree. A route that survives the rewrite is measured, never
24
+ assumed.
25
+ """
26
+
27
+ from __future__ import annotations
28
+
29
+ import json
30
+ import os
31
+ import subprocess
32
+ import sys
33
+ from pathlib import Path
34
+
35
+ from pr_loop_shared_constants.code_rules_gate_constants import (
36
+ BASELINE_IMPORT_PROBE_REJECTED_MESSAGE,
37
+ BASELINE_IMPORT_PROBE_TIMED_OUT_MESSAGE,
38
+ BASELINE_IMPORT_PROBE_TIMEOUT_SECONDS,
39
+ BASELINE_IMPORT_PROBE_UNSTARTABLE_MESSAGE,
40
+ BASELINE_IMPORT_ROOT_PROBE_SOURCE,
41
+ BASELINE_LEAK_PLUGIN_MODULE_NAME,
42
+ BASELINE_LEAK_PLUGIN_SOURCE,
43
+ BASELINE_LEAK_REPORT_ENV_VAR,
44
+ BASELINE_PRIMARY_ROOT_ENV_VAR,
45
+ PYTEST_PLUGINS_ENV_VAR,
46
+ PYTEST_PLUGINS_SEPARATOR,
47
+ PYTHON_FILE_EXTENSION,
48
+ PYTHON_INTERPRETER_COMMAND_FLAG,
49
+ PYTHONPATH_ENV_VAR,
50
+ )
51
+
52
+
53
+ def _resolved_directory(path_text: str) -> Path | None:
54
+ """Return *path_text* resolved to an absolute path, or None when it is unusable."""
55
+ if not path_text:
56
+ return None
57
+ try:
58
+ return Path(path_text).resolve()
59
+ except OSError:
60
+ return None
61
+
62
+
63
+ def _is_under(candidate: Path, root: Path) -> bool:
64
+ """Return True when *candidate* is *root* itself or sits anywhere beneath it."""
65
+ return candidate == root or root in candidate.parents
66
+
67
+
68
+ def rebased_into_baseline(
69
+ original_path: Path, repository_root: Path, baseline_worktree: Path
70
+ ) -> Path | None:
71
+ """Return *original_path* moved into the baseline worktree, or None when outside the repo.
72
+
73
+ ::
74
+
75
+ repository_root = /repo
76
+ ok: /repo/packages -> /tmp/base/tree/packages
77
+ ok: /repo -> /tmp/base/tree
78
+ flag: /elsewhere/lib -> None (nothing in the baseline tree answers for it)
79
+
80
+ Args:
81
+ original_path: An already-resolved filesystem path.
82
+ repository_root: The user's own repository root.
83
+ baseline_worktree: The detached-HEAD worktree the baseline run uses.
84
+
85
+ Returns:
86
+ The same repository-relative location inside *baseline_worktree*, or
87
+ None when *original_path* lies outside the repository.
88
+ """
89
+ if not _is_under(original_path, repository_root):
90
+ return None
91
+ return baseline_worktree / original_path.relative_to(repository_root)
92
+
93
+
94
+ def discover_import_roots(
95
+ python_executable: str,
96
+ all_environment_settings: dict[str, str],
97
+ working_directory: Path,
98
+ ) -> list[Path]:
99
+ """Return every directory the interpreter would resolve imported packages from.
100
+
101
+ Covers ``sys.path`` — which carries ``PYTHONPATH`` and every ``.pth``-added
102
+ entry — plus the package roots an editable install registers through an
103
+ import hook, which never appear on ``sys.path`` at all.
104
+
105
+ Args:
106
+ python_executable: The interpreter the staged and baseline runs use.
107
+ all_environment_settings: The environment the probe runs under.
108
+ working_directory: A directory outside the repository, so the probe's
109
+ own working directory never reads as a repository import root.
110
+
111
+ Returns:
112
+ The resolved import roots, or an empty list when the probe times out,
113
+ cannot start, or exits non-zero. Each of those says so on stderr, and
114
+ the run continues into the baseline with its leak report still armed.
115
+ """
116
+ probe = _completed_import_root_probe(
117
+ python_executable, all_environment_settings, working_directory
118
+ )
119
+ if probe is None:
120
+ return []
121
+ if probe.returncode != 0:
122
+ sys.stderr.write(
123
+ BASELINE_IMPORT_PROBE_REJECTED_MESSAGE.format(status=probe.returncode) + "\n"
124
+ )
125
+ return []
126
+ return _probed_roots(probe.stdout)
127
+
128
+
129
+ def _completed_import_root_probe(
130
+ python_executable: str,
131
+ all_environment_settings: dict[str, str],
132
+ working_directory: Path,
133
+ ) -> subprocess.CompletedProcess[str] | None:
134
+ """Run the probe interpreter to completion, or report on stderr why it produced nothing.
135
+
136
+ ::
137
+
138
+ ok: probe exits 0 -> CompletedProcess carrying its output
139
+ flag: probe hangs -> None, "did not finish within 120 seconds"
140
+ flag: interpreter missing -> None, "did not start (...)"
141
+
142
+ A probe that outlives its limit is the same situation as a probe that fails:
143
+ no roots were learned, so the caller carries on without them rather than
144
+ raising a traceback out of a pre-commit hook.
145
+ """
146
+ try:
147
+ return subprocess.run(
148
+ [python_executable, PYTHON_INTERPRETER_COMMAND_FLAG, BASELINE_IMPORT_ROOT_PROBE_SOURCE],
149
+ cwd=str(working_directory),
150
+ env=all_environment_settings,
151
+ capture_output=True,
152
+ text=True,
153
+ check=False,
154
+ timeout=BASELINE_IMPORT_PROBE_TIMEOUT_SECONDS,
155
+ )
156
+ except subprocess.TimeoutExpired:
157
+ sys.stderr.write(
158
+ BASELINE_IMPORT_PROBE_TIMED_OUT_MESSAGE.format(
159
+ seconds=BASELINE_IMPORT_PROBE_TIMEOUT_SECONDS
160
+ )
161
+ + "\n"
162
+ )
163
+ return None
164
+ except OSError as probe_failure:
165
+ sys.stderr.write(
166
+ BASELINE_IMPORT_PROBE_UNSTARTABLE_MESSAGE.format(reason=probe_failure) + "\n"
167
+ )
168
+ return None
169
+
170
+
171
+ def _probed_roots(probe_text: str) -> list[Path]:
172
+ """Return the resolved directories named on the probe's final output line."""
173
+ all_probe_lines = probe_text.strip().splitlines()
174
+ if not all_probe_lines:
175
+ return []
176
+ try:
177
+ all_root_texts = json.loads(all_probe_lines[-1])
178
+ except ValueError:
179
+ return []
180
+ all_roots = [_resolved_directory(str(each_text)) for each_text in all_root_texts]
181
+ return [each_root for each_root in all_roots if each_root is not None]
182
+
183
+
184
+ def _baseline_path_entries(
185
+ all_import_roots: list[Path], repository_root: Path, baseline_worktree: Path
186
+ ) -> list[str]:
187
+ """Return the baseline-worktree equivalent of every import root inside the repository."""
188
+ all_entries: list[str] = []
189
+ for each_root in all_import_roots:
190
+ rebased_root = rebased_into_baseline(each_root, repository_root, baseline_worktree)
191
+ if rebased_root is not None:
192
+ all_entries.append(str(rebased_root))
193
+ return all_entries
194
+
195
+
196
+ def _rebased_pythonpath_entries(
197
+ all_staged_environment_settings: dict[str, str],
198
+ repository_root: Path,
199
+ baseline_worktree: Path,
200
+ ) -> list[str]:
201
+ """Return the staged ``PYTHONPATH`` with every repository entry moved into the baseline."""
202
+ all_entries: list[str] = []
203
+ staged_pythonpath = all_staged_environment_settings.get(PYTHONPATH_ENV_VAR, "")
204
+ for each_entry in staged_pythonpath.split(os.pathsep):
205
+ resolved_entry = _resolved_directory(each_entry)
206
+ rebased_entry = (
207
+ None
208
+ if resolved_entry is None
209
+ else rebased_into_baseline(resolved_entry, repository_root, baseline_worktree)
210
+ )
211
+ all_entries.append(each_entry if rebased_entry is None else str(rebased_entry))
212
+ return [each_entry for each_entry in all_entries if each_entry]
213
+
214
+
215
+ def install_leak_plugin(plugin_directory: Path) -> None:
216
+ """Write the import-origin reporting pytest plugin into its own directory.
217
+
218
+ Args:
219
+ plugin_directory: The directory the plugin module is written to, which
220
+ the baseline run puts on its ``PYTHONPATH`` so pytest loads it.
221
+ """
222
+ plugin_directory.mkdir(parents=True, exist_ok=True)
223
+ plugin_path = plugin_directory / f"{BASELINE_LEAK_PLUGIN_MODULE_NAME}{PYTHON_FILE_EXTENSION}"
224
+ plugin_path.write_text(
225
+ BASELINE_LEAK_PLUGIN_SOURCE.format(
226
+ primary_root_env_var=BASELINE_PRIMARY_ROOT_ENV_VAR,
227
+ report_path_env_var=BASELINE_LEAK_REPORT_ENV_VAR,
228
+ ),
229
+ encoding="utf-8",
230
+ )
231
+
232
+
233
+ def _pytest_plugins_setting(all_staged_environment_settings: dict[str, str]) -> str:
234
+ """Return the ``PYTEST_PLUGINS`` value with the leak plugin added to any existing list."""
235
+ existing_setting = all_staged_environment_settings.get(PYTEST_PLUGINS_ENV_VAR, "")
236
+ if not existing_setting:
237
+ return BASELINE_LEAK_PLUGIN_MODULE_NAME
238
+ return PYTEST_PLUGINS_SEPARATOR.join([BASELINE_LEAK_PLUGIN_MODULE_NAME, existing_setting])
239
+
240
+
241
+ def baseline_pytest_environment(
242
+ all_staged_environment_settings: dict[str, str],
243
+ repository_root: Path,
244
+ baseline_worktree: Path,
245
+ all_import_roots: list[Path],
246
+ plugin_directory: Path,
247
+ leak_report_path: Path,
248
+ ) -> dict[str, str]:
249
+ """Return the environment the baseline pytest run uses.
250
+
251
+ Every repository import root leads with its baseline-worktree equivalent,
252
+ the plugin directory follows, and the staged ``PYTHONPATH`` (itself rebased
253
+ entry by entry) trails behind, so a module that exists at HEAD always
254
+ resolves out of the baseline tree.
255
+
256
+ Args:
257
+ all_staged_environment_settings: The environment the staged run used.
258
+ repository_root: The user's own repository root.
259
+ baseline_worktree: The detached-HEAD worktree the baseline run uses.
260
+ all_import_roots: The import roots ``discover_import_roots`` found.
261
+ plugin_directory: The directory holding the leak-reporting plugin.
262
+ leak_report_path: Where the plugin writes its import-origin report.
263
+
264
+ Returns:
265
+ A copy of *all_staged_environment_settings* carrying the baseline
266
+ import path and the leak plugin's three settings.
267
+ """
268
+ baseline_environment = dict(all_staged_environment_settings)
269
+ all_entries = [
270
+ *_baseline_path_entries(all_import_roots, repository_root, baseline_worktree),
271
+ str(plugin_directory),
272
+ *_rebased_pythonpath_entries(
273
+ all_staged_environment_settings, repository_root, baseline_worktree
274
+ ),
275
+ ]
276
+ baseline_environment[PYTHONPATH_ENV_VAR] = os.pathsep.join(dict.fromkeys(all_entries))
277
+ baseline_environment[PYTEST_PLUGINS_ENV_VAR] = _pytest_plugins_setting(
278
+ all_staged_environment_settings
279
+ )
280
+ baseline_environment[BASELINE_PRIMARY_ROOT_ENV_VAR] = str(repository_root)
281
+ baseline_environment[BASELINE_LEAK_REPORT_ENV_VAR] = str(leak_report_path)
282
+ return baseline_environment
283
+
284
+
285
+ def modules_imported_from_primary_tree(leak_report_path: Path) -> list[Path] | None:
286
+ """Return the user's-tree modules the baseline run imported, or None when unreported.
287
+
288
+ ::
289
+
290
+ ok: [] -> the baseline measured HEAD alone
291
+ flag: [/repo/packages/foo.py] -> the baseline read staged code
292
+ flag: None -> the run left no report; nothing is proven
293
+
294
+ Args:
295
+ leak_report_path: The report path the baseline run was given.
296
+
297
+ Returns:
298
+ The resolved module paths, empty when the baseline stayed clean, or None
299
+ when the report is missing or unreadable.
300
+ """
301
+ try:
302
+ report_text = leak_report_path.read_text(encoding="utf-8")
303
+ except OSError:
304
+ return None
305
+ try:
306
+ all_module_texts = json.loads(report_text)
307
+ except ValueError:
308
+ return None
309
+ return [Path(str(each_text)) for each_text in all_module_texts]