claude-dev-env 4.3.0 → 7.0.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/.agents/agents/pr-description-writer.md +2 -2
- package/.agents/agents/test_agent_frontmatter.py +2 -2
- package/.agents/agents/test_pr_description_writer_style.py +1 -3
- package/.agents/skills/e-code-review/SKILL.md +12 -12
- package/.agents/skills/e-code-review/preflight-proposal.contract.test.mjs +20 -0
- package/.agents/skills/e-code-review/reference/fix.md +15 -17
- package/.agents/skills/e-code-review/reference/loop.md +35 -39
- package/.agents/skills/e-code-review/reference/mode-contract.test.mjs +2 -1
- package/.agents/skills/e-code-review/reference/preflight-proposal.md +44 -9
- package/.agents/skills/e-code-review/reference/runner-selection.md +13 -14
- package/.agents/skills/issue-tracker/SKILL.md +5 -4
- package/.agents/skills/pull-request/SKILL.md +11 -13
- package/.agents/skills/pull-request/reference/publication-tasks.md +3 -3
- package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/constants.py +9 -1
- package/.agents/skills/pull-request/scripts/pull_request.py +33 -44
- package/.agents/skills/pull-request/scripts/test_github_pr_command_constants.py +9 -0
- package/.agents/skills/pull-request/scripts/test_pull_request.py +1 -3
- package/.agents/skills/skill-builder/SKILL.md +3 -196
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +6 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +64 -19
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +59 -0
- package/bin/install.agents-home.test.mjs +15 -13
- package/bin/install.mjs +28 -0
- package/bin/install.policy-lint.test.mjs +78 -0
- package/bin/install.prune.test.mjs +59 -20
- package/bin/install.pstack-plugin.test.mjs +114 -0
- package/bin/install.test.mjs +37 -0
- package/bin/refresh-pstack-plugin-skills.test.mjs +211 -0
- package/hooks/advisory/pr_done_reminder.py +342 -0
- package/hooks/advisory/test_pr_done_reminder.py +249 -0
- package/hooks/blocking/bash_post_call_dispatcher.py +53 -6
- package/hooks/blocking/code_rules_paired_test.py +22 -15
- package/hooks/blocking/test_bash_post_call_dispatcher.py +65 -4
- package/hooks/blocking/test_code_rules_enforcer_paired_test.py +36 -29
- package/hooks/blocking/test_code_rules_js_resume_task_enumeration.py +4 -4
- package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +5 -5
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +68 -133
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +20 -51
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +18 -18
- package/hooks/git-hooks/git_hooks_constants/staged_policy_lint.py +12 -0
- package/hooks/git-hooks/pre_commit.py +52 -11
- package/hooks/git-hooks/test_installed_policy_lint.py +159 -0
- package/hooks/git-hooks/test_native_policy_commit.py +87 -0
- package/hooks/git-hooks/test_pre_commit.py +246 -0
- package/hooks/hooks.json +1 -1
- package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +14 -1
- package/hooks/hooks_constants/pr_done_reminder_constants.py +98 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -43
- package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +19 -2
- package/hooks/hooks_constants/test_pr_done_reminder_constants.py +40 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +20 -32
- package/package.json +2 -2
- package/rules/durable-post-artifacts.md +2 -1
- package/rules/git-workflow.md +6 -6
- package/scripts/check.ps1 +0 -1
- package/{.agents/skills/run-claude-dev-env/driver.mjs → scripts/ci/installer-lifecycle.mjs} +3 -28
- package/scripts/ci/installer-lifecycle.test.mjs +26 -0
- package/scripts/ci/windows-installer-lifecycle.ps1 +1 -11
- package/scripts/dev_env_scripts_constants/shared_tree_constants.py +4 -5
- package/{.agents/skills/_shared/pr-loop/scripts → scripts}/durable_post_lint.py +5 -41
- package/{.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/durable_post_lint_constants.py → scripts/durable_post_lint_config/config/constants.py} +0 -10
- package/{.agents/skills/_shared/pr-loop/scripts/test_durable_post_lint_constants.py → scripts/durable_post_lint_config/config/test_constants.py} +1 -1
- package/scripts/policy_lint/adapter_detectors.py +13 -6
- package/scripts/policy_lint/adapters.py +7 -4
- package/scripts/policy_lint/config/constants.py +5 -0
- package/scripts/policy_lint/engine.py +48 -4
- package/scripts/policy_lint/model.py +1 -0
- package/scripts/policy_lint/registry.py +1 -1
- package/scripts/policy_lint/selection.py +2 -1
- package/scripts/policy_lint/selection_git.py +2 -0
- package/scripts/refresh_pstack_plugin_skills.mjs +200 -0
- package/scripts/shared_tree_paths.py +32 -3
- package/scripts/test_shared_tree_paths.py +33 -0
- package/scripts/tests/test_policy_lint_rules_archive.py +120 -0
- package/scripts/tests/test_policy_lint_rules_engine.py +25 -4
- package/scripts/tests/test_policy_lint_rules_installed_loader.py +29 -0
- package/scripts/tests/test_policy_lint_rules_registry.py +227 -13
- package/scripts/tests/test_policy_lint_selection.py +25 -2
- package/scripts/tests/test_policy_lint_selection_native_index.py +30 -0
- package/scripts/tests/test_rules.py +3 -7
- package/.agents/skills/_shared/pr-loop/.claude/CLAUDE.md +0 -1
- package/.agents/skills/_shared/pr-loop/AGENTS.md +0 -1
- package/.agents/skills/_shared/pr-loop/audit-contract.md +0 -5
- package/.agents/skills/_shared/pr-loop/audit-reply-template.md +0 -5
- package/.agents/skills/_shared/pr-loop/code-rules-gate.md +0 -5
- package/.agents/skills/_shared/pr-loop/fix-protocol.md +0 -5
- package/.agents/skills/_shared/pr-loop/gh-payloads.md +0 -5
- package/.agents/skills/_shared/pr-loop/portable-driver.md +0 -149
- package/.agents/skills/_shared/pr-loop/post-audit-thread-contract.md +0 -5
- package/.agents/skills/_shared/pr-loop/precatch-rubric.md +0 -5
- package/.agents/skills/_shared/pr-loop/preflight-proposal.contract.test.mjs +0 -119
- package/.agents/skills/_shared/pr-loop/preflight-proposal.md +0 -5
- package/.agents/skills/_shared/pr-loop/prompts/.claude/CLAUDE.md +0 -1
- package/.agents/skills/_shared/pr-loop/prompts/AGENTS.md +0 -1
- package/.agents/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +0 -324
- package/.agents/skills/_shared/pr-loop/scripts/.claude/CLAUDE.md +0 -1
- package/.agents/skills/_shared/pr-loop/scripts/AGENTS.md +0 -1
- package/.agents/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +0 -29
- package/.agents/skills/_shared/pr-loop/scripts/_cli_utils.py +0 -22
- package/.agents/skills/_shared/pr-loop/scripts/_path_resolver.py +0 -186
- package/.agents/skills/_shared/pr-loop/scripts/_xml_utils.py +0 -20
- package/.agents/skills/_shared/pr-loop/scripts/audit_category_schema.py +0 -355
- package/.agents/skills/_shared/pr-loop/scripts/build_audit_prompt.py +0 -289
- package/.agents/skills/_shared/pr-loop/scripts/build_converge_task_list.py +0 -310
- package/.agents/skills/_shared/pr-loop/scripts/build_fix_prompt.py +0 -292
- package/.agents/skills/_shared/pr-loop/scripts/init_loop_state.py +0 -134
- package/.agents/skills/_shared/pr-loop/scripts/portable_converge_driver.py +0 -1646
- package/.agents/skills/_shared/pr-loop/scripts/preflight_worktree.py +0 -392
- package/.agents/skills/_shared/pr-loop/scripts/select_converge_pacer.py +0 -215
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/.claude/CLAUDE.md +0 -1
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +0 -1
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/__init__.py +0 -0
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +0 -37
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/converge_task_list_constants.py +0 -56
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/handoff_constants.py +0 -45
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/pacer_constants.py +0 -47
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +0 -157
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/portable_driver_constants.py +0 -177
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +0 -70
- package/.agents/skills/_shared/pr-loop/scripts/teardown_worktrees.py +0 -172
- package/.agents/skills/_shared/pr-loop/scripts/test__path_resolver.py +0 -106
- package/.agents/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +0 -94
- package/.agents/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +0 -499
- package/.agents/skills/_shared/pr-loop/scripts/test_build_converge_task_list.py +0 -140
- package/.agents/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +0 -262
- package/.agents/skills/_shared/pr-loop/scripts/test_init_loop_state.py +0 -71
- package/.agents/skills/_shared/pr-loop/scripts/test_portable_converge_driver.py +0 -1119
- package/.agents/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +0 -287
- package/.agents/skills/_shared/pr-loop/scripts/test_select_converge_pacer.py +0 -207
- package/.agents/skills/_shared/pr-loop/scripts/test_teardown_worktrees.py +0 -59
- package/.agents/skills/_shared/pr-loop/scripts/test_write_handoff.py +0 -201
- package/.agents/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +0 -182
- package/.agents/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +0 -206
- package/.agents/skills/_shared/pr-loop/scripts/write_handoff.py +0 -309
- package/.agents/skills/_shared/pr-loop/state-schema.md +0 -5
- package/.agents/skills/_shared/pr-loop/worker-spawn.md +0 -5
- package/.agents/skills/autoconverge/.claude/CLAUDE.md +0 -1
- package/.agents/skills/autoconverge/AGENTS.md +0 -1
- package/.agents/skills/autoconverge/SKILL.md +0 -431
- package/.agents/skills/autoconverge/reference/.claude/CLAUDE.md +0 -1
- package/.agents/skills/autoconverge/reference/AGENTS.md +0 -1
- package/.agents/skills/autoconverge/reference/closing-report.md +0 -93
- package/.agents/skills/autoconverge/reference/convergence-gates.md +0 -31
- package/.agents/skills/autoconverge/reference/convergence.md +0 -219
- package/.agents/skills/autoconverge/reference/copilot-findings.md +0 -51
- package/.agents/skills/autoconverge/reference/gotchas.md +0 -66
- package/.agents/skills/autoconverge/reference/headless-safety.md +0 -44
- package/.agents/skills/autoconverge/reference/multi-pr.md +0 -128
- package/.agents/skills/autoconverge/reference/self-closing-loop.md +0 -84
- package/.agents/skills/autoconverge/reference/stop-conditions.md +0 -127
- package/.agents/skills/autoconverge/test_portable_pacer_gate.py +0 -54
- package/.agents/skills/autoconverge/workflow/.claude/CLAUDE.md +0 -1
- package/.agents/skills/autoconverge/workflow/AGENTS.md +0 -1
- package/.agents/skills/autoconverge/workflow/aggregate_runs.py +0 -371
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/.claude/CLAUDE.md +0 -1
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +0 -1
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/__init__.py +0 -0
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +0 -250
- package/.agents/skills/autoconverge/workflow/converge.clean-audit.test.mjs +0 -665
- package/.agents/skills/autoconverge/workflow/converge.codex-gate.test.mjs +0 -472
- package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +0 -64
- package/.agents/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +0 -1009
- package/.agents/skills/autoconverge/workflow/converge.fix-progress.test.mjs +0 -107
- package/.agents/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +0 -33
- package/.agents/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +0 -15
- package/.agents/skills/autoconverge/workflow/converge.mjs +0 -3132
- package/.agents/skills/autoconverge/workflow/converge.p2-advance.test.mjs +0 -202
- package/.agents/skills/autoconverge/workflow/converge.path-aware.test.mjs +0 -47
- package/.agents/skills/autoconverge/workflow/converge.precatch.test.mjs +0 -152
- package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +0 -116
- package/.agents/skills/autoconverge/workflow/converge_multi.mjs +0 -169
- package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +0 -173
- package/.agents/skills/autoconverge/workflow/convergence_summary.py +0 -110
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +0 -266
- package/.agents/skills/autoconverge/workflow/render_report.py +0 -1024
- package/.agents/skills/autoconverge/workflow/test_aggregate_runs.py +0 -134
- package/.agents/skills/autoconverge/workflow/test_convergence_summary.py +0 -149
- package/.agents/skills/autoconverge/workflow/test_render_report.py +0 -812
- package/.agents/skills/hitl/SKILL.md +0 -45
- package/.agents/skills/pr-cleanup/SKILL.md +0 -140
- package/.agents/skills/pr-cleanup/reference/process-inventory.md +0 -17
- package/.agents/skills/pr-cleanup/reference/task-seeds.md +0 -22
- package/.agents/skills/pr-name-by-capability/SKILL.md +0 -73
- package/.agents/skills/pr-name-by-capability/reference/fetch-commands.md +0 -29
- package/.agents/skills/pr-name-by-capability/reference/offense-examples.md +0 -32
- package/.agents/skills/pr-name-by-capability/reference/report-template.md +0 -29
- package/.agents/skills/pr-name-by-capability/reference/rule-checklist.md +0 -38
- package/.agents/skills/pr-name-by-capability/reference/task-seeds.md +0 -10
- package/.agents/skills/pr-plain-language-cleanup/SKILL.md +0 -93
- package/.agents/skills/pr-refinement/SKILL.md +0 -31
- package/.agents/skills/pr-shared-extraction/SKILL.md +0 -177
- package/.agents/skills/pr-shared-extraction/reference/examples.md +0 -46
- package/.agents/skills/pr-shared-extraction/reference/offense-taxonomy.md +0 -100
- package/.agents/skills/pr-small-cl/SKILL.md +0 -52
- package/.agents/skills/pr-title-description/SKILL.md +0 -96
- package/.agents/skills/prototype/SKILL.md +0 -85
- package/.agents/skills/prototype/reference/honest-limitations.md +0 -23
- package/.agents/skills/prototype/reference/promotion-tasks.md +0 -23
- package/.agents/skills/prototype/scripts/build_sandbox_settings.py +0 -249
- package/.agents/skills/prototype/scripts/conftest.py +0 -15
- package/.agents/skills/prototype/scripts/launch_sandbox.py +0 -205
- package/.agents/skills/prototype/scripts/probe_sandbox_safety.py +0 -311
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/__init__.py +0 -1
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +0 -41
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +0 -23
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +0 -45
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +0 -10
- package/.agents/skills/prototype/scripts/test_build_sandbox_settings.py +0 -275
- package/.agents/skills/prototype/scripts/test_launch_sandbox.py +0 -303
- package/.agents/skills/prototype/scripts/test_probe_sandbox_safety.py +0 -312
- package/.agents/skills/prototype/workflows/promotion.md +0 -25
- package/.agents/skills/prototype/workflows/sandbox.md +0 -35
- package/.agents/skills/rebase/.claude/CLAUDE.md +0 -1
- package/.agents/skills/rebase/AGENTS.md +0 -1
- package/.agents/skills/rebase/SKILL.md +0 -178
- package/.agents/skills/review-router/SKILL.md +0 -51
- package/.agents/skills/review-router/reference/route-policy.json +0 -7
- package/.agents/skills/review-router/reference/task-seeds.md +0 -1
- package/.agents/skills/review-router/scripts/conftest.py +0 -4
- package/.agents/skills/review-router/scripts/review_router.py +0 -50
- package/.agents/skills/review-router/scripts/review_router_cli.py +0 -211
- package/.agents/skills/review-router/scripts/review_router_constants/__init__.py +0 -1
- package/.agents/skills/review-router/scripts/review_router_constants/config/__init__.py +0 -0
- package/.agents/skills/review-router/scripts/review_router_constants/config/constants.py +0 -13
- package/.agents/skills/review-router/scripts/test_review_router.py +0 -23
- package/.agents/skills/review-router/scripts/test_review_router_cli.py +0 -47
- package/.agents/skills/review-tier/SKILL.md +0 -56
- package/.agents/skills/review-tier/reference/task-seeds.md +0 -1
- package/.agents/skills/review-tier/reference/tier-policy.json +0 -9
- package/.agents/skills/review-tier/scripts/conftest.py +0 -4
- package/.agents/skills/review-tier/scripts/review_tier.py +0 -359
- package/.agents/skills/review-tier/scripts/review_tier_constants/__init__.py +0 -1
- package/.agents/skills/review-tier/scripts/review_tier_constants/config/__init__.py +0 -0
- package/.agents/skills/review-tier/scripts/review_tier_constants/config/constants.py +0 -29
- package/.agents/skills/review-tier/scripts/test_generation.py +0 -159
- package/.agents/skills/review-tier/scripts/test_review_tier.py +0 -33
- package/.agents/skills/run-claude-dev-env/SKILL.md +0 -84
- package/.agents/skills/session-log/.claude/CLAUDE.md +0 -1
- package/.agents/skills/session-log/AGENTS.md +0 -1
- package/.agents/skills/session-log/SKILL.md +0 -170
- package/.agents/skills/session-log/templates/frontmatter.md +0 -40
- package/.agents/skills/session-tidy/.claude/CLAUDE.md +0 -1
- package/.agents/skills/session-tidy/AGENTS.md +0 -1
- package/.agents/skills/session-tidy/SKILL.md +0 -191
- package/.agents/skills/skill-builder/references/.claude/CLAUDE.md +0 -1
- package/.agents/skills/skill-builder/references/AGENTS.md +0 -1
- package/.agents/skills/skill-builder/references/delegation-map.md +0 -130
- package/.agents/skills/skill-builder/references/description-field.md +0 -111
- package/.agents/skills/skill-builder/references/deterministic-elements.md +0 -218
- package/.agents/skills/skill-builder/references/progressive-disclosure.md +0 -122
- package/.agents/skills/skill-builder/references/self-audit-checklist.md +0 -126
- package/.agents/skills/skill-builder/references/skill-modularity.md +0 -111
- package/.agents/skills/skill-builder/references/skill-types.md +0 -232
- package/.agents/skills/skill-builder/references/thariq-x-post-skills.json +0 -33
- package/.agents/skills/skill-builder/templates/.claude/CLAUDE.md +0 -1
- package/.agents/skills/skill-builder/templates/AGENTS.md +0 -1
- package/.agents/skills/skill-builder/templates/gap-analysis.md +0 -87
- package/.agents/skills/skill-builder/workflows/.claude/CLAUDE.md +0 -1
- package/.agents/skills/skill-builder/workflows/AGENTS.md +0 -1
- package/.agents/skills/skill-builder/workflows/improve-skill.md +0 -136
- package/.agents/skills/skill-builder/workflows/new-skill.md +0 -179
- package/.agents/skills/skill-builder/workflows/polish-skill.md +0 -139
- package/.agents/skills/source-command-sr-loop/SKILL.md +0 -82
- package/.agents/skills/update/.claude/CLAUDE.md +0 -1
- package/.agents/skills/update/AGENTS.md +0 -1
- package/.agents/skills/update/SKILL.md +0 -190
- package/_shared/pr-loop/AGENTS.md +0 -1
- package/_shared/pr-loop/CLAUDE.md +0 -1
- package/_shared/pr-loop/audit-contract.md +0 -183
- package/_shared/pr-loop/audit-reply-template.md +0 -151
- package/_shared/pr-loop/code-rules-gate.md +0 -65
- package/_shared/pr-loop/fix-protocol.md +0 -60
- package/_shared/pr-loop/gh-payloads.md +0 -75
- package/_shared/pr-loop/post-audit-thread-contract.md +0 -51
- package/_shared/pr-loop/precatch-rubric.md +0 -66
- package/_shared/pr-loop/preflight-proposal.md +0 -56
- package/_shared/pr-loop/scripts/post_audit_thread.py +0 -1242
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/post_audit_thread_constants.py +0 -199
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +0 -1115
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +0 -129
- package/_shared/pr-loop/state-schema.md +0 -45
- package/_shared/pr-loop/worker-spawn.md +0 -189
- /package/{.agents/skills/_shared/pr-loop/scripts → scripts}/test_durable_post_lint.py +0 -0
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import importlib
|
|
4
|
+
import os
|
|
4
5
|
import sys
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from types import ModuleType
|
|
7
8
|
|
|
9
|
+
from shared_tree_paths import resolve_shared_scripts_directory
|
|
10
|
+
|
|
8
11
|
from . import adapter_configuration, adapter_detectors, adapter_pairing, adapter_support
|
|
9
12
|
from .config import constants
|
|
10
13
|
from .model import Diagnostic, Document, DocumentSet
|
|
@@ -24,10 +27,10 @@ def _hooks_module(module_name: str) -> ModuleType:
|
|
|
24
27
|
|
|
25
28
|
|
|
26
29
|
def _pr_loop_script_module(module_name: str) -> ModuleType:
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
)
|
|
30
|
+
scripts_directory = str(resolve_shared_scripts_directory(
|
|
31
|
+
__file__, os.environ, constants.PR_LOOP_DIRECTORY_NAME,
|
|
32
|
+
f"{module_name}{constants.PYTHON_SUFFIX}", constants.SHARED_ROOT_PARENT_INDEX
|
|
33
|
+
))
|
|
31
34
|
if scripts_directory not in sys.path:
|
|
32
35
|
sys.path.insert(0, scripts_directory)
|
|
33
36
|
return importlib.import_module(module_name)
|
|
@@ -5,6 +5,7 @@ INVALID_INPUT_EXIT_CODE = 2
|
|
|
5
5
|
GIT_EXECUTABLE = "git"
|
|
6
6
|
UTF8_ENCODING = "utf-8"
|
|
7
7
|
PATH_SEPARATOR = "/"
|
|
8
|
+
ARCHIVED_SKILLS_DIRECTORY_NAME = "skill-archive"
|
|
8
9
|
NUL_BYTE = b"\0"
|
|
9
10
|
ALL_GIT_ROOT_ARGUMENTS = ("rev-parse", "--show-toplevel")
|
|
10
11
|
ALL_GIT_HEAD_ARGUMENTS = ("rev-parse", "--verify", "HEAD")
|
|
@@ -13,6 +14,7 @@ GIT_MERGE_BASE_ARGUMENT = "merge-base"
|
|
|
13
14
|
GIT_HEAD_REFERENCE = "HEAD"
|
|
14
15
|
GIT_INDEX_REFERENCE_PREFIX = ""
|
|
15
16
|
GIT_ENVIRONMENT_VARIABLE_PREFIX = "GIT_"
|
|
17
|
+
GIT_INDEX_ENVIRONMENT_VARIABLE = "GIT_INDEX_FILE"
|
|
16
18
|
ALL_GIT_BLOB_ARGUMENTS = ("cat-file", "blob")
|
|
17
19
|
ALL_MISSING_BLOB_ERROR_FRAGMENTS = (
|
|
18
20
|
"does not exist in",
|
|
@@ -75,3 +77,6 @@ TERMINOLOGY_FINDING_PATTERN = re.compile(
|
|
|
75
77
|
TERMINOLOGY_PATH_GROUP = "path"
|
|
76
78
|
TERMINOLOGY_LINE_NUMBER_GROUP = "line_number"
|
|
77
79
|
TERMINOLOGY_MESSAGE_GROUP = "message"
|
|
80
|
+
|
|
81
|
+
PR_LOOP_DIRECTORY_NAME = "pr-loop"
|
|
82
|
+
SHARED_ROOT_PARENT_INDEX = 2
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
from collections.abc import Iterable, Sequence
|
|
4
|
-
from pathlib import Path
|
|
4
|
+
from pathlib import Path, PurePosixPath
|
|
5
5
|
|
|
6
|
+
from .config.constants import ARCHIVED_SKILLS_DIRECTORY_NAME
|
|
6
7
|
from .model import (
|
|
7
8
|
ChangeSetRule,
|
|
8
9
|
Diagnostic,
|
|
@@ -25,6 +26,9 @@ def lint(
|
|
|
25
26
|
) -> LintReport:
|
|
26
27
|
"""Run the selected rules on one source selection.
|
|
27
28
|
|
|
29
|
+
Repository and diff selections check active code. File and editor
|
|
30
|
+
selections include archived skills.
|
|
31
|
+
|
|
28
32
|
Args:
|
|
29
33
|
request: Repository root, source, and rule sets.
|
|
30
34
|
all_registry: Optional registry replacement.
|
|
@@ -32,10 +36,14 @@ def lint(
|
|
|
32
36
|
Returns:
|
|
33
37
|
A report of diagnostics and rule execution state.
|
|
34
38
|
"""
|
|
35
|
-
document_set = select_documents(request)
|
|
36
|
-
candidate_rules =
|
|
39
|
+
document_set = _runtime_document_set(select_documents(request))
|
|
40
|
+
candidate_rules = (
|
|
41
|
+
tuple(all_registry) if all_registry is not None else default_registry()
|
|
42
|
+
)
|
|
37
43
|
selected = selected_rules(candidate_rules, request.rule_sets)
|
|
38
|
-
all_diagnostics, executed, failed, skipped = _run_selected_rules(
|
|
44
|
+
all_diagnostics, executed, failed, skipped = _run_selected_rules(
|
|
45
|
+
selected, document_set
|
|
46
|
+
)
|
|
39
47
|
return LintReport(
|
|
40
48
|
1,
|
|
41
49
|
tuple(sorted(all_diagnostics, key=_diagnostic_sort_key)),
|
|
@@ -46,6 +54,42 @@ def lint(
|
|
|
46
54
|
)
|
|
47
55
|
|
|
48
56
|
|
|
57
|
+
def _is_runtime_path(path: PurePosixPath) -> bool:
|
|
58
|
+
return path.parts[0] != ARCHIVED_SKILLS_DIRECTORY_NAME
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _runtime_document_set(document_set: DocumentSet) -> DocumentSet:
|
|
62
|
+
if document_set.selection in {SelectionKind.FILES, SelectionKind.TEXT}:
|
|
63
|
+
return document_set
|
|
64
|
+
all_documents = tuple(
|
|
65
|
+
each_document
|
|
66
|
+
for each_document in document_set.documents
|
|
67
|
+
if _is_runtime_path(each_document.path)
|
|
68
|
+
)
|
|
69
|
+
all_deleted_paths = tuple(
|
|
70
|
+
each_path
|
|
71
|
+
for each_path in document_set.deleted_paths
|
|
72
|
+
if _is_runtime_path(each_path)
|
|
73
|
+
) + tuple(
|
|
74
|
+
old_path
|
|
75
|
+
for old_path, new_path in document_set.renamed_paths
|
|
76
|
+
if _is_runtime_path(old_path) and not _is_runtime_path(new_path)
|
|
77
|
+
)
|
|
78
|
+
all_renamed_paths = tuple(
|
|
79
|
+
each_pair
|
|
80
|
+
for each_pair in document_set.renamed_paths
|
|
81
|
+
if _is_runtime_path(each_pair[1])
|
|
82
|
+
)
|
|
83
|
+
return DocumentSet(
|
|
84
|
+
all_documents,
|
|
85
|
+
document_set.selection,
|
|
86
|
+
document_set.repository_root,
|
|
87
|
+
all_deleted_paths,
|
|
88
|
+
all_renamed_paths,
|
|
89
|
+
document_set.base_revision,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
49
93
|
def _run_selected_rules(
|
|
50
94
|
all_rules: tuple[Rule, ...], document_set: DocumentSet
|
|
51
95
|
) -> tuple[list[Diagnostic], tuple[str, ...], tuple[str, ...], tuple[str, ...]]:
|
|
@@ -141,7 +141,7 @@ def default_registry() -> tuple[model.Rule, ...]:
|
|
|
141
141
|
model.ChangeSetRule(
|
|
142
142
|
"terminology-sweep",
|
|
143
143
|
frozenset({"changed"}),
|
|
144
|
-
frozenset({model.SelectionKind.STAGED}),
|
|
144
|
+
frozenset({model.SelectionKind.STAGED, model.SelectionKind.BASE}),
|
|
145
145
|
adapters.terminology_diagnostics,
|
|
146
146
|
),
|
|
147
147
|
model.RepositoryRule(
|
|
@@ -172,7 +172,7 @@ def _documents_from_changes(
|
|
|
172
172
|
selection_kind = SelectionKind.STAGED if is_staged else SelectionKind.BASE
|
|
173
173
|
return DocumentSet(
|
|
174
174
|
tuple(all_documents), selection_kind, repository_root,
|
|
175
|
-
tuple(all_deleted_paths), tuple(all_renamed_paths),
|
|
175
|
+
tuple(all_deleted_paths), tuple(all_renamed_paths), base_revision,
|
|
176
176
|
)
|
|
177
177
|
|
|
178
178
|
|
|
@@ -354,6 +354,7 @@ def _normalize_document_set(repository_root: Path, document_set: DocumentSet) ->
|
|
|
354
354
|
repository_root,
|
|
355
355
|
all_deleted_paths,
|
|
356
356
|
all_renamed_paths,
|
|
357
|
+
document_set.base_revision,
|
|
357
358
|
)
|
|
358
359
|
|
|
359
360
|
|
|
@@ -10,6 +10,7 @@ from .config.constants import (
|
|
|
10
10
|
ALL_MISSING_BLOB_ERROR_FRAGMENTS,
|
|
11
11
|
ALL_UNBORN_HEAD_ERROR_FRAGMENTS,
|
|
12
12
|
GIT_ENVIRONMENT_VARIABLE_PREFIX,
|
|
13
|
+
GIT_INDEX_ENVIRONMENT_VARIABLE,
|
|
13
14
|
GIT_EXECUTABLE,
|
|
14
15
|
NUL_BYTE,
|
|
15
16
|
UTF8_ENCODING,
|
|
@@ -105,6 +106,7 @@ def _git_subprocess_environment() -> dict[str, str]:
|
|
|
105
106
|
each_name: each_environment_text
|
|
106
107
|
for each_name, each_environment_text in os.environ.items()
|
|
107
108
|
if not each_name.upper().startswith(git_prefix)
|
|
109
|
+
or each_name.upper() == GIT_INDEX_ENVIRONMENT_VARIABLE
|
|
108
110
|
}
|
|
109
111
|
|
|
110
112
|
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keep a pstack skills-directory plugin manifest listing every skill folder.
|
|
3
|
+
*
|
|
4
|
+
* Claude Code loads a folder under `~/.claude/skills` as a plugin when the
|
|
5
|
+
* folder holds `.claude-plugin/plugin.json`, and it namespaces that plugin's
|
|
6
|
+
* skills as `pstack:how`. It does not scan a plugin root for skill folders, so
|
|
7
|
+
* the manifest names each sub-skill path:
|
|
8
|
+
*
|
|
9
|
+
* plugin root holds: how/SKILL.md, why/SKILL.md, docs/, .claude-plugin/
|
|
10
|
+
* manifest before: "skills": ["./how"]
|
|
11
|
+
* manifest after: "skills": ["./how", "./why"] <- why/ picked up
|
|
12
|
+
* second run: no write <- the list matches
|
|
13
|
+
* docs/, .claude-plugin/ <- skipped, no SKILL.md
|
|
14
|
+
*
|
|
15
|
+
* The installer calls `refreshPstackPluginManifest` for a pstack folder the
|
|
16
|
+
* user installed. Running this file directly refreshes the same manifest after
|
|
17
|
+
* a pstack update.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
existsSync,
|
|
22
|
+
mkdirSync,
|
|
23
|
+
readdirSync,
|
|
24
|
+
readFileSync,
|
|
25
|
+
realpathSync,
|
|
26
|
+
writeFileSync,
|
|
27
|
+
} from 'node:fs';
|
|
28
|
+
import { homedir } from 'node:os';
|
|
29
|
+
import { join } from 'node:path';
|
|
30
|
+
import { fileURLToPath } from 'node:url';
|
|
31
|
+
|
|
32
|
+
export const PSTACK_PLUGIN_DIRECTORY_NAME = 'pstack';
|
|
33
|
+
export const PSTACK_PLUGIN_MANIFEST_RELATIVE_PATH = join('.claude-plugin', 'plugin.json');
|
|
34
|
+
export const SKILL_MANIFEST_FILE_NAME = 'SKILL.md';
|
|
35
|
+
export const SKILL_PATH_PREFIX = './';
|
|
36
|
+
|
|
37
|
+
const CLAUDE_HOME_DIRECTORY_NAME = '.claude';
|
|
38
|
+
const MANIFEST_SKILLS_KEY = 'skills';
|
|
39
|
+
const MANIFEST_INDENT_SPACES = 2;
|
|
40
|
+
const FILE_ENCODING = 'utf8';
|
|
41
|
+
const HIDDEN_NAME_PREFIX = '.';
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Fields a first write seeds from the published pstack plugin. An existing
|
|
45
|
+
* manifest keeps its own values. This refresher changes only the skills list.
|
|
46
|
+
*/
|
|
47
|
+
const SEEDED_MANIFEST_FIELDS = Object.freeze({
|
|
48
|
+
$schema: 'https://anthropic.com/claude-code/plugin.schema.json',
|
|
49
|
+
name: PSTACK_PLUGIN_DIRECTORY_NAME,
|
|
50
|
+
version: '0.14.8',
|
|
51
|
+
description:
|
|
52
|
+
'if you want to go fast, go deep first. pstack helps you write less, but higher '
|
|
53
|
+
+ 'quality code. rigorous agent workflows you can parallelize with confidence.',
|
|
54
|
+
author: Object.freeze({ name: 'Lauren Tan' }),
|
|
55
|
+
homepage: 'https://github.com/cursor/plugins/tree/main/pstack',
|
|
56
|
+
repository: 'https://github.com/cursor/plugins',
|
|
57
|
+
license: 'MIT',
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Name every sub-folder of a plugin root that Claude Code loads as a skill.
|
|
62
|
+
*
|
|
63
|
+
* @param {string} pluginRoot Plugin folder holding one sub-folder per skill.
|
|
64
|
+
* @returns {string[]} The sorted names of each visible sub-folder holding a
|
|
65
|
+
* skill manifest. A hidden folder and a folder without `SKILL.md` are left out.
|
|
66
|
+
*/
|
|
67
|
+
export function findPstackSkillDirectoryNames(pluginRoot) {
|
|
68
|
+
return readdirSync(pluginRoot, { withFileTypes: true })
|
|
69
|
+
.filter((eachEntry) => eachEntry.isDirectory())
|
|
70
|
+
.map((eachEntry) => eachEntry.name)
|
|
71
|
+
.filter((eachName) => !eachName.startsWith(HIDDEN_NAME_PREFIX))
|
|
72
|
+
.filter((eachName) => existsSync(join(pluginRoot, eachName, SKILL_MANIFEST_FILE_NAME)))
|
|
73
|
+
.sort();
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readManifestSkillPaths(manifestPath) {
|
|
77
|
+
if (!existsSync(manifestPath)) return { manifest: null, previousSkillPaths: [] };
|
|
78
|
+
const manifest = JSON.parse(readFileSync(manifestPath, FILE_ENCODING));
|
|
79
|
+
const listedSkillPaths = manifest[MANIFEST_SKILLS_KEY];
|
|
80
|
+
if (typeof listedSkillPaths === 'string') {
|
|
81
|
+
return { manifest, previousSkillPaths: [listedSkillPaths] };
|
|
82
|
+
}
|
|
83
|
+
if (Array.isArray(listedSkillPaths)) {
|
|
84
|
+
return { manifest, previousSkillPaths: listedSkillPaths };
|
|
85
|
+
}
|
|
86
|
+
return { manifest, previousSkillPaths: [] };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Rewrite a pstack plugin manifest's skills list to match its folder.
|
|
91
|
+
*
|
|
92
|
+
* Writes only when the list changes. A second call on an unchanged folder
|
|
93
|
+
* leaves the file byte for byte. An existing manifest keeps every field but
|
|
94
|
+
* the skills list.
|
|
95
|
+
*
|
|
96
|
+
* @param {string} pluginRoot Plugin folder holding the skill sub-folders.
|
|
97
|
+
* @returns {{didWrite: boolean, reason: string, previousSkillPaths: string[],
|
|
98
|
+
* currentSkillPaths: string[]}} Whether this call wrote, and the paths
|
|
99
|
+
* before and after. `reason` names a skip, and is empty on a write or an
|
|
100
|
+
* unchanged list.
|
|
101
|
+
*/
|
|
102
|
+
export function refreshPstackPluginManifest(pluginRoot) {
|
|
103
|
+
const declined = { didWrite: false, previousSkillPaths: [], currentSkillPaths: [] };
|
|
104
|
+
if (!existsSync(pluginRoot)) {
|
|
105
|
+
return { ...declined, reason: 'plugin root is absent' };
|
|
106
|
+
}
|
|
107
|
+
if (existsSync(join(pluginRoot, SKILL_MANIFEST_FILE_NAME))) {
|
|
108
|
+
return { ...declined, reason: 'plugin root is itself a skill' };
|
|
109
|
+
}
|
|
110
|
+
const allSkillNames = findPstackSkillDirectoryNames(pluginRoot);
|
|
111
|
+
if (allSkillNames.length === 0) {
|
|
112
|
+
return { ...declined, reason: 'plugin root holds no skills' };
|
|
113
|
+
}
|
|
114
|
+
const manifestPath = join(pluginRoot, PSTACK_PLUGIN_MANIFEST_RELATIVE_PATH);
|
|
115
|
+
const { manifest, previousSkillPaths } = readManifestSkillPaths(manifestPath);
|
|
116
|
+
const currentSkillPaths = allSkillNames.map((eachName) => SKILL_PATH_PREFIX + eachName);
|
|
117
|
+
const outcome = { reason: '', previousSkillPaths, currentSkillPaths };
|
|
118
|
+
const isUnchanged = manifest !== null
|
|
119
|
+
&& previousSkillPaths.length === currentSkillPaths.length
|
|
120
|
+
&& previousSkillPaths.every((eachPath, eachIndex) => eachPath === currentSkillPaths[eachIndex]);
|
|
121
|
+
if (isUnchanged) {
|
|
122
|
+
return { ...outcome, didWrite: false };
|
|
123
|
+
}
|
|
124
|
+
const refreshedManifest = {
|
|
125
|
+
...(manifest === null ? SEEDED_MANIFEST_FIELDS : manifest),
|
|
126
|
+
[MANIFEST_SKILLS_KEY]: currentSkillPaths,
|
|
127
|
+
};
|
|
128
|
+
mkdirSync(join(pluginRoot, '.claude-plugin'), { recursive: true });
|
|
129
|
+
writeFileSync(
|
|
130
|
+
manifestPath,
|
|
131
|
+
JSON.stringify(refreshedManifest, null, MANIFEST_INDENT_SPACES) + '\n',
|
|
132
|
+
FILE_ENCODING,
|
|
133
|
+
);
|
|
134
|
+
return { ...outcome, didWrite: true };
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Return the pstack plugin folder under a Claude home.
|
|
139
|
+
*
|
|
140
|
+
* @param {string} claudeHome The Claude home directory holding `skills`.
|
|
141
|
+
* @returns {string} The pstack plugin folder path, present or not.
|
|
142
|
+
*/
|
|
143
|
+
export function pstackPluginRootFor(claudeHome) {
|
|
144
|
+
return join(claudeHome, 'skills', PSTACK_PLUGIN_DIRECTORY_NAME);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Return the Claude home used when no folder argument is given.
|
|
149
|
+
*
|
|
150
|
+
* A named profile sets `CLAUDE_CONFIG_DIR`. The installer writes into that
|
|
151
|
+
* root. The command-line form reads the same variable.
|
|
152
|
+
*
|
|
153
|
+
* @param {Record<string, string|undefined>} environment The process environment.
|
|
154
|
+
* @returns {string} The Claude home directory holding `skills`.
|
|
155
|
+
*/
|
|
156
|
+
export function claudeHomeFrom(environment) {
|
|
157
|
+
return environment.CLAUDE_CONFIG_DIR || join(homedir(), CLAUDE_HOME_DIRECTORY_NAME);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function main() {
|
|
161
|
+
const explicitRoot = process.argv[2];
|
|
162
|
+
const pluginRoot = explicitRoot || pstackPluginRootFor(claudeHomeFrom(process.env));
|
|
163
|
+
const outcome = refreshPstackPluginManifest(pluginRoot);
|
|
164
|
+
if (!outcome.didWrite && outcome.reason) {
|
|
165
|
+
console.log(`No manifest written: ${outcome.reason}.`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (!outcome.didWrite) {
|
|
169
|
+
console.log(`No change. ${outcome.currentSkillPaths.length} skills listed.`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
console.log(`Updated. ${outcome.currentSkillPaths.length} skills listed.`);
|
|
173
|
+
for (const eachPath of outcome.currentSkillPaths) {
|
|
174
|
+
if (!outcome.previousSkillPaths.includes(eachPath)) console.log(` added ${eachPath}`);
|
|
175
|
+
}
|
|
176
|
+
for (const eachPath of outcome.previousSkillPaths) {
|
|
177
|
+
if (!outcome.currentSkillPaths.includes(eachPath)) console.log(` removed ${eachPath}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Return whether node ran this module as its entry point.
|
|
183
|
+
*
|
|
184
|
+
* A directory link to this script gives node an invoked path that differs from
|
|
185
|
+
* this module's own path. Both sides resolve to their real location before
|
|
186
|
+
* the comparison.
|
|
187
|
+
*
|
|
188
|
+
* @param {string|undefined} invokedPath The path node reports as `argv[1]`.
|
|
189
|
+
* @returns {boolean} True when node ran this module as its entry point.
|
|
190
|
+
*/
|
|
191
|
+
function isEntryPoint(invokedPath) {
|
|
192
|
+
if (!invokedPath) return false;
|
|
193
|
+
try {
|
|
194
|
+
return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(invokedPath);
|
|
195
|
+
} catch {
|
|
196
|
+
return fileURLToPath(import.meta.url) === invokedPath;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
if (isEntryPoint(process.argv[1])) main();
|
|
@@ -11,7 +11,9 @@ from collections.abc import Mapping
|
|
|
11
11
|
from pathlib import Path
|
|
12
12
|
|
|
13
13
|
from dev_env_scripts_constants.shared_tree_constants import (
|
|
14
|
+
AGENTS_DIRECTORY_SUFFIX,
|
|
14
15
|
CLAUDE_CONFIG_DIR_ENV_VAR,
|
|
16
|
+
DEFAULT_MANAGED_ROOT_NAME,
|
|
15
17
|
PROCESS_TREE_DIRECTORY_NAME,
|
|
16
18
|
PROCESS_TREE_KILL_MODULE_FILENAME,
|
|
17
19
|
SCRIPTS_DIRECTORY_NAME,
|
|
@@ -29,6 +31,29 @@ def _shared_scripts_directory(from_root: Path, shared_subpackage_name: str) -> P
|
|
|
29
31
|
)
|
|
30
32
|
|
|
31
33
|
|
|
34
|
+
def _linked_managed_root(module_path: Path, anchor_depth: int) -> Path | None:
|
|
35
|
+
"""Resolve the managed root whose scripts pointer targets this installed tree.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
module_path: The module path inside the managed scripts tree.
|
|
39
|
+
anchor_depth: Parent index of the package or agents root.
|
|
40
|
+
|
|
41
|
+
Returns:
|
|
42
|
+
The Claude managed root when its scripts pointer resolves to this tree.
|
|
43
|
+
"""
|
|
44
|
+
package_root = module_path.resolve().parents[anchor_depth]
|
|
45
|
+
if not package_root.name.endswith(AGENTS_DIRECTORY_SUFFIX):
|
|
46
|
+
return None
|
|
47
|
+
managed_name = package_root.name.removesuffix(AGENTS_DIRECTORY_SUFFIX) or DEFAULT_MANAGED_ROOT_NAME
|
|
48
|
+
managed_root = package_root.parent / managed_name
|
|
49
|
+
linked_scripts = managed_root / SCRIPTS_DIRECTORY_NAME
|
|
50
|
+
if not linked_scripts.is_dir():
|
|
51
|
+
return None
|
|
52
|
+
if linked_scripts.resolve() != package_root / SCRIPTS_DIRECTORY_NAME:
|
|
53
|
+
return None
|
|
54
|
+
return managed_root
|
|
55
|
+
|
|
56
|
+
|
|
32
57
|
def resolve_shared_scripts_directory(
|
|
33
58
|
module_file: str | Path,
|
|
34
59
|
all_environment: Mapping[str, str],
|
|
@@ -44,9 +69,10 @@ def resolve_shared_scripts_directory(
|
|
|
44
69
|
ok: an un-resolved parent keeps the link side, where _shared sits
|
|
45
70
|
flag: a resolved parent walks the link and misses _shared entirely
|
|
46
71
|
|
|
47
|
-
Tries the un-resolved parent first, then
|
|
48
|
-
then the
|
|
49
|
-
un-resolved candidate
|
|
72
|
+
Tries the un-resolved parent first, then a verified installed scripts
|
|
73
|
+
pointer, then the configured managed root, then the resolved parent.
|
|
74
|
+
If none holds the marker, returns the un-resolved candidate so an
|
|
75
|
+
import failure names a readable path.
|
|
50
76
|
|
|
51
77
|
Args:
|
|
52
78
|
module_file: Path of the importing module (``__file__``).
|
|
@@ -63,6 +89,9 @@ def resolve_shared_scripts_directory(
|
|
|
63
89
|
module_path.absolute().parents[anchor_depth], shared_subpackage_name
|
|
64
90
|
)
|
|
65
91
|
all_candidates = [un_resolved_candidate]
|
|
92
|
+
linked_root = _linked_managed_root(module_path, anchor_depth)
|
|
93
|
+
if linked_root is not None:
|
|
94
|
+
all_candidates.append(_shared_scripts_directory(linked_root, shared_subpackage_name))
|
|
66
95
|
configured_root_text = all_environment.get(CLAUDE_CONFIG_DIR_ENV_VAR, "").strip()
|
|
67
96
|
if configured_root_text:
|
|
68
97
|
all_candidates.append(
|
|
@@ -171,3 +171,36 @@ def test_generalized_resolver_serves_a_deeper_anchor(tmp_path: Path) -> None:
|
|
|
171
171
|
3,
|
|
172
172
|
)
|
|
173
173
|
assert resolved_scripts_directory == expected_scripts_directory
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@pytest.mark.parametrize("managed_name", (".claude", "profile-a"))
|
|
177
|
+
def test_resolved_agents_scripts_find_the_verified_managed_root(
|
|
178
|
+
tmp_path: Path, managed_name: str,
|
|
179
|
+
) -> None:
|
|
180
|
+
agents_name = ".agents" if managed_name == ".claude" else f"{managed_name}.agents"
|
|
181
|
+
agents_root = tmp_path / agents_name
|
|
182
|
+
scripts_directory = agents_root / "scripts"
|
|
183
|
+
scripts_directory.mkdir(parents=True)
|
|
184
|
+
module_file = scripts_directory / "runner.py"
|
|
185
|
+
module_file.write_text("", encoding="utf-8")
|
|
186
|
+
managed_root = tmp_path / managed_name
|
|
187
|
+
managed_root.mkdir()
|
|
188
|
+
_create_directory_link(from_link=managed_root / "scripts", to_target=scripts_directory)
|
|
189
|
+
expected = _write_process_tree_kill_stub(managed_root)
|
|
190
|
+
resolved_directory = shared_tree_paths.resolve_shared_process_tree_scripts_directory(module_file, {})
|
|
191
|
+
assert resolved_directory == expected
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def test_neighboring_managed_root_cannot_supply_a_foreign_shared_tree(tmp_path: Path) -> None:
|
|
195
|
+
agents_root = tmp_path / ".agents"
|
|
196
|
+
scripts_directory = agents_root / "scripts"
|
|
197
|
+
scripts_directory.mkdir(parents=True)
|
|
198
|
+
module_file = scripts_directory / "runner.py"
|
|
199
|
+
module_file.write_text("", encoding="utf-8")
|
|
200
|
+
managed_root = tmp_path / ".claude"
|
|
201
|
+
foreign_scripts = managed_root / "scripts"
|
|
202
|
+
foreign_scripts.mkdir(parents=True)
|
|
203
|
+
_write_process_tree_kill_stub(managed_root)
|
|
204
|
+
resolved_directory = shared_tree_paths.resolve_shared_process_tree_scripts_directory(module_file, {})
|
|
205
|
+
expected = agents_root / "_shared" / "process-tree" / "scripts"
|
|
206
|
+
assert resolved_directory == expected
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Archive boundaries for automatic and explicitly requested policy checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
from pathlib import Path, PurePosixPath
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
from policy_lint.engine import _runtime_document_set, lint
|
|
11
|
+
from policy_lint.model import (
|
|
12
|
+
Diagnostic,
|
|
13
|
+
Document,
|
|
14
|
+
DocumentRule,
|
|
15
|
+
DocumentSet,
|
|
16
|
+
LintRequest,
|
|
17
|
+
Location,
|
|
18
|
+
SelectionKind,
|
|
19
|
+
Severity,
|
|
20
|
+
TextDocument,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _git(repository_root: Path, *arguments: str) -> None:
|
|
25
|
+
environment = {
|
|
26
|
+
variable_name: variable_text
|
|
27
|
+
for variable_name, variable_text in os.environ.items()
|
|
28
|
+
if not variable_name.upper().startswith("GIT_")
|
|
29
|
+
}
|
|
30
|
+
subprocess.run(
|
|
31
|
+
["git", *arguments], cwd=repository_root, env=environment,
|
|
32
|
+
check=True, capture_output=True, text=True,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@pytest.fixture
|
|
37
|
+
def archive_repository(tmp_path: Path) -> Path:
|
|
38
|
+
_git(tmp_path, "init", "--quiet", "-b", "main")
|
|
39
|
+
_git(tmp_path, "config", "user.name", "Archive Tests")
|
|
40
|
+
_git(tmp_path, "config", "user.email", "archive@example.invalid")
|
|
41
|
+
_git(tmp_path, "config", "commit.gpgsign", "false")
|
|
42
|
+
for relative_path in ("active.py", "skill-archive/old.py", "src/skill-archive/live.py"):
|
|
43
|
+
file_path = tmp_path / relative_path
|
|
44
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
45
|
+
file_path.write_text("first\n", encoding="utf-8")
|
|
46
|
+
_git(tmp_path, "add", ".")
|
|
47
|
+
_git(tmp_path, "commit", "--quiet", "-m", "archive fixture")
|
|
48
|
+
for file_path in tmp_path.rglob("*.py"):
|
|
49
|
+
file_path.write_text("second\n", encoding="utf-8")
|
|
50
|
+
_git(tmp_path, "add", ".")
|
|
51
|
+
return tmp_path
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _marker_diagnostics(document: Document, repository_root: Path) -> tuple[Diagnostic, ...]:
|
|
55
|
+
assert (repository_root / document.path).is_file()
|
|
56
|
+
return (Diagnostic("marker", Severity.ERROR, "Checked document", Location(document.path, 1)),)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _accept_document(document: Document) -> bool:
|
|
60
|
+
return bool(document.path.parts)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _marker_rule() -> DocumentRule:
|
|
64
|
+
return DocumentRule(
|
|
65
|
+
"marker", frozenset({"changed", "repository"}),
|
|
66
|
+
_accept_document, _marker_diagnostics,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@pytest.mark.parametrize("selection", (SelectionKind.STAGED, SelectionKind.BASE, SelectionKind.REPOSITORY))
|
|
71
|
+
def test_automatic_checks_keep_live_code_and_exclude_only_root_archive(
|
|
72
|
+
archive_repository: Path, selection: SelectionKind,
|
|
73
|
+
) -> None:
|
|
74
|
+
requests = {
|
|
75
|
+
SelectionKind.STAGED: LintRequest.staged(archive_repository),
|
|
76
|
+
SelectionKind.BASE: LintRequest.base(archive_repository, "HEAD"),
|
|
77
|
+
SelectionKind.REPOSITORY: LintRequest.repository(archive_repository),
|
|
78
|
+
}
|
|
79
|
+
report = lint(requests[selection], all_registry=(_marker_rule(),))
|
|
80
|
+
assert report.checked_documents == (
|
|
81
|
+
PurePosixPath("active.py"), PurePosixPath("src/skill-archive/live.py"),
|
|
82
|
+
)
|
|
83
|
+
assert len(report.diagnostics) == 2
|
|
84
|
+
assert report.executed_rules == ("marker",)
|
|
85
|
+
assert report.exit_code == 1
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.mark.parametrize("selection", (SelectionKind.FILES, SelectionKind.TEXT))
|
|
89
|
+
def test_explicit_archive_checks_still_execute_rules(
|
|
90
|
+
archive_repository: Path, selection: SelectionKind,
|
|
91
|
+
) -> None:
|
|
92
|
+
archive_path = Path("skill-archive/old.py")
|
|
93
|
+
request = (
|
|
94
|
+
LintRequest.files(archive_repository, [archive_path])
|
|
95
|
+
if selection is SelectionKind.FILES
|
|
96
|
+
else LintRequest(archive_repository, TextDocument(archive_path, "editor text\n"))
|
|
97
|
+
)
|
|
98
|
+
report = lint(request, all_registry=(_marker_rule(),))
|
|
99
|
+
assert report.checked_documents == (PurePosixPath("skill-archive/old.py"),)
|
|
100
|
+
assert len(report.diagnostics) == 1
|
|
101
|
+
assert report.exit_code == 1
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_retirement_and_restoration_preserve_live_change_metadata(tmp_path: Path) -> None:
|
|
105
|
+
retired_path = PurePosixPath("active/retired.py")
|
|
106
|
+
archive_path = PurePosixPath("skill-archive/retired.py")
|
|
107
|
+
restored_source = PurePosixPath("skill-archive/restored.py")
|
|
108
|
+
restored_path = PurePosixPath("active/restored.py")
|
|
109
|
+
deleted_path = PurePosixPath("active/deleted.py")
|
|
110
|
+
document_set = DocumentSet(
|
|
111
|
+
(Document.from_text(archive_path, "archived"), Document.from_text(restored_path, "restored")),
|
|
112
|
+
SelectionKind.BASE,
|
|
113
|
+
tmp_path,
|
|
114
|
+
(deleted_path, PurePosixPath("skill-archive/deleted.py")),
|
|
115
|
+
((retired_path, archive_path), (restored_source, restored_path)),
|
|
116
|
+
)
|
|
117
|
+
runtime_set = _runtime_document_set(document_set)
|
|
118
|
+
assert tuple(document.path for document in runtime_set.documents) == (restored_path,)
|
|
119
|
+
assert runtime_set.deleted_paths == (deleted_path, retired_path)
|
|
120
|
+
assert runtime_set.renamed_paths == ((restored_source, restored_path),)
|
|
@@ -10,7 +10,7 @@ from pathlib import Path, PurePosixPath
|
|
|
10
10
|
import pytest
|
|
11
11
|
from policy_lint import adapters
|
|
12
12
|
from policy_lint.config.constants import INCOMPLETE_EXIT_CODE
|
|
13
|
-
from policy_lint.engine import lint
|
|
13
|
+
from policy_lint.engine import _runtime_document_set, lint
|
|
14
14
|
from policy_lint.model import (
|
|
15
15
|
ContentOrigin,
|
|
16
16
|
Diagnostic,
|
|
@@ -309,7 +309,11 @@ def test_state_description_diagnostics_maps_and_scopes_phrases(
|
|
|
309
309
|
lint_report = _lint_documents(
|
|
310
310
|
tmp_path,
|
|
311
311
|
(source_document,),
|
|
312
|
-
(
|
|
312
|
+
(
|
|
313
|
+
_changed_document_rule(
|
|
314
|
+
"state-description", adapters.state_description_diagnostics
|
|
315
|
+
),
|
|
316
|
+
),
|
|
313
317
|
)
|
|
314
318
|
assert len(lint_report.diagnostics) == 1
|
|
315
319
|
assert lint_report.diagnostics[0].location is not None
|
|
@@ -351,8 +355,12 @@ def test_failed_adapter_rolls_back_continues_and_exits_incomplete(
|
|
|
351
355
|
_source_document("pkg/second.py"),
|
|
352
356
|
)
|
|
353
357
|
all_rules = (
|
|
354
|
-
_changed_document_rule(
|
|
355
|
-
|
|
358
|
+
_changed_document_rule(
|
|
359
|
+
"flaky", lambda document, _root: _flaky_diagnostics(document)
|
|
360
|
+
),
|
|
361
|
+
_changed_document_rule(
|
|
362
|
+
"stable", lambda document, _root: _stable_diagnostics(document)
|
|
363
|
+
),
|
|
356
364
|
)
|
|
357
365
|
lint_report = _lint_documents(repository_root, all_documents, all_rules)
|
|
358
366
|
assert lint_report.failed_rules == ("flaky",)
|
|
@@ -379,3 +387,16 @@ def test_process_interrupt_is_not_swallowed(tmp_path: Path) -> None:
|
|
|
379
387
|
(_source_document("pkg/mod.py"),),
|
|
380
388
|
(_changed_document_rule("interrupt", interrupt_diagnostics),),
|
|
381
389
|
)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def test_runtime_document_set_keeps_base_revision_for_base_selection(
|
|
393
|
+
tmp_path: Path,
|
|
394
|
+
) -> None:
|
|
395
|
+
document_set = DocumentSet(
|
|
396
|
+
(_source_document("pkg/mod.py"),),
|
|
397
|
+
SelectionKind.BASE,
|
|
398
|
+
tmp_path,
|
|
399
|
+
base_revision="abc123def",
|
|
400
|
+
)
|
|
401
|
+
runtime_document_set = _runtime_document_set(document_set)
|
|
402
|
+
assert runtime_document_set.base_revision == "abc123def"
|