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
|
@@ -7,15 +7,22 @@ from pr_loop_shared_constants.preflight_constants import GIT_DIRECTORY_NAME
|
|
|
7
7
|
__all__ = (
|
|
8
8
|
"ALL_AGENT_CONFIG_DENY_TOOLS",
|
|
9
9
|
"ALL_AGENT_CONFIG_PATH_PATTERNS",
|
|
10
|
+
"ALL_INERT_FILE_PERMISSION_TOOLS",
|
|
11
|
+
"ALL_LEGACY_PERMISSION_REAP_TOOLS",
|
|
10
12
|
"ALL_PERMISSION_ALLOW_TOOLS",
|
|
11
13
|
"ALL_TRUST_ENTRY_PROJECT_PATH_BOUNDARY_QUOTE_CHARACTERS",
|
|
12
14
|
"AUTO_MODE_ENVIRONMENT_ENTRY_PREFIX",
|
|
13
15
|
"AUTO_MODE_ENVIRONMENT_ENTRY_TEMPLATE",
|
|
14
16
|
"CLAUDE_SETTINGS_DIRECTORY_NAME",
|
|
15
17
|
"CLAUDE_SETTINGS_FILENAME",
|
|
18
|
+
"CLAUDE_SETTINGS_LOCAL_FILENAME",
|
|
16
19
|
"GIT_DIRECTORY_NAME",
|
|
20
|
+
"HOME_PROJECT_PATH_ALIAS",
|
|
21
|
+
"INERT_RULES_REMOVED_LOG_PREFIX",
|
|
22
|
+
"PROJECT_LOCAL_INERT_RULES_REMOVED_LOG_PREFIX",
|
|
17
23
|
"TEXT_FILE_ENCODING",
|
|
18
24
|
"UNIQUE_TEMPORARY_SUFFIX_BYTE_LENGTH",
|
|
25
|
+
"get_claude_project_local_settings_path",
|
|
19
26
|
"get_claude_user_settings_path",
|
|
20
27
|
)
|
|
21
28
|
|
|
@@ -24,6 +31,20 @@ ALL_PERMISSION_ALLOW_TOOLS: tuple[str, ...] = ("Edit", "Read")
|
|
|
24
31
|
|
|
25
32
|
ALL_AGENT_CONFIG_DENY_TOOLS: tuple[str, ...] = ("Edit", "Read")
|
|
26
33
|
|
|
34
|
+
ALL_LEGACY_PERMISSION_REAP_TOOLS: tuple[str, ...] = ("Write", "Glob")
|
|
35
|
+
|
|
36
|
+
ALL_INERT_FILE_PERMISSION_TOOLS: tuple[str, ...] = ("Write", "Glob", "NotebookEdit")
|
|
37
|
+
|
|
38
|
+
HOME_PROJECT_PATH_ALIAS: str = "$HOME"
|
|
39
|
+
|
|
40
|
+
INERT_RULES_REMOVED_LOG_PREFIX: str = (
|
|
41
|
+
"Inert Write/Glob/NotebookEdit rules removed: "
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
PROJECT_LOCAL_INERT_RULES_REMOVED_LOG_PREFIX: str = (
|
|
45
|
+
"Project-local inert Write/Glob/NotebookEdit rules removed: "
|
|
46
|
+
)
|
|
47
|
+
|
|
27
48
|
ALL_AGENT_CONFIG_PATH_PATTERNS: tuple[str, ...] = (
|
|
28
49
|
"settings*.json",
|
|
29
50
|
"hooks/**",
|
|
@@ -81,6 +102,8 @@ CLAUDE_SETTINGS_DIRECTORY_NAME: str = ".claude"
|
|
|
81
102
|
|
|
82
103
|
CLAUDE_SETTINGS_FILENAME: str = "settings.json"
|
|
83
104
|
|
|
105
|
+
CLAUDE_SETTINGS_LOCAL_FILENAME: str = "settings.local.json"
|
|
106
|
+
|
|
84
107
|
TEXT_FILE_ENCODING: str = "utf-8"
|
|
85
108
|
|
|
86
109
|
UNIQUE_TEMPORARY_SUFFIX_BYTE_LENGTH: int = 8
|
|
@@ -88,3 +111,24 @@ UNIQUE_TEMPORARY_SUFFIX_BYTE_LENGTH: int = 8
|
|
|
88
111
|
|
|
89
112
|
def get_claude_user_settings_path() -> Path:
|
|
90
113
|
return Path.home() / CLAUDE_SETTINGS_DIRECTORY_NAME / CLAUDE_SETTINGS_FILENAME
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def get_claude_project_local_settings_path(
|
|
117
|
+
project_root_path: Path | None = None,
|
|
118
|
+
) -> Path:
|
|
119
|
+
"""Return ``<project>/.claude/settings.local.json`` for the project root.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
project_root_path: Project root; defaults to the process cwd.
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Path to the project-local Claude settings file (may not exist yet).
|
|
126
|
+
"""
|
|
127
|
+
resolved_project_root_path = (
|
|
128
|
+
project_root_path if project_root_path is not None else Path.cwd()
|
|
129
|
+
)
|
|
130
|
+
return (
|
|
131
|
+
resolved_project_root_path
|
|
132
|
+
/ CLAUDE_SETTINGS_DIRECTORY_NAME
|
|
133
|
+
/ CLAUDE_SETTINGS_LOCAL_FILENAME
|
|
134
|
+
)
|
|
@@ -146,3 +146,205 @@ STAGED_TEST_GROUP_FAILURE_MESSAGE: str = (
|
|
|
146
146
|
MINIMUM_STAGED_PYTEST_PYTHON_MAJOR: int = 3
|
|
147
147
|
|
|
148
148
|
MINIMUM_STAGED_PYTEST_PYTHON_MINOR: int = 12
|
|
149
|
+
|
|
150
|
+
JUNIT_XML_FLAG_PREFIX: str = "--junitxml="
|
|
151
|
+
|
|
152
|
+
JUNIT_XML_TESTCASE_TAG: str = "testcase"
|
|
153
|
+
|
|
154
|
+
JUNIT_XML_FAILURE_TAG: str = "failure"
|
|
155
|
+
|
|
156
|
+
JUNIT_XML_ERROR_TAG: str = "error"
|
|
157
|
+
|
|
158
|
+
JUNIT_XML_CLASSNAME_ATTRIBUTE: str = "classname"
|
|
159
|
+
|
|
160
|
+
JUNIT_XML_NAME_ATTRIBUTE: str = "name"
|
|
161
|
+
|
|
162
|
+
JUNIT_XML_MISSING_ATTRIBUTE_FALLBACK: str = ""
|
|
163
|
+
|
|
164
|
+
REGRESSION_JUNIT_TEMP_DIRECTORY_PREFIX: str = "code_rules_gate_junit_"
|
|
165
|
+
|
|
166
|
+
REGRESSION_STAGED_JUNIT_SUBDIRECTORY_NAME: str = "staged"
|
|
167
|
+
|
|
168
|
+
REGRESSION_BASELINE_JUNIT_SUBDIRECTORY_NAME: str = "baseline"
|
|
169
|
+
|
|
170
|
+
REGRESSION_BASELINE_WORKTREE_TEMP_DIRECTORY_PREFIX: str = "code_rules_gate_baseline_"
|
|
171
|
+
|
|
172
|
+
REGRESSION_BASELINE_WORKTREE_DIRECTORY_NAME: str = "tree"
|
|
173
|
+
|
|
174
|
+
GIT_HEAD_REVISION: str = "HEAD"
|
|
175
|
+
|
|
176
|
+
ALL_GIT_HEAD_EXISTS_ARGS: tuple[str, ...] = ("rev-parse", "--verify", "HEAD")
|
|
177
|
+
|
|
178
|
+
ALL_GIT_WORKTREE_ADD_DETACH_ARGS: tuple[str, ...] = (
|
|
179
|
+
"worktree",
|
|
180
|
+
"add",
|
|
181
|
+
"--detach",
|
|
182
|
+
"--quiet",
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
ALL_GIT_WORKTREE_REMOVE_FORCE_ARGS: tuple[str, ...] = ("worktree", "remove", "--force")
|
|
186
|
+
|
|
187
|
+
ALL_GIT_WORKTREE_PRUNE_ARGS: tuple[str, ...] = ("worktree", "prune")
|
|
188
|
+
|
|
189
|
+
REGRESSION_NO_BASELINE_MESSAGE: str = (
|
|
190
|
+
"code_rules_gate: no prior commit to compare against (first commit on this branch); "
|
|
191
|
+
"every staged test failure blocks."
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
REGRESSION_WORKTREE_ADD_FAILED_MESSAGE: str = (
|
|
195
|
+
"code_rules_gate: could not open a detached HEAD worktree for the pre-staged baseline "
|
|
196
|
+
"(git worktree add --detach failed); falling back to blocking on every staged test "
|
|
197
|
+
"failure."
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
REGRESSION_WORKTREE_REMOVE_FAILED_MESSAGE: str = (
|
|
201
|
+
"code_rules_gate: git worktree remove --force failed after the baseline check — your "
|
|
202
|
+
"staged changes are untouched in this worktree. Run 'git worktree list' and "
|
|
203
|
+
"'git worktree prune' if a temporary baseline tree remains registered."
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
REGRESSION_PRE_EXISTING_FAILURE_BYPASSED_MESSAGE: str = (
|
|
207
|
+
"code_rules_gate: staged test group rooted at {group_root} has {count} failure(s) "
|
|
208
|
+
"already present before this change (not caused by it); not blocking."
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
REGRESSION_GROUP_FAILURE_MESSAGE: str = (
|
|
212
|
+
"code_rules_gate: staged test group rooted at {group_root} has {count} failure(s) "
|
|
213
|
+
"this change introduces; commit blocked."
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
BASELINE_LEAK_PLUGIN_DIRECTORY_NAME: str = "baseline_import_plugin"
|
|
217
|
+
|
|
218
|
+
BASELINE_LEAK_PLUGIN_MODULE_NAME: str = "code_rules_gate_baseline_import_plugin"
|
|
219
|
+
|
|
220
|
+
BASELINE_LEAK_REPORT_FILENAME: str = "imported_from_primary_tree.json"
|
|
221
|
+
|
|
222
|
+
BASELINE_PRIMARY_ROOT_ENV_VAR: str = "CODE_RULES_GATE_BASELINE_PRIMARY_ROOT"
|
|
223
|
+
|
|
224
|
+
BASELINE_LEAK_REPORT_ENV_VAR: str = "CODE_RULES_GATE_BASELINE_LEAK_REPORT"
|
|
225
|
+
|
|
226
|
+
PYTEST_PLUGINS_ENV_VAR: str = "PYTEST_PLUGINS"
|
|
227
|
+
|
|
228
|
+
PYTEST_PLUGINS_SEPARATOR: str = ","
|
|
229
|
+
|
|
230
|
+
BASELINE_IMPORT_PROBE_TIMEOUT_SECONDS: int = 120
|
|
231
|
+
|
|
232
|
+
BASELINE_IMPORT_PROBE_DEGRADED_TAIL: str = (
|
|
233
|
+
"The HEAD baseline runs with its import roots un-rebased; a module that still "
|
|
234
|
+
"resolves into your working tree is reported after that run and blocks the group."
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
BASELINE_IMPORT_PROBE_TIMED_OUT_MESSAGE: str = (
|
|
238
|
+
"code_rules_gate: the import-root probe did not finish within {seconds} seconds. "
|
|
239
|
+
+ BASELINE_IMPORT_PROBE_DEGRADED_TAIL
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
BASELINE_IMPORT_PROBE_UNSTARTABLE_MESSAGE: str = (
|
|
243
|
+
"code_rules_gate: the import-root probe did not start ({reason}). "
|
|
244
|
+
+ BASELINE_IMPORT_PROBE_DEGRADED_TAIL
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
BASELINE_IMPORT_PROBE_REJECTED_MESSAGE: str = (
|
|
248
|
+
"code_rules_gate: the import-root probe exited with status {status}. "
|
|
249
|
+
+ BASELINE_IMPORT_PROBE_DEGRADED_TAIL
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
PYTHON_INTERPRETER_COMMAND_FLAG: str = "-c"
|
|
253
|
+
|
|
254
|
+
BASELINE_IMPORT_ROOT_PROBE_SOURCE: str = (
|
|
255
|
+
"import json\n"
|
|
256
|
+
"import sys\n"
|
|
257
|
+
"from pathlib import Path\n"
|
|
258
|
+
"all_roots = [each_entry for each_entry in sys.path if each_entry]\n"
|
|
259
|
+
"for each_finder in list(sys.meta_path):\n"
|
|
260
|
+
" owning_module = sys.modules.get(getattr(each_finder, '__module__', '') or '')\n"
|
|
261
|
+
" for each_attribute in ('MAPPING', 'NAMESPACES'):\n"
|
|
262
|
+
" mapping = getattr(owning_module, each_attribute, None)\n"
|
|
263
|
+
" if not isinstance(mapping, dict):\n"
|
|
264
|
+
" continue\n"
|
|
265
|
+
" for each_name, each_target in mapping.items():\n"
|
|
266
|
+
" all_targets = each_target if isinstance(each_target, list) else [each_target]\n"
|
|
267
|
+
" for each_package_directory in all_targets:\n"
|
|
268
|
+
" try:\n"
|
|
269
|
+
" all_roots.append(\n"
|
|
270
|
+
" str(Path(each_package_directory).parents[str(each_name).count('.')])\n"
|
|
271
|
+
" )\n"
|
|
272
|
+
" except (IndexError, TypeError, OSError):\n"
|
|
273
|
+
" continue\n"
|
|
274
|
+
"print(json.dumps(all_roots))\n"
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
BASELINE_LEAK_PLUGIN_SOURCE: str = '''"""Report every module the baseline pytest run imported out of the user's own tree."""
|
|
278
|
+
|
|
279
|
+
import json
|
|
280
|
+
import os
|
|
281
|
+
import sys
|
|
282
|
+
import sysconfig
|
|
283
|
+
from pathlib import Path
|
|
284
|
+
|
|
285
|
+
PRIMARY_ROOT_ENV_VAR = "{primary_root_env_var}"
|
|
286
|
+
REPORT_PATH_ENV_VAR = "{report_path_env_var}"
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _resolved(path_text):
|
|
290
|
+
if not path_text:
|
|
291
|
+
return None
|
|
292
|
+
try:
|
|
293
|
+
return Path(path_text).resolve()
|
|
294
|
+
except OSError:
|
|
295
|
+
return None
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _is_under(candidate, root):
|
|
299
|
+
return candidate == root or root in candidate.parents
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _interpreter_roots():
|
|
303
|
+
all_texts = [sys.prefix, sys.base_prefix, *sysconfig.get_paths().values()]
|
|
304
|
+
all_roots = [_resolved(each_text) for each_text in all_texts]
|
|
305
|
+
return [each_root for each_root in all_roots if each_root is not None]
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _already_reported(report_path):
|
|
309
|
+
try:
|
|
310
|
+
return set(json.loads(report_path.read_text(encoding="utf-8")))
|
|
311
|
+
except (OSError, ValueError):
|
|
312
|
+
return set()
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _imported_from_primary_tree(primary_root, report_path):
|
|
316
|
+
all_interpreter_roots = _interpreter_roots()
|
|
317
|
+
all_leaked = _already_reported(report_path)
|
|
318
|
+
for each_module in list(sys.modules.values()):
|
|
319
|
+
module_path = _resolved(getattr(each_module, "__file__", None))
|
|
320
|
+
if module_path is None or not _is_under(module_path, primary_root):
|
|
321
|
+
continue
|
|
322
|
+
if any(_is_under(module_path, each_root) for each_root in all_interpreter_roots):
|
|
323
|
+
continue
|
|
324
|
+
all_leaked.add(str(module_path))
|
|
325
|
+
return sorted(all_leaked)
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def pytest_sessionfinish(session, exitstatus):
|
|
329
|
+
primary_root = _resolved(os.environ.get(PRIMARY_ROOT_ENV_VAR))
|
|
330
|
+
report_path = _resolved(os.environ.get(REPORT_PATH_ENV_VAR))
|
|
331
|
+
if primary_root is None or report_path is None:
|
|
332
|
+
return
|
|
333
|
+
report_path.write_text(
|
|
334
|
+
json.dumps(_imported_from_primary_tree(primary_root, report_path)), encoding="utf-8"
|
|
335
|
+
)
|
|
336
|
+
'''
|
|
337
|
+
|
|
338
|
+
REGRESSION_BASELINE_IMPORT_LEAK_MESSAGE: str = (
|
|
339
|
+
"code_rules_gate: the HEAD baseline run for the group rooted at {group_root} imported "
|
|
340
|
+
"{count} module(s) from your working tree, starting with {first_module}. It measured the "
|
|
341
|
+
"staged code, not HEAD, so that baseline is not trusted and every staged failure in this "
|
|
342
|
+
"group blocks. An editable install whose import hook runs ahead of the path scan is the "
|
|
343
|
+
"usual cause."
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
REGRESSION_BASELINE_LEAK_UNREPORTED_MESSAGE: str = (
|
|
347
|
+
"code_rules_gate: the HEAD baseline run for the group rooted at {group_root} wrote no "
|
|
348
|
+
"import-origin report, so whether it loaded your working-tree code is unknown. That "
|
|
349
|
+
"baseline is not trusted and every staged failure in this group blocks."
|
|
350
|
+
)
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
"""Configuration constants for the Copilot premium-interaction quota pre-check."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from pathlib import Path
|
|
6
|
-
|
|
7
|
-
COPILOT_QUOTA_ACCOUNT_ENV_VAR_NAME: str = "COPILOT_QUOTA_ACCOUNT"
|
|
8
|
-
GH_TOKEN_ENV_VAR_NAME: str = "GH_TOKEN"
|
|
9
|
-
COPILOT_INTERNAL_USER_API_PATH: str = "copilot_internal/user"
|
|
10
|
-
|
|
11
|
-
QUOTA_SNAPSHOTS_FIELD_NAME: str = "quota_snapshots"
|
|
12
|
-
PREMIUM_INTERACTIONS_FIELD_NAME: str = "premium_interactions"
|
|
13
|
-
PREMIUM_UNLIMITED_FIELD_NAME: str = "unlimited"
|
|
14
|
-
PREMIUM_REMAINING_FIELD_NAME: str = "remaining"
|
|
15
|
-
PREMIUM_OVERAGE_PERMITTED_FIELD_NAME: str = "overage_permitted"
|
|
16
|
-
PREMIUM_ENTITLEMENT_FIELD_NAME: str = "entitlement"
|
|
17
|
-
PREMIUM_PERCENT_REMAINING_FIELD_NAME: str = "percent_remaining"
|
|
18
|
-
|
|
19
|
-
EXIT_CODE_QUOTA_AVAILABLE: int = 0
|
|
20
|
-
EXIT_CODE_OUT_OF_QUOTA: int = 1
|
|
21
|
-
EXIT_CODE_QUOTA_API_DOWN: int = 2
|
|
22
|
-
EXIT_CODE_NO_ACCOUNT_CONFIGURED: int = 3
|
|
23
|
-
|
|
24
|
-
COPILOT_QUOTA_DEFAULT_ENV_FILE_PATH: Path = Path(__file__).resolve().parents[4] / ".env"
|
|
1
|
+
"""Configuration constants for the Copilot premium-interaction quota pre-check."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
COPILOT_QUOTA_ACCOUNT_ENV_VAR_NAME: str = "COPILOT_QUOTA_ACCOUNT"
|
|
8
|
+
GH_TOKEN_ENV_VAR_NAME: str = "GH_TOKEN"
|
|
9
|
+
COPILOT_INTERNAL_USER_API_PATH: str = "copilot_internal/user"
|
|
10
|
+
|
|
11
|
+
QUOTA_SNAPSHOTS_FIELD_NAME: str = "quota_snapshots"
|
|
12
|
+
PREMIUM_INTERACTIONS_FIELD_NAME: str = "premium_interactions"
|
|
13
|
+
PREMIUM_UNLIMITED_FIELD_NAME: str = "unlimited"
|
|
14
|
+
PREMIUM_REMAINING_FIELD_NAME: str = "remaining"
|
|
15
|
+
PREMIUM_OVERAGE_PERMITTED_FIELD_NAME: str = "overage_permitted"
|
|
16
|
+
PREMIUM_ENTITLEMENT_FIELD_NAME: str = "entitlement"
|
|
17
|
+
PREMIUM_PERCENT_REMAINING_FIELD_NAME: str = "percent_remaining"
|
|
18
|
+
|
|
19
|
+
EXIT_CODE_QUOTA_AVAILABLE: int = 0
|
|
20
|
+
EXIT_CODE_OUT_OF_QUOTA: int = 1
|
|
21
|
+
EXIT_CODE_QUOTA_API_DOWN: int = 2
|
|
22
|
+
EXIT_CODE_NO_ACCOUNT_CONFIGURED: int = 3
|
|
23
|
+
|
|
24
|
+
COPILOT_QUOTA_DEFAULT_ENV_FILE_PATH: Path = Path(__file__).resolve().parents[4] / ".env"
|
package/_shared/pr-loop/scripts/pr_loop_shared_constants/stale_worktree_rule_sweep_constants.py
CHANGED
|
@@ -1,107 +1,107 @@
|
|
|
1
|
-
"""Constants and rule-format readers for stale_worktree_rule_sweep.
|
|
2
|
-
|
|
3
|
-
The worktrees root is derived from the same ~/.claude home the permission
|
|
4
|
-
grant script mints its rules under, so the location is named in exactly one
|
|
5
|
-
place. A worktree rule points at a directory one or more segments below that
|
|
6
|
-
root (flat or nested layouts)::
|
|
7
|
-
|
|
8
|
-
Edit(<claude home>/worktrees/<worktree name>/.claude/**)
|
|
9
|
-
^^^^^^^^^^^^^^^
|
|
10
|
-
segment 1 (flat)
|
|
11
|
-
|
|
12
|
-
Edit(<claude home>/worktrees/<repository>/<worktree name>/.claude/**)
|
|
13
|
-
^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
|
|
14
|
-
nested layout
|
|
15
|
-
|
|
16
|
-
worktree_directory_for_rule(rule, root) -> project directory under root
|
|
17
|
-
|
|
18
|
-
A rule whose target path lies outside the worktrees root reads back as None,
|
|
19
|
-
so the sweep leaves it untouched.
|
|
20
|
-
"""
|
|
21
|
-
|
|
22
|
-
from pathlib import Path
|
|
23
|
-
|
|
24
|
-
from pr_loop_shared_constants.claude_permissions_constants import (
|
|
25
|
-
get_claude_user_settings_path,
|
|
26
|
-
)
|
|
27
|
-
|
|
28
|
-
WORKTREES_SUBDIRECTORY_NAME: str = "worktrees"
|
|
29
|
-
|
|
30
|
-
MINIMUM_WORKTREE_PATH_SEGMENT_COUNT: int = 1
|
|
31
|
-
|
|
32
|
-
RULE_PATH_OPEN_DELIMITER: str = "("
|
|
33
|
-
|
|
34
|
-
RULE_PATH_CLOSE_DELIMITER: str = ")"
|
|
35
|
-
|
|
36
|
-
CLAUDE_HOME_PATH_MARKER: str = "/.claude"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def get_claude_worktrees_root() -> Path:
|
|
40
|
-
"""Return the ~/.claude/worktrees directory the grant rules point under.
|
|
41
|
-
|
|
42
|
-
Reuses get_claude_user_settings_path so the ~/.claude home is resolved
|
|
43
|
-
in one place: the settings file's parent directory is the home, and the
|
|
44
|
-
worktrees live directly beneath it.
|
|
45
|
-
|
|
46
|
-
Returns:
|
|
47
|
-
The absolute path to the user's worktrees root directory.
|
|
48
|
-
"""
|
|
49
|
-
claude_home_directory = get_claude_user_settings_path().parent
|
|
50
|
-
return claude_home_directory / WORKTREES_SUBDIRECTORY_NAME
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def extract_rule_target_path(rule: str) -> str | None:
|
|
54
|
-
"""Return the path a permission rule targets, or None when unparsable.
|
|
55
|
-
|
|
56
|
-
A rule reads `Tool(<path>)`; the target is the text between the first
|
|
57
|
-
open delimiter and the last close delimiter::
|
|
58
|
-
|
|
59
|
-
Edit(/repo/wt/.claude/**) -> /repo/wt/.claude/**
|
|
60
|
-
not a rule -> None
|
|
61
|
-
|
|
62
|
-
Args:
|
|
63
|
-
rule: The permission rule string to read.
|
|
64
|
-
|
|
65
|
-
Returns:
|
|
66
|
-
The target path string, or None when the delimiters are absent.
|
|
67
|
-
"""
|
|
68
|
-
open_index = rule.find(RULE_PATH_OPEN_DELIMITER)
|
|
69
|
-
close_index = rule.rfind(RULE_PATH_CLOSE_DELIMITER)
|
|
70
|
-
if open_index == -1 or close_index <= open_index:
|
|
71
|
-
return None
|
|
72
|
-
return rule[open_index + 1 : close_index]
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
def worktree_directory_for_rule(rule: str, worktrees_root: Path) -> Path | None:
|
|
76
|
-
"""Return the worktree directory a rule targets, or None when it is not one.
|
|
77
|
-
|
|
78
|
-
Keeps every path segment below the worktrees root so flat
|
|
79
|
-
(`worktrees/<name>`) and nested (`worktrees/<repo>/<name>`) layouts
|
|
80
|
-
both resolve to the on-disk project directory::
|
|
81
|
-
|
|
82
|
-
Edit(<root>/flat-wt/.claude/**) -> <root>/flat-wt
|
|
83
|
-
Edit(<root>/repo/wt/.claude/**) -> <root>/repo/wt
|
|
84
|
-
Edit(/elsewhere/project/.claude/**) -> None
|
|
85
|
-
|
|
86
|
-
Args:
|
|
87
|
-
rule: The permission rule string to read.
|
|
88
|
-
worktrees_root: The ~/.claude/worktrees directory rules live under.
|
|
89
|
-
|
|
90
|
-
Returns:
|
|
91
|
-
The worktree directory, or None when the target lies outside the
|
|
92
|
-
worktrees root or has no path segments under it.
|
|
93
|
-
"""
|
|
94
|
-
target_path = extract_rule_target_path(rule)
|
|
95
|
-
if target_path is None:
|
|
96
|
-
return None
|
|
97
|
-
project_portion = target_path.rsplit(CLAUDE_HOME_PATH_MARKER, 1)[0]
|
|
98
|
-
if not project_portion:
|
|
99
|
-
return None
|
|
100
|
-
try:
|
|
101
|
-
relative_path = Path(project_portion).relative_to(worktrees_root)
|
|
102
|
-
except ValueError:
|
|
103
|
-
return None
|
|
104
|
-
all_segments = relative_path.parts
|
|
105
|
-
if len(all_segments) < MINIMUM_WORKTREE_PATH_SEGMENT_COUNT:
|
|
106
|
-
return None
|
|
107
|
-
return worktrees_root.joinpath(*all_segments)
|
|
1
|
+
"""Constants and rule-format readers for stale_worktree_rule_sweep.
|
|
2
|
+
|
|
3
|
+
The worktrees root is derived from the same ~/.claude home the permission
|
|
4
|
+
grant script mints its rules under, so the location is named in exactly one
|
|
5
|
+
place. A worktree rule points at a directory one or more segments below that
|
|
6
|
+
root (flat or nested layouts)::
|
|
7
|
+
|
|
8
|
+
Edit(<claude home>/worktrees/<worktree name>/.claude/**)
|
|
9
|
+
^^^^^^^^^^^^^^^
|
|
10
|
+
segment 1 (flat)
|
|
11
|
+
|
|
12
|
+
Edit(<claude home>/worktrees/<repository>/<worktree name>/.claude/**)
|
|
13
|
+
^^^^^^^^^^^^ ^^^^^^^^^^^^^^^
|
|
14
|
+
nested layout
|
|
15
|
+
|
|
16
|
+
worktree_directory_for_rule(rule, root) -> project directory under root
|
|
17
|
+
|
|
18
|
+
A rule whose target path lies outside the worktrees root reads back as None,
|
|
19
|
+
so the sweep leaves it untouched.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from pr_loop_shared_constants.claude_permissions_constants import (
|
|
25
|
+
get_claude_user_settings_path,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
WORKTREES_SUBDIRECTORY_NAME: str = "worktrees"
|
|
29
|
+
|
|
30
|
+
MINIMUM_WORKTREE_PATH_SEGMENT_COUNT: int = 1
|
|
31
|
+
|
|
32
|
+
RULE_PATH_OPEN_DELIMITER: str = "("
|
|
33
|
+
|
|
34
|
+
RULE_PATH_CLOSE_DELIMITER: str = ")"
|
|
35
|
+
|
|
36
|
+
CLAUDE_HOME_PATH_MARKER: str = "/.claude"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def get_claude_worktrees_root() -> Path:
|
|
40
|
+
"""Return the ~/.claude/worktrees directory the grant rules point under.
|
|
41
|
+
|
|
42
|
+
Reuses get_claude_user_settings_path so the ~/.claude home is resolved
|
|
43
|
+
in one place: the settings file's parent directory is the home, and the
|
|
44
|
+
worktrees live directly beneath it.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The absolute path to the user's worktrees root directory.
|
|
48
|
+
"""
|
|
49
|
+
claude_home_directory = get_claude_user_settings_path().parent
|
|
50
|
+
return claude_home_directory / WORKTREES_SUBDIRECTORY_NAME
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def extract_rule_target_path(rule: str) -> str | None:
|
|
54
|
+
"""Return the path a permission rule targets, or None when unparsable.
|
|
55
|
+
|
|
56
|
+
A rule reads `Tool(<path>)`; the target is the text between the first
|
|
57
|
+
open delimiter and the last close delimiter::
|
|
58
|
+
|
|
59
|
+
Edit(/repo/wt/.claude/**) -> /repo/wt/.claude/**
|
|
60
|
+
not a rule -> None
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
rule: The permission rule string to read.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
The target path string, or None when the delimiters are absent.
|
|
67
|
+
"""
|
|
68
|
+
open_index = rule.find(RULE_PATH_OPEN_DELIMITER)
|
|
69
|
+
close_index = rule.rfind(RULE_PATH_CLOSE_DELIMITER)
|
|
70
|
+
if open_index == -1 or close_index <= open_index:
|
|
71
|
+
return None
|
|
72
|
+
return rule[open_index + 1 : close_index]
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def worktree_directory_for_rule(rule: str, worktrees_root: Path) -> Path | None:
|
|
76
|
+
"""Return the worktree directory a rule targets, or None when it is not one.
|
|
77
|
+
|
|
78
|
+
Keeps every path segment below the worktrees root so flat
|
|
79
|
+
(`worktrees/<name>`) and nested (`worktrees/<repo>/<name>`) layouts
|
|
80
|
+
both resolve to the on-disk project directory::
|
|
81
|
+
|
|
82
|
+
Edit(<root>/flat-wt/.claude/**) -> <root>/flat-wt
|
|
83
|
+
Edit(<root>/repo/wt/.claude/**) -> <root>/repo/wt
|
|
84
|
+
Edit(/elsewhere/project/.claude/**) -> None
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
rule: The permission rule string to read.
|
|
88
|
+
worktrees_root: The ~/.claude/worktrees directory rules live under.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
The worktree directory, or None when the target lies outside the
|
|
92
|
+
worktrees root or has no path segments under it.
|
|
93
|
+
"""
|
|
94
|
+
target_path = extract_rule_target_path(rule)
|
|
95
|
+
if target_path is None:
|
|
96
|
+
return None
|
|
97
|
+
project_portion = target_path.rsplit(CLAUDE_HOME_PATH_MARKER, 1)[0]
|
|
98
|
+
if not project_portion:
|
|
99
|
+
return None
|
|
100
|
+
try:
|
|
101
|
+
relative_path = Path(project_portion).relative_to(worktrees_root)
|
|
102
|
+
except ValueError:
|
|
103
|
+
return None
|
|
104
|
+
all_segments = relative_path.parts
|
|
105
|
+
if len(all_segments) < MINIMUM_WORKTREE_PATH_SEGMENT_COUNT:
|
|
106
|
+
return None
|
|
107
|
+
return worktrees_root.joinpath(*all_segments)
|