claude-dev-env 4.2.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 +7 -0
- package/.agents/skills/pull-request/SKILL.md +216 -0
- package/.agents/skills/pull-request/reference/publication-tasks.md +18 -0
- package/.agents/skills/pull-request/scripts/github_pr_command_constants/__init__.py +1 -0
- package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/__init__.py +1 -0
- package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/constants.py +69 -0
- package/.agents/skills/pull-request/scripts/pull_request.py +265 -0
- package/.agents/skills/pull-request/scripts/recover_legacy_author.py +251 -0
- package/.agents/skills/pull-request/scripts/test_github_pr_command_constants.py +58 -0
- package/.agents/skills/pull-request/scripts/test_pull_request.py +396 -0
- package/.agents/skills/pull-request/scripts/test_recover_legacy_author.py +221 -0
- package/.agents/skills/skill-builder/SKILL.md +3 -196
- package/.agents/skills/test-runner/SKILL.md +69 -0
- package/.agents/skills/test-runner/scripts/preflight_checks.py +365 -0
- package/.agents/skills/test-runner/scripts/run_tests.py +316 -0
- package/.agents/skills/test-runner/scripts/test_preflight_checks.py +185 -0
- package/.agents/skills/test-runner/scripts/test_run_tests.py +281 -0
- package/.agents/skills/test-runner/scripts/test_runner_constants/__init__.py +1 -0
- package/.agents/skills/test-runner/scripts/test_runner_constants/config/__init__.py +1 -0
- package/.agents/skills/test-runner/scripts/test_runner_constants/config/constants.py +96 -0
- package/.agents/skills-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
- 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/ever-shipped-skills.mjs +2 -0
- package/bin/ever-shipped-skills.test.mjs +8 -0
- package/bin/install.agents-home.test.mjs +15 -13
- package/bin/install.mjs +37 -1
- package/bin/install.policy-lint.test.mjs +78 -0
- package/bin/install.prune.test.mjs +149 -20
- package/bin/install.pstack-plugin.test.mjs +114 -0
- package/bin/install.test.mjs +61 -7
- 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/pii_prevention_blocker.py +32 -51
- package/hooks/blocking/test_bash_post_call_dispatcher.py +55 -17
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +1 -4
- package/hooks/blocking/test_code_rules_command_dispatch.py +2 -2
- 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_pii_prevention_blocker.py +37 -89
- 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/gh_post_body_texts.py +195 -0
- 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 -16
- package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +14 -2
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -8
- package/hooks/hooks_constants/gh_post_body_texts_constants.py +12 -0
- 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 +17 -7
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +0 -7
- package/hooks/hooks_constants/test_failure_recorder_constants.py +3 -3
- package/hooks/hooks_constants/test_gh_post_body_texts_constants.py +19 -0
- 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/hooks/observability/test_failure_recorder.py +1 -1
- package/hooks/pyproject.toml +1 -1
- package/hooks/test_gh_post_body_texts.py +79 -0
- package/package.json +2 -2
- package/rules/durable-post-artifacts.md +32 -20
- package/rules/gh-cli-conventions.md +23 -13
- package/rules/git-workflow.md +21 -8
- 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/scripts/durable_post_lint.py +206 -0
- package/scripts/durable_post_lint_config/config/constants.py +96 -0
- package/scripts/durable_post_lint_config/config/test_constants.py +25 -0
- package/scripts/policy_lint/adapter_configuration.py +53 -12
- 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/repository_checks/config/constants.py +0 -5
- package/scripts/shared_tree_paths.py +32 -3
- package/scripts/test_durable_post_lint.py +332 -0
- package/scripts/test_shared_tree_paths.py +33 -0
- package/scripts/tests/test_adapter_configuration.py +54 -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/hooks/_gh_pr_author_swap_utils.py +0 -1225
- package/hooks/blocking/conventional_pr_title_gate.py +0 -443
- package/hooks/blocking/gh_body_arg_blocker.py +0 -176
- package/hooks/blocking/gh_pr_author_enforcer.py +0 -487
- package/hooks/blocking/gh_pr_author_restore.py +0 -105
- package/hooks/blocking/pr_description_writer_gate.py +0 -256
- package/hooks/blocking/test_conventional_pr_title_gate.py +0 -640
- package/hooks/blocking/test_gh_body_arg_blocker.py +0 -441
- package/hooks/blocking/test_gh_pr_author_enforcer.py +0 -1166
- package/hooks/blocking/test_gh_pr_author_restore.py +0 -513
- package/hooks/blocking/test_gh_pr_author_swap_utils.py +0 -910
- package/hooks/blocking/test_pr_description_writer_gate.py +0 -234
- package/hooks/blocking/test_preflight_check.py +0 -343
- package/hooks/blocking/test_test_preflight_check.py +0 -80
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +0 -322
- package/hooks/blocking/volatile_path_in_post_blocker.py +0 -461
- package/hooks/hooks_constants/conventional_pr_title_gate_constants.py +0 -58
- package/hooks/hooks_constants/gh_pr_author_swap_constants.py +0 -76
- package/hooks/hooks_constants/pr_description_writer_gate_constants.py +0 -73
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +0 -55
- package/hooks/observability/pr_description_writer_spawn_tracker.py +0 -113
- package/hooks/observability/test_pr_description_writer_spawn_tracker.py +0 -150
- package/hooks/session/gh_pr_author_session_cleanup.py +0 -175
- package/hooks/session/test_gh_pr_author_session_cleanup.py +0 -576
- package/hooks/test__gh_pr_author_swap_utils.py +0 -333
|
@@ -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();
|
|
@@ -131,11 +131,6 @@ ALL_TRACKED_SECRET_EXACT_EXEMPTIONS: frozenset[tuple[str, str, str]] = frozenset
|
|
|
131
131
|
"home-path",
|
|
132
132
|
"09f83c224a02c370d3d63de5c330e1a830fc92866330fc7963b5b5fd20ffcb91",
|
|
133
133
|
),
|
|
134
|
-
(
|
|
135
|
-
"packages/claude-dev-env/hooks/blocking/volatile_path_in_post_blocker.py",
|
|
136
|
-
"home-path",
|
|
137
|
-
"ed9203c8078e4c6b832600085f28f24656d679aa8dca82ecc2557afc3df25036",
|
|
138
|
-
),
|
|
139
134
|
(
|
|
140
135
|
"packages/claude-dev-env/hooks/hooks_constants/hardcoded_user_path_constants.py",
|
|
141
136
|
"home-path",
|
|
@@ -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(
|
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
"""Tests for action-aware durable GitHub post linting."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
import pytest
|
|
10
|
+
|
|
11
|
+
_SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
|
|
12
|
+
if str(_SCRIPTS_DIRECTORY) not in sys.path:
|
|
13
|
+
sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
|
|
14
|
+
|
|
15
|
+
import durable_post_lint
|
|
16
|
+
|
|
17
|
+
SCRIPT_PATH = _SCRIPTS_DIRECTORY / "durable_post_lint.py"
|
|
18
|
+
VALID_PR_BODY = """## Summary
|
|
19
|
+
|
|
20
|
+
Summary text.
|
|
21
|
+
|
|
22
|
+
## Description
|
|
23
|
+
|
|
24
|
+
Description text.
|
|
25
|
+
|
|
26
|
+
## Why
|
|
27
|
+
|
|
28
|
+
Why text.
|
|
29
|
+
|
|
30
|
+
## How
|
|
31
|
+
|
|
32
|
+
How text.
|
|
33
|
+
|
|
34
|
+
## Verification
|
|
35
|
+
|
|
36
|
+
Verification text.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@pytest.mark.parametrize(
|
|
41
|
+
("action", "title", "body_text"),
|
|
42
|
+
[
|
|
43
|
+
("pr-create", "feat: add durable post linting", VALID_PR_BODY),
|
|
44
|
+
("pr-edit", "fix(cli): validate post input", None),
|
|
45
|
+
("pr-comment", None, "Comment body."),
|
|
46
|
+
("pr-review", None, "Review body."),
|
|
47
|
+
("issue-create", None, "Issue body."),
|
|
48
|
+
("issue-edit", None, "Updated issue body."),
|
|
49
|
+
("issue-comment", None, "Issue comment."),
|
|
50
|
+
("github-mcp-post", None, "MCP body."),
|
|
51
|
+
],
|
|
52
|
+
)
|
|
53
|
+
def test_all_actions_accept_valid_inputs(
|
|
54
|
+
action: str, title: str | None, body_text: str | None
|
|
55
|
+
) -> None:
|
|
56
|
+
all_findings = durable_post_lint.lint_durable_post(
|
|
57
|
+
action=action,
|
|
58
|
+
title=title,
|
|
59
|
+
body_text=body_text,
|
|
60
|
+
)
|
|
61
|
+
assert all_findings == ()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def test_unknown_action_raises_usage_error() -> None:
|
|
65
|
+
with pytest.raises(durable_post_lint.DurablePostUsageError):
|
|
66
|
+
durable_post_lint.lint_durable_post(
|
|
67
|
+
action="release-create",
|
|
68
|
+
title=None,
|
|
69
|
+
body_text="Release body.",
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@pytest.mark.parametrize(
|
|
74
|
+
("title", "body_text"),
|
|
75
|
+
[
|
|
76
|
+
(None, VALID_PR_BODY),
|
|
77
|
+
("feat: add durable post linting", None),
|
|
78
|
+
],
|
|
79
|
+
)
|
|
80
|
+
def test_pr_create_requires_title_and_body(
|
|
81
|
+
title: str | None, body_text: str | None
|
|
82
|
+
) -> None:
|
|
83
|
+
with pytest.raises(durable_post_lint.DurablePostUsageError):
|
|
84
|
+
durable_post_lint.lint_durable_post(
|
|
85
|
+
action="pr-create",
|
|
86
|
+
title=title,
|
|
87
|
+
body_text=body_text,
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_pr_edit_requires_one_edit_field() -> None:
|
|
92
|
+
with pytest.raises(durable_post_lint.DurablePostUsageError):
|
|
93
|
+
durable_post_lint.lint_durable_post(
|
|
94
|
+
action="pr-edit",
|
|
95
|
+
title=None,
|
|
96
|
+
body_text=None,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@pytest.mark.parametrize(
|
|
101
|
+
"action",
|
|
102
|
+
[
|
|
103
|
+
"pr-comment",
|
|
104
|
+
"pr-review",
|
|
105
|
+
"issue-create",
|
|
106
|
+
"issue-edit",
|
|
107
|
+
"issue-comment",
|
|
108
|
+
"github-mcp-post",
|
|
109
|
+
],
|
|
110
|
+
)
|
|
111
|
+
def test_body_actions_require_body(action: str) -> None:
|
|
112
|
+
with pytest.raises(durable_post_lint.DurablePostUsageError):
|
|
113
|
+
durable_post_lint.lint_durable_post(
|
|
114
|
+
action=action,
|
|
115
|
+
title=None,
|
|
116
|
+
body_text=None,
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@pytest.mark.parametrize(
|
|
121
|
+
"title",
|
|
122
|
+
[
|
|
123
|
+
"feat: add durable post linting",
|
|
124
|
+
"fix(cli): validate body files",
|
|
125
|
+
"refactor!: replace post validation",
|
|
126
|
+
"docs(skill)!: explain durable posts",
|
|
127
|
+
],
|
|
128
|
+
)
|
|
129
|
+
def test_pr_titles_accept_repository_conventional_forms(title: str) -> None:
|
|
130
|
+
all_findings = durable_post_lint.lint_durable_post(
|
|
131
|
+
action="pr-edit",
|
|
132
|
+
title=title,
|
|
133
|
+
body_text=None,
|
|
134
|
+
)
|
|
135
|
+
assert all_findings == ()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@pytest.mark.parametrize(
|
|
139
|
+
"title",
|
|
140
|
+
[
|
|
141
|
+
"Add durable post linting",
|
|
142
|
+
"feature: add durable post linting",
|
|
143
|
+
"fix(): add durable post linting",
|
|
144
|
+
"fix:add durable post linting",
|
|
145
|
+
"fix: ",
|
|
146
|
+
],
|
|
147
|
+
)
|
|
148
|
+
def test_pr_titles_reject_nonconventional_forms(title: str) -> None:
|
|
149
|
+
all_findings = durable_post_lint.lint_durable_post(
|
|
150
|
+
action="pr-edit",
|
|
151
|
+
title=title,
|
|
152
|
+
body_text=None,
|
|
153
|
+
)
|
|
154
|
+
assert [each_finding.code for each_finding in all_findings] == ["invalid-pr-title"]
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@pytest.mark.parametrize(
|
|
158
|
+
"missing_heading",
|
|
159
|
+
["Summary", "Description", "Why", "How", "Verification"],
|
|
160
|
+
)
|
|
161
|
+
def test_pr_body_reports_each_missing_heading(missing_heading: str) -> None:
|
|
162
|
+
body_text = VALID_PR_BODY.replace(f"## {missing_heading}\n", "")
|
|
163
|
+
all_findings = durable_post_lint.lint_durable_post(
|
|
164
|
+
action="pr-create",
|
|
165
|
+
title="feat: add durable post linting",
|
|
166
|
+
body_text=body_text,
|
|
167
|
+
)
|
|
168
|
+
assert [each_finding.message for each_finding in all_findings] == [
|
|
169
|
+
f"body is missing required heading: {missing_heading}"
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def test_non_pr_body_needs_no_pr_headings() -> None:
|
|
174
|
+
all_findings = durable_post_lint.lint_durable_post(
|
|
175
|
+
action="issue-comment",
|
|
176
|
+
title=None,
|
|
177
|
+
body_text="A concise issue comment.",
|
|
178
|
+
)
|
|
179
|
+
assert all_findings == ()
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@pytest.mark.parametrize(
|
|
183
|
+
("body_text", "expected_marker"),
|
|
184
|
+
[
|
|
185
|
+
(
|
|
186
|
+
r"See C:\Users\example\.claude-profile-a\jobs\job-1\artifact.png",
|
|
187
|
+
".claude-profile-a/jobs/",
|
|
188
|
+
),
|
|
189
|
+
(
|
|
190
|
+
r"Edited C:\Users\example\.claude\worktrees\feature\file.py",
|
|
191
|
+
".claude/worktrees/",
|
|
192
|
+
),
|
|
193
|
+
(
|
|
194
|
+
r"Saved in C:\Users\example\AppData\Local\Temp\report.txt",
|
|
195
|
+
"appdata/local/temp",
|
|
196
|
+
),
|
|
197
|
+
("Saved in /tmp/report.txt", "/tmp/"),
|
|
198
|
+
(r"Saved in %TEMP%\report.txt", "%temp%"),
|
|
199
|
+
(r"Saved in $env:TEMP\report.txt", "$env:temp"),
|
|
200
|
+
("Saved in $CLAUDE_JOB_DIR/report.txt", "$claude_job_dir"),
|
|
201
|
+
],
|
|
202
|
+
)
|
|
203
|
+
def test_volatile_path_markers_match_hook_behavior(
|
|
204
|
+
body_text: str, expected_marker: str
|
|
205
|
+
) -> None:
|
|
206
|
+
assert durable_post_lint.find_volatile_path_marker(body_text) == expected_marker
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
@pytest.mark.parametrize(
|
|
210
|
+
"body_text",
|
|
211
|
+
[
|
|
212
|
+
"the `.claude/worktrees/` prefix is a manifest key",
|
|
213
|
+
".claude/worktrees/ entries are skipped by the manifest",
|
|
214
|
+
"(.claude-profile-a/jobs/) is a directory name",
|
|
215
|
+
"worktrees live under `.claude/worktrees/<name>`",
|
|
216
|
+
"the constant is `.claude/worktrees/`. Next.",
|
|
217
|
+
],
|
|
218
|
+
)
|
|
219
|
+
def test_anchored_marker_near_misses_remain_allowed(body_text: str) -> None:
|
|
220
|
+
assert durable_post_lint.find_volatile_path_marker(body_text) is None
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_volatile_body_returns_content_finding() -> None:
|
|
224
|
+
all_findings = durable_post_lint.lint_durable_post(
|
|
225
|
+
action="issue-comment",
|
|
226
|
+
title=None,
|
|
227
|
+
body_text="Saved in /tmp/report.txt",
|
|
228
|
+
)
|
|
229
|
+
assert [each_finding.code for each_finding in all_findings] == [
|
|
230
|
+
"volatile-local-path"
|
|
231
|
+
]
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_read_body_file_accepts_utf8(tmp_path: Path) -> None:
|
|
235
|
+
body_file = tmp_path / "body.md"
|
|
236
|
+
body_file.write_text("Café", encoding="utf-8")
|
|
237
|
+
assert durable_post_lint.read_body_file(body_file) == "Café"
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def test_read_body_file_rejects_missing_file(tmp_path: Path) -> None:
|
|
241
|
+
missing_file = tmp_path / "missing.md"
|
|
242
|
+
with pytest.raises(durable_post_lint.DurablePostInputError):
|
|
243
|
+
durable_post_lint.read_body_file(missing_file)
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def test_read_body_file_rejects_invalid_utf8(tmp_path: Path) -> None:
|
|
247
|
+
body_file = tmp_path / "body.md"
|
|
248
|
+
body_file.write_bytes(b"\xff\xfe")
|
|
249
|
+
with pytest.raises(durable_post_lint.DurablePostInputError):
|
|
250
|
+
durable_post_lint.read_body_file(body_file)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def test_cli_returns_clean_exit_for_valid_pr_create(tmp_path: Path) -> None:
|
|
254
|
+
body_file = tmp_path / "body.md"
|
|
255
|
+
body_file.write_text(VALID_PR_BODY, encoding="utf-8")
|
|
256
|
+
completed_process = subprocess.run(
|
|
257
|
+
[
|
|
258
|
+
sys.executable,
|
|
259
|
+
str(SCRIPT_PATH),
|
|
260
|
+
"--action",
|
|
261
|
+
"pr-create",
|
|
262
|
+
"--title",
|
|
263
|
+
"feat: add durable post linting",
|
|
264
|
+
"--body-file",
|
|
265
|
+
str(body_file),
|
|
266
|
+
],
|
|
267
|
+
check=False,
|
|
268
|
+
capture_output=True,
|
|
269
|
+
text=True,
|
|
270
|
+
)
|
|
271
|
+
assert completed_process.returncode == 0
|
|
272
|
+
assert completed_process.stdout == ""
|
|
273
|
+
assert completed_process.stderr == ""
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def test_cli_returns_content_exit_without_echoing_body(tmp_path: Path) -> None:
|
|
277
|
+
body_text = "private body words saved in /tmp/report.txt"
|
|
278
|
+
body_file = tmp_path / "body.md"
|
|
279
|
+
body_file.write_text(body_text, encoding="utf-8")
|
|
280
|
+
completed_process = subprocess.run(
|
|
281
|
+
[
|
|
282
|
+
sys.executable,
|
|
283
|
+
str(SCRIPT_PATH),
|
|
284
|
+
"--action",
|
|
285
|
+
"issue-comment",
|
|
286
|
+
"--body-file",
|
|
287
|
+
str(body_file),
|
|
288
|
+
],
|
|
289
|
+
check=False,
|
|
290
|
+
capture_output=True,
|
|
291
|
+
text=True,
|
|
292
|
+
)
|
|
293
|
+
assert completed_process.returncode == 1
|
|
294
|
+
assert "volatile local artifact path" in completed_process.stderr
|
|
295
|
+
assert body_text not in completed_process.stderr
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def test_cli_returns_usage_exit_for_missing_body_file(tmp_path: Path) -> None:
|
|
299
|
+
missing_file = tmp_path / "missing.md"
|
|
300
|
+
completed_process = subprocess.run(
|
|
301
|
+
[
|
|
302
|
+
sys.executable,
|
|
303
|
+
str(SCRIPT_PATH),
|
|
304
|
+
"--action",
|
|
305
|
+
"issue-comment",
|
|
306
|
+
"--body-file",
|
|
307
|
+
str(missing_file),
|
|
308
|
+
],
|
|
309
|
+
check=False,
|
|
310
|
+
capture_output=True,
|
|
311
|
+
text=True,
|
|
312
|
+
)
|
|
313
|
+
assert completed_process.returncode == 2
|
|
314
|
+
assert "body file is not readable" in completed_process.stderr
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def test_cli_has_no_inline_body_flag() -> None:
|
|
318
|
+
completed_process = subprocess.run(
|
|
319
|
+
[
|
|
320
|
+
sys.executable,
|
|
321
|
+
str(SCRIPT_PATH),
|
|
322
|
+
"--action",
|
|
323
|
+
"issue-comment",
|
|
324
|
+
"--body",
|
|
325
|
+
"Inline body.",
|
|
326
|
+
],
|
|
327
|
+
check=False,
|
|
328
|
+
capture_output=True,
|
|
329
|
+
text=True,
|
|
330
|
+
)
|
|
331
|
+
assert completed_process.returncode == 2
|
|
332
|
+
assert "unrecognized arguments" in completed_process.stderr
|
|
@@ -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
|