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
package/CLAUDE.md
CHANGED
|
@@ -1,84 +1,51 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Development Assistant
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
## Communication
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
##
|
|
7
|
+
## Security
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
Collect credentials through secure UI only; never request secrets in chat.
|
|
10
10
|
|
|
11
|
-
|
|
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
|
-
|
|
13
|
+
## Advisors
|
|
14
14
|
|
|
15
|
-
|
|
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
|
-
|
|
17
|
+
## Files and workspaces
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
Put all work in an isolated worktree under the repo's `.claude/worktrees/`.
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
Default to Edit for existing files; reach for Write only when the path is genuinely new.
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
## Code and tests
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Tests must exercise real behavior, real data, and production paths.
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
Keep changes within scope. Prefer durable systemic fixes for reusable behavior.
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Do not rewrite entire files or rename public parameters without need.
|
|
30
30
|
|
|
31
|
-
##
|
|
31
|
+
## Reviews
|
|
32
32
|
|
|
33
|
-
|
|
33
|
+
Verify every sub-agent file list, count, description, and finding against the repository and diff.
|
|
34
34
|
|
|
35
|
-
|
|
35
|
+
Do not commit untracked files unless explicitly instructed.
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
## Delegation
|
|
38
38
|
|
|
39
|
-
|
|
39
|
+
Request precise file-and-line answers from research subagents.
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
## Task tracking
|
|
42
42
|
|
|
43
|
-
|
|
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
|
-
|
|
45
|
+
## Repository rule
|
|
48
46
|
|
|
49
|
-
|
|
47
|
+
Before changing skill, rule, or hook installation in the claude-dev-env repo, read `docs/references/skill-install-system.md`.
|
|
50
48
|
|
|
51
|
-
|
|
49
|
+
## Definitions
|
|
52
50
|
|
|
53
|
-
|
|
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 =
|
|
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
|
|
300
|
-
|
|
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
|
|
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]
|