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
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""Django and Playwright readiness checks for check_server_health, check_django_database,
|
|
2
|
+
check_test_db_flag, check_runserver_port_conflicts, and build_frontend."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import subprocess
|
|
7
|
+
from collections.abc import Sequence
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from urllib.parse import urlparse
|
|
10
|
+
|
|
11
|
+
from test_runner_constants.config.constants import (
|
|
12
|
+
ALL_BLOCKED_STATUS_CODES,
|
|
13
|
+
ALL_COLLECTSTATIC_ARGUMENTS,
|
|
14
|
+
ALL_FRONTEND_BUILD_ARGUMENTS,
|
|
15
|
+
ALL_LOCAL_SERVER_HOSTS,
|
|
16
|
+
ALL_PROCESS_INFORMATION_FIELDS,
|
|
17
|
+
ALL_RUNSERVER_OPTIONS_WITH_VALUES,
|
|
18
|
+
BUILD_TIMEOUT_SECONDS,
|
|
19
|
+
COMMAND_ARGUMENT_SEPARATOR,
|
|
20
|
+
CURL_EXECUTABLE_NAME,
|
|
21
|
+
CURL_MAX_TIME_FLAG,
|
|
22
|
+
CURL_OUTPUT_FLAG,
|
|
23
|
+
CURL_SILENT_FLAG,
|
|
24
|
+
CURL_STATUS_FORMAT,
|
|
25
|
+
CURL_WRITE_OUT_FLAG,
|
|
26
|
+
DEFAULT_DJANGO_PORT,
|
|
27
|
+
DIRECTORY_LIST_SEPARATOR,
|
|
28
|
+
DJANGO_DATABASE_FILENAME,
|
|
29
|
+
EXIT_CODE_SUCCESS,
|
|
30
|
+
FRONTEND_BUILD_FAILED_MESSAGE,
|
|
31
|
+
FRONTEND_DIRECTORY_NAME,
|
|
32
|
+
HEALTH_CHECK_ERROR_TEMPLATE,
|
|
33
|
+
HEALTH_CHECK_TIMEOUT_SECONDS,
|
|
34
|
+
MANAGE_PY_FILENAME,
|
|
35
|
+
MINIMUM_PROCESS_ARGUMENT_COUNT,
|
|
36
|
+
MINIMUM_RUNSERVER_PROCESS_COUNT,
|
|
37
|
+
MISSING_DATABASE_ERROR_TEMPLATE,
|
|
38
|
+
MISSING_TEST_DATABASE_FLAG_TEMPLATE,
|
|
39
|
+
NULL_DEVICE_PATH,
|
|
40
|
+
PORT_CONFLICT_ERROR_TEMPLATE,
|
|
41
|
+
PROCESS_CURL_TIMEOUT_SECONDS,
|
|
42
|
+
PROCESS_LIST_ARGUMENTS,
|
|
43
|
+
PROCESS_LIST_RUNSERVER_TOKEN,
|
|
44
|
+
PROCESS_LIST_SELF_FILTER_TOKEN,
|
|
45
|
+
RUNSERVER_COMMAND_INDEX,
|
|
46
|
+
RUNSERVER_COMMAND_NAME,
|
|
47
|
+
TEST_DATABASE_FLAG,
|
|
48
|
+
UNREACHABLE_ERROR_TEMPLATE,
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
import psutil
|
|
53
|
+
except ImportError:
|
|
54
|
+
psutil = None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def check_server_health(target_url: str) -> str | None:
|
|
58
|
+
"""Return a diagnostic when the target server is unavailable.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
target_url: HTTP endpoint to request.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
A diagnostic string when the endpoint cannot serve tests, otherwise None.
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
completed_process = subprocess.run(
|
|
68
|
+
_build_curl_arguments(target_url),
|
|
69
|
+
capture_output=True,
|
|
70
|
+
text=True,
|
|
71
|
+
timeout=HEALTH_CHECK_TIMEOUT_SECONDS,
|
|
72
|
+
check=False,
|
|
73
|
+
)
|
|
74
|
+
http_status_code = int(completed_process.stdout.strip())
|
|
75
|
+
except (OSError, ValueError, subprocess.TimeoutExpired):
|
|
76
|
+
return UNREACHABLE_ERROR_TEMPLATE.format(target_url)
|
|
77
|
+
return _server_health_error(target_url, http_status_code)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _build_curl_arguments(target_url: str) -> list[str]:
|
|
81
|
+
return [
|
|
82
|
+
CURL_EXECUTABLE_NAME,
|
|
83
|
+
CURL_SILENT_FLAG,
|
|
84
|
+
CURL_OUTPUT_FLAG,
|
|
85
|
+
NULL_DEVICE_PATH,
|
|
86
|
+
CURL_WRITE_OUT_FLAG,
|
|
87
|
+
CURL_STATUS_FORMAT,
|
|
88
|
+
CURL_MAX_TIME_FLAG,
|
|
89
|
+
str(PROCESS_CURL_TIMEOUT_SECONDS),
|
|
90
|
+
target_url,
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _server_health_error(target_url: str, http_status_code: int) -> str | None:
|
|
95
|
+
if http_status_code == EXIT_CODE_SUCCESS:
|
|
96
|
+
return UNREACHABLE_ERROR_TEMPLATE.format(target_url)
|
|
97
|
+
if http_status_code in ALL_BLOCKED_STATUS_CODES:
|
|
98
|
+
return HEALTH_CHECK_ERROR_TEMPLATE.format(
|
|
99
|
+
target_url, f"HTTP {http_status_code}"
|
|
100
|
+
)
|
|
101
|
+
return None
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def check_django_database(project_root: Path) -> str | None:
|
|
105
|
+
"""Return a diagnostic when a Django database file is missing.
|
|
106
|
+
|
|
107
|
+
Args:
|
|
108
|
+
project_root: Django project directory to inspect.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
A diagnostic when manage.py exists without db.sqlite3, otherwise None.
|
|
112
|
+
"""
|
|
113
|
+
manage_py_path = project_root / MANAGE_PY_FILENAME
|
|
114
|
+
if not manage_py_path.is_file():
|
|
115
|
+
return None
|
|
116
|
+
database_path = project_root / DJANGO_DATABASE_FILENAME
|
|
117
|
+
if database_path.is_file():
|
|
118
|
+
return None
|
|
119
|
+
return MISSING_DATABASE_ERROR_TEMPLATE.format(
|
|
120
|
+
DJANGO_DATABASE_FILENAME, project_root
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def extract_port_from_url(target_url: str) -> str:
|
|
125
|
+
"""Return the URL port used by Django diagnostics.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
target_url: URL whose authority may carry a port.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
The port in the URL, or the Django default when the URL has no valid port.
|
|
132
|
+
"""
|
|
133
|
+
try:
|
|
134
|
+
parsed_url = urlparse(target_url)
|
|
135
|
+
if parsed_url.port is not None:
|
|
136
|
+
return str(parsed_url.port)
|
|
137
|
+
except ValueError:
|
|
138
|
+
return DEFAULT_DJANGO_PORT
|
|
139
|
+
return DEFAULT_DJANGO_PORT
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def check_test_db_flag(target_url: str, project_root: Path) -> str | None:
|
|
143
|
+
"""Return a diagnostic when a detected runserver is missing `--test-db`.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
target_url: URL that identifies the expected Django port.
|
|
147
|
+
project_root: Django project that owns the selected test run.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
A diagnostic when runserver is found without --test-db, otherwise None.
|
|
151
|
+
"""
|
|
152
|
+
if not (project_root / MANAGE_PY_FILENAME).is_file():
|
|
153
|
+
return None
|
|
154
|
+
if (process_listing := _read_process_listing()) is None:
|
|
155
|
+
return None
|
|
156
|
+
target_port = extract_port_from_url(target_url)
|
|
157
|
+
is_runserver_found = False
|
|
158
|
+
for each_line in process_listing.splitlines():
|
|
159
|
+
if PROCESS_LIST_RUNSERVER_TOKEN not in each_line:
|
|
160
|
+
continue
|
|
161
|
+
if PROCESS_LIST_SELF_FILTER_TOKEN in each_line:
|
|
162
|
+
continue
|
|
163
|
+
runserver_arguments = _extract_runserver_arguments(each_line)
|
|
164
|
+
if not _runserver_uses_target_port(runserver_arguments, target_port):
|
|
165
|
+
continue
|
|
166
|
+
is_runserver_found = True
|
|
167
|
+
if TEST_DATABASE_FLAG in runserver_arguments:
|
|
168
|
+
return None
|
|
169
|
+
if not is_runserver_found:
|
|
170
|
+
return None
|
|
171
|
+
return MISSING_TEST_DATABASE_FLAG_TEMPLATE.format(target_port, target_port)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _extract_runserver_arguments(process_line: str) -> tuple[str, ...]:
|
|
175
|
+
all_process_tokens = tuple(process_line.split())
|
|
176
|
+
try:
|
|
177
|
+
runserver_index = all_process_tokens.index(RUNSERVER_COMMAND_NAME)
|
|
178
|
+
except ValueError:
|
|
179
|
+
return ()
|
|
180
|
+
return all_process_tokens[runserver_index + 1 :]
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _runserver_uses_target_port(
|
|
184
|
+
all_runserver_arguments: Sequence[str], target_port: str
|
|
185
|
+
) -> bool:
|
|
186
|
+
endpoint_argument = _find_runserver_endpoint_argument(all_runserver_arguments)
|
|
187
|
+
if endpoint_argument is None:
|
|
188
|
+
return target_port == DEFAULT_DJANGO_PORT
|
|
189
|
+
return endpoint_argument == target_port or endpoint_argument.endswith(
|
|
190
|
+
f":{target_port}"
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def _find_runserver_endpoint_argument(
|
|
195
|
+
all_runserver_arguments: Sequence[str],
|
|
196
|
+
) -> str | None:
|
|
197
|
+
should_skip_next_argument = False
|
|
198
|
+
for each_argument in all_runserver_arguments:
|
|
199
|
+
if should_skip_next_argument:
|
|
200
|
+
should_skip_next_argument = False
|
|
201
|
+
continue
|
|
202
|
+
if each_argument in ALL_RUNSERVER_OPTIONS_WITH_VALUES:
|
|
203
|
+
should_skip_next_argument = True
|
|
204
|
+
continue
|
|
205
|
+
if not each_argument.startswith("-"):
|
|
206
|
+
return each_argument
|
|
207
|
+
return None
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _read_process_listing() -> str | None:
|
|
211
|
+
try:
|
|
212
|
+
completed_process = subprocess.run(
|
|
213
|
+
list(PROCESS_LIST_ARGUMENTS),
|
|
214
|
+
capture_output=True,
|
|
215
|
+
text=True,
|
|
216
|
+
timeout=PROCESS_CURL_TIMEOUT_SECONDS,
|
|
217
|
+
check=False,
|
|
218
|
+
)
|
|
219
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
220
|
+
return None
|
|
221
|
+
return completed_process.stdout or ""
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _get_runserver_processes_on_port(
|
|
225
|
+
target_port: str,
|
|
226
|
+
) -> list[tuple[int, str]]:
|
|
227
|
+
if psutil is None:
|
|
228
|
+
return []
|
|
229
|
+
all_runserver_processes: list[tuple[int, str]] = []
|
|
230
|
+
port_token = f":{target_port}"
|
|
231
|
+
for each_process in psutil.process_iter(ALL_PROCESS_INFORMATION_FIELDS):
|
|
232
|
+
process_record = _read_runserver_process(each_process, port_token)
|
|
233
|
+
if process_record is not None:
|
|
234
|
+
all_runserver_processes.append(process_record)
|
|
235
|
+
return all_runserver_processes
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _read_runserver_process(
|
|
239
|
+
process: object,
|
|
240
|
+
port_token: str,
|
|
241
|
+
) -> tuple[int, str] | None:
|
|
242
|
+
try:
|
|
243
|
+
process_information = getattr(process, "info", {})
|
|
244
|
+
if not isinstance(process_information, dict):
|
|
245
|
+
return None
|
|
246
|
+
commandline_parts = process_information.get("cmdline") or []
|
|
247
|
+
if len(commandline_parts) < MINIMUM_PROCESS_ARGUMENT_COUNT:
|
|
248
|
+
return None
|
|
249
|
+
if commandline_parts[1] != MANAGE_PY_FILENAME:
|
|
250
|
+
return None
|
|
251
|
+
if commandline_parts[RUNSERVER_COMMAND_INDEX] != RUNSERVER_COMMAND_NAME:
|
|
252
|
+
return None
|
|
253
|
+
if port_token not in COMMAND_ARGUMENT_SEPARATOR.join(commandline_parts):
|
|
254
|
+
return None
|
|
255
|
+
process_identifier = process_information.get("pid")
|
|
256
|
+
if not isinstance(process_identifier, int):
|
|
257
|
+
return None
|
|
258
|
+
return (process_identifier, process_information.get("cwd") or "")
|
|
259
|
+
except (
|
|
260
|
+
KeyError,
|
|
261
|
+
psutil.AccessDenied,
|
|
262
|
+
psutil.NoSuchProcess,
|
|
263
|
+
psutil.ZombieProcess,
|
|
264
|
+
):
|
|
265
|
+
return None
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def check_runserver_port_conflicts(
|
|
269
|
+
target_url: str,
|
|
270
|
+
project_root: Path,
|
|
271
|
+
) -> str | None:
|
|
272
|
+
"""Return a diagnostic for cross-worktree runserver conflicts.
|
|
273
|
+
|
|
274
|
+
Args:
|
|
275
|
+
target_url: URL whose host and port identify the server.
|
|
276
|
+
project_root: Project directory running the test command.
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
A diagnostic when separate worktrees share the port, otherwise None.
|
|
280
|
+
"""
|
|
281
|
+
parsed_target_url = urlparse(target_url)
|
|
282
|
+
if (parsed_target_url.hostname or "") not in ALL_LOCAL_SERVER_HOSTS:
|
|
283
|
+
return None
|
|
284
|
+
target_port = str(parsed_target_url.port or DEFAULT_DJANGO_PORT)
|
|
285
|
+
all_processes = _get_runserver_processes_on_port(target_port)
|
|
286
|
+
all_directories = _unique_process_directories(all_processes)
|
|
287
|
+
if _has_no_process_conflict(all_processes, all_directories):
|
|
288
|
+
return None
|
|
289
|
+
return _build_conflict_message(target_port, project_root, all_directories)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _unique_process_directories(
|
|
293
|
+
all_processes: Sequence[tuple[int, str]],
|
|
294
|
+
) -> set[str]:
|
|
295
|
+
return {
|
|
296
|
+
str(Path(each_directory).resolve())
|
|
297
|
+
for _, each_directory in all_processes
|
|
298
|
+
if each_directory
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def _has_no_process_conflict(
|
|
303
|
+
all_processes: Sequence[tuple[int, str]],
|
|
304
|
+
all_directories: set[str],
|
|
305
|
+
) -> bool:
|
|
306
|
+
return (
|
|
307
|
+
len(all_processes) < MINIMUM_RUNSERVER_PROCESS_COUNT
|
|
308
|
+
or len(all_directories) < MINIMUM_RUNSERVER_PROCESS_COUNT
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _build_conflict_message(
|
|
313
|
+
target_port: str,
|
|
314
|
+
project_root: Path,
|
|
315
|
+
all_directories: set[str],
|
|
316
|
+
) -> str:
|
|
317
|
+
project_root_realpath = str(project_root.resolve())
|
|
318
|
+
all_other_worktrees = sorted(
|
|
319
|
+
each_directory
|
|
320
|
+
for each_directory in all_directories
|
|
321
|
+
if each_directory != project_root_realpath
|
|
322
|
+
)
|
|
323
|
+
all_conflicting_directories = all_other_worktrees or sorted(all_directories)
|
|
324
|
+
return PORT_CONFLICT_ERROR_TEMPLATE.format(
|
|
325
|
+
target_port, DIRECTORY_LIST_SEPARATOR.join(all_conflicting_directories)
|
|
326
|
+
)
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def build_frontend(project_root: Path) -> str | None:
|
|
330
|
+
"""Build the frontend and collect Django static files when present.
|
|
331
|
+
|
|
332
|
+
Args:
|
|
333
|
+
project_root: Project directory containing the optional frontend.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
A diagnostic when either preparation step fails, otherwise None.
|
|
337
|
+
"""
|
|
338
|
+
frontend_path = project_root / FRONTEND_DIRECTORY_NAME
|
|
339
|
+
if not frontend_path.is_dir():
|
|
340
|
+
return None
|
|
341
|
+
if _run_preparation(ALL_FRONTEND_BUILD_ARGUMENTS, frontend_path):
|
|
342
|
+
return FRONTEND_BUILD_FAILED_MESSAGE
|
|
343
|
+
if not (project_root / MANAGE_PY_FILENAME).is_file():
|
|
344
|
+
return None
|
|
345
|
+
if _run_preparation(ALL_COLLECTSTATIC_ARGUMENTS, project_root):
|
|
346
|
+
return FRONTEND_BUILD_FAILED_MESSAGE
|
|
347
|
+
return None
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _run_preparation(
|
|
351
|
+
all_arguments: Sequence[str],
|
|
352
|
+
working_directory: Path,
|
|
353
|
+
) -> bool:
|
|
354
|
+
try:
|
|
355
|
+
completed_process = subprocess.run(
|
|
356
|
+
list(all_arguments),
|
|
357
|
+
cwd=working_directory,
|
|
358
|
+
capture_output=True,
|
|
359
|
+
text=True,
|
|
360
|
+
timeout=BUILD_TIMEOUT_SECONDS,
|
|
361
|
+
check=False,
|
|
362
|
+
)
|
|
363
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
364
|
+
return True
|
|
365
|
+
return completed_process.returncode != EXIT_CODE_SUCCESS
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Run pytest or Playwright after Django and Playwright readiness checks.
|
|
3
|
+
|
|
4
|
+
::
|
|
5
|
+
|
|
6
|
+
python run_tests.py -- python -m pytest tests/
|
|
7
|
+
python run_tests.py -- npx playwright test tests/account.spec.ts
|
|
8
|
+
|
|
9
|
+
The selected child receives its arguments unchanged, inherits this process's
|
|
10
|
+
environment and output streams, and runs from the selected project directory.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import os
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
from collections.abc import Sequence
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import NoReturn
|
|
23
|
+
|
|
24
|
+
import preflight_checks
|
|
25
|
+
from test_runner_constants.config.constants import (
|
|
26
|
+
ALL_EXECUTABLE_SUFFIXES,
|
|
27
|
+
ALL_URL_TRAILING_PUNCTUATION,
|
|
28
|
+
ARGUMENT_ASSIGNMENT_SEPARATOR,
|
|
29
|
+
BASE_URL_FLAG,
|
|
30
|
+
COMMAND_SEPARATOR,
|
|
31
|
+
DEFAULT_DJANGO_URL,
|
|
32
|
+
DEFAULT_PLAYWRIGHT_URL,
|
|
33
|
+
ERROR_CHILD_COMMAND_REQUIRED,
|
|
34
|
+
ERROR_CHILD_LAUNCH_TEMPLATE,
|
|
35
|
+
ERROR_COMMAND_SEPARATOR_REQUIRED,
|
|
36
|
+
ERROR_PREFLIGHT_FAILURE_TEMPLATE,
|
|
37
|
+
ERROR_PROJECT_NOT_DIRECTORY_TEMPLATE,
|
|
38
|
+
ERROR_UNSUPPORTED_RUNNER,
|
|
39
|
+
EXIT_CODE_FAILURE,
|
|
40
|
+
EXIT_CODE_INVALID_ARGUMENTS,
|
|
41
|
+
MANAGE_PY_FILENAME,
|
|
42
|
+
MINIMUM_NPX_ARGUMENT_COUNT,
|
|
43
|
+
MINIMUM_PLAYWRIGHT_ARGUMENT_COUNT,
|
|
44
|
+
NPX_EXECUTABLE_NAME,
|
|
45
|
+
PLAYWRIGHT_EXECUTABLE_NAME,
|
|
46
|
+
PLAYWRIGHT_RUNNER_NAME,
|
|
47
|
+
PLAYWRIGHT_TEST_COMMAND_INDEX,
|
|
48
|
+
PLAYWRIGHT_TEST_SUBCOMMAND,
|
|
49
|
+
PROJECT_FLAG,
|
|
50
|
+
PWD_ENVIRONMENT_NAME,
|
|
51
|
+
PYTEST_EXECUTABLE_PREFIX,
|
|
52
|
+
PYTEST_RUNNER_NAME,
|
|
53
|
+
PYTHON_EXECUTABLE_PREFIX,
|
|
54
|
+
PYTHON_MODULE_FLAG,
|
|
55
|
+
SERVER_URL_PATTERN,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
build_frontend = preflight_checks.build_frontend
|
|
59
|
+
check_django_database = preflight_checks.check_django_database
|
|
60
|
+
check_runserver_port_conflicts = preflight_checks.check_runserver_port_conflicts
|
|
61
|
+
check_server_health = preflight_checks.check_server_health
|
|
62
|
+
check_test_db_flag = preflight_checks.check_test_db_flag
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class SelectedRunner:
|
|
67
|
+
"""The supported runner selected from the child arguments."""
|
|
68
|
+
|
|
69
|
+
name: str
|
|
70
|
+
is_playwright: bool
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class _ArgumentParser(argparse.ArgumentParser):
|
|
74
|
+
def error(self, message: str) -> NoReturn:
|
|
75
|
+
raise ValueError(message)
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _build_argument_parser() -> argparse.ArgumentParser:
|
|
79
|
+
parser = _ArgumentParser(add_help=True)
|
|
80
|
+
parser.add_argument(PROJECT_FLAG, type=Path, default=None)
|
|
81
|
+
return parser
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _split_command_arguments(
|
|
85
|
+
all_arguments: Sequence[str],
|
|
86
|
+
) -> tuple[list[str], list[str]]:
|
|
87
|
+
all_arguments_as_list = list(all_arguments)
|
|
88
|
+
if COMMAND_SEPARATOR not in all_arguments_as_list:
|
|
89
|
+
raise ValueError(ERROR_COMMAND_SEPARATOR_REQUIRED)
|
|
90
|
+
separator_index = all_arguments_as_list.index(COMMAND_SEPARATOR)
|
|
91
|
+
launcher_arguments = all_arguments_as_list[:separator_index]
|
|
92
|
+
child_arguments = all_arguments_as_list[separator_index + 1 :]
|
|
93
|
+
if not child_arguments:
|
|
94
|
+
raise ValueError(ERROR_CHILD_COMMAND_REQUIRED)
|
|
95
|
+
return launcher_arguments, child_arguments
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _resolve_project_root(maybe_project_path: Path | None) -> Path:
|
|
99
|
+
selected_path = maybe_project_path or Path(
|
|
100
|
+
os.environ.get(PWD_ENVIRONMENT_NAME, os.getcwd())
|
|
101
|
+
)
|
|
102
|
+
resolved_path = selected_path.expanduser().resolve()
|
|
103
|
+
if not resolved_path.is_dir():
|
|
104
|
+
raise ValueError(ERROR_PROJECT_NOT_DIRECTORY_TEMPLATE.format(resolved_path))
|
|
105
|
+
return resolved_path
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _parse_command_arguments(
|
|
109
|
+
all_arguments: Sequence[str],
|
|
110
|
+
) -> tuple[Path, list[str]]:
|
|
111
|
+
launcher_arguments, child_arguments = _split_command_arguments(all_arguments)
|
|
112
|
+
parsed_arguments = _build_argument_parser().parse_args(launcher_arguments)
|
|
113
|
+
project_root = _resolve_project_root(parsed_arguments.project)
|
|
114
|
+
return project_root, child_arguments
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _basename_without_suffix(executable_path: str) -> str:
|
|
118
|
+
executable_name = Path(executable_path).name.casefold()
|
|
119
|
+
for each_suffix in ALL_EXECUTABLE_SUFFIXES:
|
|
120
|
+
if executable_name.endswith(each_suffix):
|
|
121
|
+
return executable_name.removesuffix(each_suffix)
|
|
122
|
+
return executable_name
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _is_python_module_runner(
|
|
126
|
+
all_child_arguments: Sequence[str],
|
|
127
|
+
) -> bool:
|
|
128
|
+
executable_name = _basename_without_suffix(all_child_arguments[0])
|
|
129
|
+
if not executable_name.startswith(PYTHON_EXECUTABLE_PREFIX):
|
|
130
|
+
return False
|
|
131
|
+
for each_index, each_argument in enumerate(all_child_arguments[1:], start=1):
|
|
132
|
+
if each_argument != PYTHON_MODULE_FLAG:
|
|
133
|
+
continue
|
|
134
|
+
return (
|
|
135
|
+
each_index + 1 < len(all_child_arguments)
|
|
136
|
+
and all_child_arguments[each_index + 1] == PYTEST_RUNNER_NAME
|
|
137
|
+
)
|
|
138
|
+
return False
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _is_pytest_executable(executable_path: str) -> bool:
|
|
142
|
+
return _basename_without_suffix(executable_path).startswith(
|
|
143
|
+
PYTEST_EXECUTABLE_PREFIX
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _is_playwright_executable(
|
|
148
|
+
all_child_arguments: Sequence[str],
|
|
149
|
+
) -> bool:
|
|
150
|
+
executable_name = _basename_without_suffix(all_child_arguments[0])
|
|
151
|
+
if executable_name == PLAYWRIGHT_EXECUTABLE_NAME:
|
|
152
|
+
return (
|
|
153
|
+
len(all_child_arguments) >= MINIMUM_PLAYWRIGHT_ARGUMENT_COUNT
|
|
154
|
+
and all_child_arguments[1] == PLAYWRIGHT_TEST_SUBCOMMAND
|
|
155
|
+
)
|
|
156
|
+
return (
|
|
157
|
+
executable_name == NPX_EXECUTABLE_NAME
|
|
158
|
+
and len(all_child_arguments) >= MINIMUM_NPX_ARGUMENT_COUNT
|
|
159
|
+
and all_child_arguments[1] == PLAYWRIGHT_EXECUTABLE_NAME
|
|
160
|
+
and all_child_arguments[PLAYWRIGHT_TEST_COMMAND_INDEX]
|
|
161
|
+
== PLAYWRIGHT_TEST_SUBCOMMAND
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def select_runner(all_child_arguments: Sequence[str]) -> SelectedRunner:
|
|
166
|
+
"""Select one supported runner from the child arguments.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
all_child_arguments: Child executable and its arguments.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
The selected runner classification.
|
|
173
|
+
|
|
174
|
+
Raises:
|
|
175
|
+
ValueError: The child arguments are empty or unsupported.
|
|
176
|
+
"""
|
|
177
|
+
if not all_child_arguments:
|
|
178
|
+
raise ValueError(ERROR_CHILD_COMMAND_REQUIRED)
|
|
179
|
+
if _is_playwright_executable(all_child_arguments):
|
|
180
|
+
return SelectedRunner(PLAYWRIGHT_RUNNER_NAME, True)
|
|
181
|
+
if _is_pytest_executable(all_child_arguments[0]) or _is_python_module_runner(
|
|
182
|
+
all_child_arguments
|
|
183
|
+
):
|
|
184
|
+
return SelectedRunner(PYTEST_RUNNER_NAME, False)
|
|
185
|
+
raise ValueError(ERROR_UNSUPPORTED_RUNNER)
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def extract_target_url(
|
|
189
|
+
all_child_arguments: Sequence[str],
|
|
190
|
+
is_playwright: bool,
|
|
191
|
+
) -> str:
|
|
192
|
+
"""Return a URL from the child arguments, or the runner default.
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
all_child_arguments: Child executable and its arguments.
|
|
196
|
+
is_playwright: Whether to use the Playwright default URL.
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
A URL from the child arguments, or the runner default.
|
|
200
|
+
"""
|
|
201
|
+
for each_index, each_argument in enumerate(all_child_arguments):
|
|
202
|
+
if each_argument == BASE_URL_FLAG and each_index + 1 < len(all_child_arguments):
|
|
203
|
+
return all_child_arguments[each_index + 1].rstrip(
|
|
204
|
+
ALL_URL_TRAILING_PUNCTUATION
|
|
205
|
+
)
|
|
206
|
+
if each_argument.startswith(f"{BASE_URL_FLAG}{ARGUMENT_ASSIGNMENT_SEPARATOR}"):
|
|
207
|
+
return each_argument.split(ARGUMENT_ASSIGNMENT_SEPARATOR, 1)[1].rstrip(
|
|
208
|
+
ALL_URL_TRAILING_PUNCTUATION
|
|
209
|
+
)
|
|
210
|
+
url_match = SERVER_URL_PATTERN.search(each_argument)
|
|
211
|
+
if url_match:
|
|
212
|
+
return url_match.group(0).rstrip(ALL_URL_TRAILING_PUNCTUATION)
|
|
213
|
+
return DEFAULT_PLAYWRIGHT_URL if is_playwright else DEFAULT_DJANGO_URL
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _run_django_preflight(project_root: Path, target_url: str) -> str | None:
|
|
217
|
+
database_error = check_django_database(project_root)
|
|
218
|
+
if database_error:
|
|
219
|
+
return database_error
|
|
220
|
+
return check_server_health(target_url)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _run_playwright_preflight(
|
|
224
|
+
project_root: Path,
|
|
225
|
+
target_url: str,
|
|
226
|
+
) -> str | None:
|
|
227
|
+
conflict_error = check_runserver_port_conflicts(target_url, project_root)
|
|
228
|
+
if conflict_error:
|
|
229
|
+
return conflict_error
|
|
230
|
+
test_database_error = check_test_db_flag(target_url, project_root)
|
|
231
|
+
if test_database_error:
|
|
232
|
+
return test_database_error
|
|
233
|
+
return build_frontend(project_root) or check_server_health(target_url)
|
|
234
|
+
|
|
235
|
+
|
|
236
|
+
def run_preflight(
|
|
237
|
+
selected_runner: SelectedRunner,
|
|
238
|
+
project_root: Path,
|
|
239
|
+
all_child_arguments: Sequence[str],
|
|
240
|
+
) -> str | None:
|
|
241
|
+
"""Run checks required by the selected runner before child launch.
|
|
242
|
+
|
|
243
|
+
Args:
|
|
244
|
+
selected_runner: Runner selected from child arguments.
|
|
245
|
+
project_root: Directory used for project checks.
|
|
246
|
+
all_child_arguments: Complete child arguments used for URL selection.
|
|
247
|
+
|
|
248
|
+
Returns:
|
|
249
|
+
A blocking diagnostic when a readiness check fails, otherwise None.
|
|
250
|
+
"""
|
|
251
|
+
is_django_project = (project_root / MANAGE_PY_FILENAME).is_file()
|
|
252
|
+
if not selected_runner.is_playwright and not is_django_project:
|
|
253
|
+
return None
|
|
254
|
+
target_url = extract_target_url(all_child_arguments, selected_runner.is_playwright)
|
|
255
|
+
if is_django_project and not selected_runner.is_playwright:
|
|
256
|
+
return _run_django_preflight(project_root, target_url)
|
|
257
|
+
if selected_runner.is_playwright:
|
|
258
|
+
return _run_playwright_preflight(project_root, target_url)
|
|
259
|
+
return check_server_health(target_url)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
def run_child_process(
|
|
263
|
+
all_child_arguments: Sequence[str],
|
|
264
|
+
project_root: Path,
|
|
265
|
+
) -> int:
|
|
266
|
+
"""Run the child with inherited streams and environment.
|
|
267
|
+
|
|
268
|
+
Args:
|
|
269
|
+
all_child_arguments: Child executable and its arguments.
|
|
270
|
+
project_root: Working directory for the child.
|
|
271
|
+
|
|
272
|
+
Returns:
|
|
273
|
+
The child's exit status, or one when the child cannot start.
|
|
274
|
+
"""
|
|
275
|
+
try:
|
|
276
|
+
completed_process = subprocess.run(
|
|
277
|
+
list(all_child_arguments),
|
|
278
|
+
cwd=project_root,
|
|
279
|
+
check=False,
|
|
280
|
+
)
|
|
281
|
+
except OSError as error:
|
|
282
|
+
print(ERROR_CHILD_LAUNCH_TEMPLATE.format(error), file=sys.stderr)
|
|
283
|
+
return EXIT_CODE_FAILURE
|
|
284
|
+
return completed_process.returncode
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def main(all_arguments: Sequence[str]) -> int:
|
|
288
|
+
"""Validate arguments, run preflight, then launch the child.
|
|
289
|
+
|
|
290
|
+
Args:
|
|
291
|
+
all_arguments: Command arguments without the executable name.
|
|
292
|
+
|
|
293
|
+
Returns:
|
|
294
|
+
A validation or child exit status.
|
|
295
|
+
"""
|
|
296
|
+
try:
|
|
297
|
+
project_root, all_child_arguments = _parse_command_arguments(all_arguments)
|
|
298
|
+
selected_runner = select_runner(all_child_arguments)
|
|
299
|
+
except ValueError as error:
|
|
300
|
+
print(str(error), file=sys.stderr)
|
|
301
|
+
return EXIT_CODE_INVALID_ARGUMENTS
|
|
302
|
+
try:
|
|
303
|
+
preflight_error = run_preflight(
|
|
304
|
+
selected_runner, project_root, all_child_arguments
|
|
305
|
+
)
|
|
306
|
+
except (OSError, ValueError, subprocess.SubprocessError) as error:
|
|
307
|
+
print(ERROR_PREFLIGHT_FAILURE_TEMPLATE.format(error), file=sys.stderr)
|
|
308
|
+
return EXIT_CODE_FAILURE
|
|
309
|
+
if preflight_error:
|
|
310
|
+
print(preflight_error, file=sys.stderr)
|
|
311
|
+
return EXIT_CODE_FAILURE
|
|
312
|
+
return run_child_process(all_child_arguments, project_root)
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
if __name__ == "__main__":
|
|
316
|
+
raise SystemExit(main(sys.argv[1:]))
|