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,251 @@
|
|
|
1
|
+
"""Restore one explicitly selected legacy GitHub author-swap record."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import stat
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import time
|
|
12
|
+
from collections.abc import Callable, Sequence
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from github_pr_command_constants.config.constants import (
|
|
16
|
+
ALL_GH_AUTH_SWITCH_COMMAND_HEAD,
|
|
17
|
+
LEGACY_RECORD_ACTIVE_MESSAGE,
|
|
18
|
+
LEGACY_RECORD_CHANGED_MESSAGE,
|
|
19
|
+
LEGACY_RECORD_CONFIRMATION_MESSAGE,
|
|
20
|
+
LEGACY_RECORD_INACTIVE_CONFIRMATION_FLAG,
|
|
21
|
+
LEGACY_RECORD_NAME_PATTERN,
|
|
22
|
+
LEGACY_RECORD_ORIGINAL_ACCOUNT_KEY,
|
|
23
|
+
LEGACY_RECORD_PERMISSION_MODE,
|
|
24
|
+
LEGACY_RECORD_REJECTED_MESSAGE,
|
|
25
|
+
LEGACY_RECORD_STALE_AGE_SECONDS,
|
|
26
|
+
LEGACY_RESTORE_FAILED_MESSAGE,
|
|
27
|
+
RECOVERY_CLEAN_EXIT_CODE,
|
|
28
|
+
RECOVERY_FAILED_EXIT_CODE,
|
|
29
|
+
RECOVERY_REJECTED_EXIT_CODE,
|
|
30
|
+
RECOVERY_UNRESOLVED_EXIT_CODE,
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
CommandRunner = Callable[..., subprocess.CompletedProcess[str]]
|
|
34
|
+
FileFingerprint = tuple[int, int, int, int, int]
|
|
35
|
+
VerifiedRecord = tuple[str, FileFingerprint]
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class LegacyRecordRejected(ValueError):
|
|
39
|
+
"""Report an invalid legacy state record."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class LegacyRecordActive(ValueError):
|
|
43
|
+
"""Report a legacy state record that is not old enough for recovery."""
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class LegacyRecordChanged(ValueError):
|
|
47
|
+
"""Report a legacy state record that changed during recovery."""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _build_parser() -> argparse.ArgumentParser:
|
|
51
|
+
parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
|
|
52
|
+
parser.add_argument("state_file", type=Path)
|
|
53
|
+
parser.add_argument(LEGACY_RECORD_INACTIVE_CONFIRMATION_FLAG, action="store_true")
|
|
54
|
+
return parser
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _posix_metadata_is_secure(
|
|
58
|
+
file_mode: int,
|
|
59
|
+
file_owner_id: int,
|
|
60
|
+
current_user_id: int | None,
|
|
61
|
+
) -> bool:
|
|
62
|
+
if not stat.S_ISREG(file_mode):
|
|
63
|
+
return False
|
|
64
|
+
if current_user_id is None:
|
|
65
|
+
return True
|
|
66
|
+
return (
|
|
67
|
+
stat.S_IMODE(file_mode) == LEGACY_RECORD_PERMISSION_MODE
|
|
68
|
+
and file_owner_id == current_user_id
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _secure_metadata(
|
|
73
|
+
state_file: Path,
|
|
74
|
+
current_user_id: int | None,
|
|
75
|
+
) -> os.stat_result:
|
|
76
|
+
try:
|
|
77
|
+
file_metadata = state_file.lstat()
|
|
78
|
+
except OSError as error:
|
|
79
|
+
raise LegacyRecordRejected from error
|
|
80
|
+
if not _posix_metadata_is_secure(
|
|
81
|
+
file_metadata.st_mode,
|
|
82
|
+
file_metadata.st_uid,
|
|
83
|
+
current_user_id,
|
|
84
|
+
):
|
|
85
|
+
raise LegacyRecordRejected
|
|
86
|
+
return file_metadata
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _fingerprint(file_metadata: os.stat_result) -> FileFingerprint:
|
|
90
|
+
return (
|
|
91
|
+
file_metadata.st_dev,
|
|
92
|
+
file_metadata.st_ino,
|
|
93
|
+
file_metadata.st_mode,
|
|
94
|
+
file_metadata.st_size,
|
|
95
|
+
file_metadata.st_mtime_ns,
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _parse_original_account(record_text: str) -> str:
|
|
100
|
+
try:
|
|
101
|
+
decoded_record = json.loads(record_text)
|
|
102
|
+
except json.JSONDecodeError as error:
|
|
103
|
+
raise LegacyRecordRejected from error
|
|
104
|
+
if not isinstance(decoded_record, dict):
|
|
105
|
+
raise LegacyRecordRejected
|
|
106
|
+
if set(decoded_record) != {LEGACY_RECORD_ORIGINAL_ACCOUNT_KEY}:
|
|
107
|
+
raise LegacyRecordRejected
|
|
108
|
+
original_account = decoded_record[LEGACY_RECORD_ORIGINAL_ACCOUNT_KEY]
|
|
109
|
+
if not isinstance(original_account, str) or not original_account.strip():
|
|
110
|
+
raise LegacyRecordRejected
|
|
111
|
+
return original_account.strip()
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _read_verified_record(
|
|
115
|
+
state_file: Path,
|
|
116
|
+
now_seconds: float,
|
|
117
|
+
current_user_id: int | None,
|
|
118
|
+
) -> VerifiedRecord:
|
|
119
|
+
if LEGACY_RECORD_NAME_PATTERN.fullmatch(state_file.name) is None:
|
|
120
|
+
raise LegacyRecordRejected
|
|
121
|
+
initial_metadata = _secure_metadata(state_file, current_user_id)
|
|
122
|
+
if now_seconds - initial_metadata.st_mtime < LEGACY_RECORD_STALE_AGE_SECONDS:
|
|
123
|
+
raise LegacyRecordActive
|
|
124
|
+
try:
|
|
125
|
+
record_text = state_file.read_text(encoding="utf-8")
|
|
126
|
+
except (OSError, UnicodeError) as error:
|
|
127
|
+
raise LegacyRecordRejected from error
|
|
128
|
+
original_account = _parse_original_account(record_text)
|
|
129
|
+
initial_fingerprint = _fingerprint(initial_metadata)
|
|
130
|
+
verified_metadata = _secure_metadata(state_file, current_user_id)
|
|
131
|
+
if _fingerprint(verified_metadata) != initial_fingerprint:
|
|
132
|
+
raise LegacyRecordChanged
|
|
133
|
+
return original_account, initial_fingerprint
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _delete_unchanged_record(
|
|
137
|
+
state_file: Path,
|
|
138
|
+
expected_fingerprint: FileFingerprint,
|
|
139
|
+
current_user_id: int | None,
|
|
140
|
+
) -> None:
|
|
141
|
+
current_metadata = _secure_metadata(state_file, current_user_id)
|
|
142
|
+
if _fingerprint(current_metadata) != expected_fingerprint:
|
|
143
|
+
raise LegacyRecordChanged
|
|
144
|
+
try:
|
|
145
|
+
state_file.unlink()
|
|
146
|
+
except OSError as error:
|
|
147
|
+
raise LegacyRecordChanged from error
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _load_record_with_diagnostics(
|
|
151
|
+
state_file: Path,
|
|
152
|
+
now_seconds: float,
|
|
153
|
+
current_user_id: int | None,
|
|
154
|
+
) -> tuple[VerifiedRecord | None, int]:
|
|
155
|
+
try:
|
|
156
|
+
return _read_verified_record(state_file, now_seconds, current_user_id), 0
|
|
157
|
+
except LegacyRecordActive:
|
|
158
|
+
sys.stderr.write(LEGACY_RECORD_ACTIVE_MESSAGE)
|
|
159
|
+
return None, RECOVERY_REJECTED_EXIT_CODE
|
|
160
|
+
except LegacyRecordChanged:
|
|
161
|
+
sys.stderr.write(LEGACY_RECORD_CHANGED_MESSAGE)
|
|
162
|
+
return None, RECOVERY_FAILED_EXIT_CODE
|
|
163
|
+
except LegacyRecordRejected:
|
|
164
|
+
sys.stderr.write(LEGACY_RECORD_REJECTED_MESSAGE)
|
|
165
|
+
return None, RECOVERY_REJECTED_EXIT_CODE
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _restore_and_delete(
|
|
169
|
+
state_file: Path,
|
|
170
|
+
verified_record: VerifiedRecord,
|
|
171
|
+
current_user_id: int | None,
|
|
172
|
+
command_runner: CommandRunner,
|
|
173
|
+
) -> int:
|
|
174
|
+
original_account, expected_fingerprint = verified_record
|
|
175
|
+
try:
|
|
176
|
+
completed_restore = command_runner(
|
|
177
|
+
[*ALL_GH_AUTH_SWITCH_COMMAND_HEAD, original_account],
|
|
178
|
+
capture_output=True,
|
|
179
|
+
check=False,
|
|
180
|
+
shell=False,
|
|
181
|
+
text=True,
|
|
182
|
+
)
|
|
183
|
+
except OSError:
|
|
184
|
+
sys.stderr.write(LEGACY_RESTORE_FAILED_MESSAGE)
|
|
185
|
+
return RECOVERY_FAILED_EXIT_CODE
|
|
186
|
+
if completed_restore.returncode != 0:
|
|
187
|
+
sys.stderr.write(LEGACY_RESTORE_FAILED_MESSAGE)
|
|
188
|
+
return RECOVERY_FAILED_EXIT_CODE
|
|
189
|
+
try:
|
|
190
|
+
_delete_unchanged_record(state_file, expected_fingerprint, current_user_id)
|
|
191
|
+
except (LegacyRecordChanged, LegacyRecordRejected):
|
|
192
|
+
sys.stderr.write(LEGACY_RECORD_CHANGED_MESSAGE)
|
|
193
|
+
return RECOVERY_FAILED_EXIT_CODE
|
|
194
|
+
return RECOVERY_CLEAN_EXIT_CODE
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _recover_selected_record(
|
|
198
|
+
state_file: Path,
|
|
199
|
+
now_seconds: float,
|
|
200
|
+
current_user_id: int | None,
|
|
201
|
+
command_runner: CommandRunner,
|
|
202
|
+
) -> int:
|
|
203
|
+
verified_record, exit_code = _load_record_with_diagnostics(
|
|
204
|
+
state_file, now_seconds, current_user_id
|
|
205
|
+
)
|
|
206
|
+
if verified_record is None:
|
|
207
|
+
return exit_code
|
|
208
|
+
return _restore_and_delete(
|
|
209
|
+
state_file, verified_record, current_user_id, command_runner
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def main(
|
|
214
|
+
all_arguments: Sequence[str],
|
|
215
|
+
*,
|
|
216
|
+
now_seconds: float,
|
|
217
|
+
current_user_id: int | None,
|
|
218
|
+
command_runner: CommandRunner,
|
|
219
|
+
) -> int:
|
|
220
|
+
"""Recover one confirmed-inactive legacy state record.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
all_arguments: Command arguments after the script name.
|
|
224
|
+
now_seconds: Current wall-clock time in seconds.
|
|
225
|
+
current_user_id: Current POSIX user identifier, or None on Windows.
|
|
226
|
+
command_runner: Process runner used by tests and production.
|
|
227
|
+
|
|
228
|
+
Returns:
|
|
229
|
+
Zero on success, one on failure, two on rejection, or three when unresolved.
|
|
230
|
+
"""
|
|
231
|
+
arguments = _build_parser().parse_args(list(all_arguments))
|
|
232
|
+
if not arguments.confirm_inactive:
|
|
233
|
+
sys.stderr.write(LEGACY_RECORD_CONFIRMATION_MESSAGE)
|
|
234
|
+
return RECOVERY_UNRESOLVED_EXIT_CODE
|
|
235
|
+
return _recover_selected_record(
|
|
236
|
+
arguments.state_file,
|
|
237
|
+
now_seconds,
|
|
238
|
+
current_user_id,
|
|
239
|
+
command_runner,
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
if __name__ == "__main__":
|
|
244
|
+
raise SystemExit(
|
|
245
|
+
main(
|
|
246
|
+
sys.argv[1:],
|
|
247
|
+
now_seconds=time.time(),
|
|
248
|
+
current_user_id=os.getuid() if hasattr(os, "getuid") else None,
|
|
249
|
+
command_runner=subprocess.run,
|
|
250
|
+
)
|
|
251
|
+
)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Tests for explicit GitHub pull request command constants."""
|
|
2
|
+
|
|
3
|
+
import sys
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
_SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
|
|
7
|
+
if str(_SCRIPTS_DIRECTORY) not in sys.path:
|
|
8
|
+
sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
|
|
9
|
+
|
|
10
|
+
from github_pr_command_constants.config import constants
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_action_and_linter_mappings_are_complete() -> None:
|
|
14
|
+
assert constants.ALL_LINTER_ACTIONS_BY_COMMAND == {
|
|
15
|
+
constants.ACTION_COMMENT: constants.LINTER_ACTION_COMMENT,
|
|
16
|
+
constants.ACTION_CREATE: constants.LINTER_ACTION_CREATE,
|
|
17
|
+
constants.ACTION_EDIT: constants.LINTER_ACTION_EDIT,
|
|
18
|
+
constants.ACTION_REVIEW: constants.LINTER_ACTION_REVIEW,
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_review_event_flags_are_complete() -> None:
|
|
23
|
+
assert constants.ALL_REVIEW_FLAGS_BY_EVENT == {
|
|
24
|
+
constants.REVIEW_EVENT_APPROVE: "--approve",
|
|
25
|
+
constants.REVIEW_EVENT_COMMENT: "--comment",
|
|
26
|
+
constants.REVIEW_EVENT_REQUEST_CHANGES: "--request-changes",
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_account_boundary_constants_are_safe_and_stable() -> None:
|
|
31
|
+
assert constants.SELECTED_ACCOUNT_ENVIRONMENT_KEY == "GITHUB_DEFAULT_ACCOUNT"
|
|
32
|
+
assert constants.ACCOUNT_LOOKUP_FAILURE_EXIT_CODE == 1
|
|
33
|
+
assert constants.ACCOUNT_LOOKUP_FAILED_MESSAGE.endswith("lookup failed\n")
|
|
34
|
+
assert constants.ACCOUNT_LOOKUP_EMPTY_MESSAGE.endswith("returned no value\n")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_durable_post_linter_location_uses_managed_root() -> None:
|
|
38
|
+
assert constants.CONFIG_DIR_OVERRIDE_ENVIRONMENT_KEY == "CLAUDE_CONFIG_DIR"
|
|
39
|
+
assert constants.DEFAULT_MANAGED_ROOT_DIRECTORY_NAME == ".claude"
|
|
40
|
+
assert constants.ALL_DURABLE_POST_LINTER_RELATIVE_PATH_PARTS == (
|
|
41
|
+
"scripts",
|
|
42
|
+
"durable_post_lint.py",
|
|
43
|
+
)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_legacy_recovery_constants_define_one_record_boundary() -> None:
|
|
47
|
+
assert constants.ALL_GH_AUTH_SWITCH_COMMAND_HEAD == (
|
|
48
|
+
"gh",
|
|
49
|
+
"auth",
|
|
50
|
+
"switch",
|
|
51
|
+
"--user",
|
|
52
|
+
)
|
|
53
|
+
assert constants.LEGACY_RECORD_NAME_PATTERN.fullmatch(
|
|
54
|
+
"gh_pr_author_swap_session-a.json"
|
|
55
|
+
)
|
|
56
|
+
assert not constants.LEGACY_RECORD_NAME_PATTERN.fullmatch("other.json")
|
|
57
|
+
assert constants.LEGACY_RECORD_STALE_AGE_SECONDS == 1800
|
|
58
|
+
assert constants.RECOVERY_UNRESOLVED_EXIT_CODE == 3
|
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
"""Contract tests for process-local pull request publication."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import threading
|
|
9
|
+
from collections.abc import Callable
|
|
10
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from unittest.mock import Mock
|
|
13
|
+
|
|
14
|
+
import pytest
|
|
15
|
+
|
|
16
|
+
_SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
|
|
17
|
+
if str(_SCRIPTS_DIRECTORY) not in sys.path:
|
|
18
|
+
sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
|
|
19
|
+
|
|
20
|
+
import pull_request
|
|
21
|
+
|
|
22
|
+
REPOSITORY = "jl-cmd/claude-dev-env"
|
|
23
|
+
PR_NUMBER = "2518"
|
|
24
|
+
PR_TITLE = "feat(skills): add guarded pull request publication"
|
|
25
|
+
ACCOUNT_A = "JonEcho"
|
|
26
|
+
ACCOUNT_B = "ReviewBot"
|
|
27
|
+
BODY_FILENAME = "pull-request-body.md"
|
|
28
|
+
LINTER_PATH = _SCRIPTS_DIRECTORY.parents[3] / "scripts/durable_post_lint.py"
|
|
29
|
+
COMMENT_CLI = ["comment", "--repo", REPOSITORY, "--number", PR_NUMBER, "--body-file"]
|
|
30
|
+
ACTION_CASES = [
|
|
31
|
+
(
|
|
32
|
+
[
|
|
33
|
+
"create",
|
|
34
|
+
"--repo",
|
|
35
|
+
REPOSITORY,
|
|
36
|
+
"--base",
|
|
37
|
+
"main",
|
|
38
|
+
"--head",
|
|
39
|
+
"feature/post",
|
|
40
|
+
"--title",
|
|
41
|
+
PR_TITLE,
|
|
42
|
+
"--body-file",
|
|
43
|
+
BODY_FILENAME,
|
|
44
|
+
],
|
|
45
|
+
"pr-create",
|
|
46
|
+
[
|
|
47
|
+
"gh",
|
|
48
|
+
"pr",
|
|
49
|
+
"create",
|
|
50
|
+
"--draft",
|
|
51
|
+
"--repo",
|
|
52
|
+
REPOSITORY,
|
|
53
|
+
"--base",
|
|
54
|
+
"main",
|
|
55
|
+
"--head",
|
|
56
|
+
"feature/post",
|
|
57
|
+
"--title",
|
|
58
|
+
PR_TITLE,
|
|
59
|
+
"--body-file",
|
|
60
|
+
BODY_FILENAME,
|
|
61
|
+
],
|
|
62
|
+
),
|
|
63
|
+
(
|
|
64
|
+
[
|
|
65
|
+
"edit",
|
|
66
|
+
"--repo",
|
|
67
|
+
REPOSITORY,
|
|
68
|
+
"--number",
|
|
69
|
+
PR_NUMBER,
|
|
70
|
+
"--title",
|
|
71
|
+
PR_TITLE,
|
|
72
|
+
"--body-file",
|
|
73
|
+
BODY_FILENAME,
|
|
74
|
+
],
|
|
75
|
+
"pr-edit",
|
|
76
|
+
[
|
|
77
|
+
"gh",
|
|
78
|
+
"pr",
|
|
79
|
+
"edit",
|
|
80
|
+
PR_NUMBER,
|
|
81
|
+
"--repo",
|
|
82
|
+
REPOSITORY,
|
|
83
|
+
"--title",
|
|
84
|
+
PR_TITLE,
|
|
85
|
+
"--body-file",
|
|
86
|
+
BODY_FILENAME,
|
|
87
|
+
],
|
|
88
|
+
),
|
|
89
|
+
(
|
|
90
|
+
[*COMMENT_CLI, BODY_FILENAME],
|
|
91
|
+
"pr-comment",
|
|
92
|
+
[
|
|
93
|
+
"gh",
|
|
94
|
+
"pr",
|
|
95
|
+
"comment",
|
|
96
|
+
PR_NUMBER,
|
|
97
|
+
"--repo",
|
|
98
|
+
REPOSITORY,
|
|
99
|
+
"--body-file",
|
|
100
|
+
BODY_FILENAME,
|
|
101
|
+
],
|
|
102
|
+
),
|
|
103
|
+
(
|
|
104
|
+
[
|
|
105
|
+
"review",
|
|
106
|
+
"--repo",
|
|
107
|
+
REPOSITORY,
|
|
108
|
+
"--number",
|
|
109
|
+
PR_NUMBER,
|
|
110
|
+
"--body-file",
|
|
111
|
+
BODY_FILENAME,
|
|
112
|
+
],
|
|
113
|
+
"pr-review",
|
|
114
|
+
[
|
|
115
|
+
"gh",
|
|
116
|
+
"pr",
|
|
117
|
+
"review",
|
|
118
|
+
PR_NUMBER,
|
|
119
|
+
"--repo",
|
|
120
|
+
REPOSITORY,
|
|
121
|
+
"--comment",
|
|
122
|
+
"--body-file",
|
|
123
|
+
BODY_FILENAME,
|
|
124
|
+
],
|
|
125
|
+
),
|
|
126
|
+
]
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def _completion(
|
|
130
|
+
all_arguments: list[str], code: int = 0, stdout: str = "", stderr: str = ""
|
|
131
|
+
) -> subprocess.CompletedProcess[str]:
|
|
132
|
+
return subprocess.CompletedProcess(all_arguments, code, stdout, stderr)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
@pytest.fixture
|
|
136
|
+
def body_file(tmp_path: Path) -> Path:
|
|
137
|
+
post_file = tmp_path / BODY_FILENAME
|
|
138
|
+
post_file.write_text("Durable body text.", encoding="utf-8")
|
|
139
|
+
return post_file
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _comment_arguments(body_file: Path) -> list[str]:
|
|
143
|
+
return [*COMMENT_CLI, str(body_file)]
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _account_text(account_name: str) -> str:
|
|
147
|
+
return account_name[::-1]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _calls(command_runner: Mock) -> list[list[str]]:
|
|
151
|
+
return [each_call.args[0] for each_call in command_runner.call_args_list]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _run_comment(
|
|
155
|
+
body_file: Path, command_runner: Mock, environment: dict[str, str]
|
|
156
|
+
) -> int:
|
|
157
|
+
return pull_request.main(
|
|
158
|
+
_comment_arguments(body_file),
|
|
159
|
+
all_environment=environment,
|
|
160
|
+
command_runner=command_runner,
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _account_runner(
|
|
165
|
+
*, auth_code: int = 0, auth_stdout: str, action_code: int = 0, auth_stderr: str = ""
|
|
166
|
+
) -> Mock:
|
|
167
|
+
return Mock(
|
|
168
|
+
side_effect=[
|
|
169
|
+
_completion([]),
|
|
170
|
+
_completion([], auth_code, auth_stdout, auth_stderr),
|
|
171
|
+
_completion([], action_code),
|
|
172
|
+
]
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@pytest.mark.parametrize(
|
|
177
|
+
("all_cli_arguments", "linter_action", "expected_gh"), ACTION_CASES
|
|
178
|
+
)
|
|
179
|
+
def test_actions_run_exact_linter_then_gh_arguments(
|
|
180
|
+
all_cli_arguments: list[str], linter_action: str, expected_gh: list[str]
|
|
181
|
+
) -> None:
|
|
182
|
+
command_runner = Mock(side_effect=[_completion([]), _completion([])])
|
|
183
|
+
exit_code = pull_request.main(
|
|
184
|
+
all_cli_arguments, all_environment={}, command_runner=command_runner
|
|
185
|
+
)
|
|
186
|
+
title_arguments = ["--title", PR_TITLE] if "--title" in all_cli_arguments else []
|
|
187
|
+
expected_linter = [
|
|
188
|
+
sys.executable,
|
|
189
|
+
str(LINTER_PATH),
|
|
190
|
+
"--action",
|
|
191
|
+
linter_action,
|
|
192
|
+
*title_arguments,
|
|
193
|
+
"--body-file",
|
|
194
|
+
BODY_FILENAME,
|
|
195
|
+
]
|
|
196
|
+
assert exit_code == 0
|
|
197
|
+
assert _calls(command_runner) == [expected_linter, expected_gh]
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def test_body_transport_uses_files_only(body_file: Path) -> None:
|
|
201
|
+
command_runner = Mock(side_effect=[_completion([]), _completion([])])
|
|
202
|
+
assert _run_comment(body_file, command_runner, {}) == 0
|
|
203
|
+
for each_command in _calls(command_runner):
|
|
204
|
+
assert "--body-file" in each_command
|
|
205
|
+
assert "--body" not in each_command
|
|
206
|
+
assert "Durable body text." not in each_command
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def test_linter_failure_stops_credential_lookup_and_action(body_file: Path) -> None:
|
|
210
|
+
command_runner = Mock(return_value=_completion([], 1))
|
|
211
|
+
exit_code = _run_comment(
|
|
212
|
+
body_file, command_runner, {"GITHUB_DEFAULT_ACCOUNT": ACCOUNT_A}
|
|
213
|
+
)
|
|
214
|
+
assert exit_code == 1
|
|
215
|
+
assert command_runner.call_count == 1
|
|
216
|
+
assert Path(_calls(command_runner)[0][1]).name == "durable_post_lint.py"
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_account_lookup_scopes_action_environment(body_file: Path) -> None:
|
|
220
|
+
account_text = _account_text(ACCOUNT_A)
|
|
221
|
+
command_runner = _account_runner(auth_stdout=f"{account_text}\n")
|
|
222
|
+
parent_environment = {
|
|
223
|
+
"GITHUB_DEFAULT_ACCOUNT": ACCOUNT_A,
|
|
224
|
+
"GITHUB_TOKEN": "parent-setting",
|
|
225
|
+
"UNCHANGED": "same",
|
|
226
|
+
}
|
|
227
|
+
assert _run_comment(body_file, command_runner, parent_environment) == 0
|
|
228
|
+
assert _calls(command_runner)[1] == ["gh", "auth", "token", "--user", ACCOUNT_A]
|
|
229
|
+
action_environment = command_runner.call_args_list[2].kwargs["env"]
|
|
230
|
+
assert action_environment["GH_TOKEN"] == account_text
|
|
231
|
+
assert "GITHUB_TOKEN" not in action_environment
|
|
232
|
+
assert action_environment["UNCHANGED"] == "same"
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def test_absent_account_uses_copied_parent_environment(body_file: Path) -> None:
|
|
236
|
+
command_runner = Mock(side_effect=[_completion([]), _completion([])])
|
|
237
|
+
parent_environment = {"GITHUB_TOKEN": "parent-setting", "UNCHANGED": "same"}
|
|
238
|
+
assert _run_comment(body_file, command_runner, parent_environment) == 0
|
|
239
|
+
action_environment = command_runner.call_args_list[1].kwargs["env"]
|
|
240
|
+
assert action_environment == parent_environment
|
|
241
|
+
assert action_environment is not parent_environment
|
|
242
|
+
assert command_runner.call_count == 2
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def test_token_lookup_failure_prints_generic_safe_stderr(
|
|
246
|
+
body_file: Path, capsys: pytest.CaptureFixture[str]
|
|
247
|
+
) -> None:
|
|
248
|
+
account_text = _account_text(ACCOUNT_A)
|
|
249
|
+
command_runner = _account_runner(
|
|
250
|
+
auth_code=1, auth_stdout=account_text, auth_stderr=f"unsafe {account_text}"
|
|
251
|
+
)
|
|
252
|
+
exit_code = _run_comment(
|
|
253
|
+
body_file, command_runner, {"GITHUB_DEFAULT_ACCOUNT": ACCOUNT_A}
|
|
254
|
+
)
|
|
255
|
+
captured = capsys.readouterr()
|
|
256
|
+
assert exit_code == 1
|
|
257
|
+
assert captured.err == "error: GitHub account lookup failed\n"
|
|
258
|
+
assert account_text not in captured.out + captured.err
|
|
259
|
+
assert command_runner.call_count == 2
|
|
260
|
+
command_runner = Mock(side_effect=[_completion([]), OSError("unavailable")])
|
|
261
|
+
assert (
|
|
262
|
+
_run_comment(body_file, command_runner, {"GITHUB_DEFAULT_ACCOUNT": ACCOUNT_A})
|
|
263
|
+
== 1
|
|
264
|
+
)
|
|
265
|
+
assert capsys.readouterr().err == "error: GitHub account lookup failed\n"
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_empty_account_token_stops_before_action(
|
|
269
|
+
body_file: Path, capsys: pytest.CaptureFixture[str]
|
|
270
|
+
) -> None:
|
|
271
|
+
command_runner = _account_runner(auth_stdout=" \n")
|
|
272
|
+
exit_code = _run_comment(
|
|
273
|
+
body_file, command_runner, {"GITHUB_DEFAULT_ACCOUNT": ACCOUNT_A}
|
|
274
|
+
)
|
|
275
|
+
captured = capsys.readouterr()
|
|
276
|
+
assert exit_code == 1
|
|
277
|
+
assert captured.err == "error: GitHub account lookup returned no value\n"
|
|
278
|
+
assert command_runner.call_count == 2
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def test_action_child_results_pass_through_or_report_safely(
|
|
282
|
+
body_file: Path, capsys: pytest.CaptureFixture[str]
|
|
283
|
+
) -> None:
|
|
284
|
+
command_runner = Mock(side_effect=[_completion([]), _completion([], 7)])
|
|
285
|
+
assert _run_comment(body_file, command_runner, {}) == 7
|
|
286
|
+
command_runner = Mock(side_effect=[_completion([]), OSError("unavailable")])
|
|
287
|
+
assert _run_comment(body_file, command_runner, {}) == 1
|
|
288
|
+
assert capsys.readouterr().err == "error: GitHub action failed\n"
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def test_supplied_parent_environment_remains_unchanged(body_file: Path) -> None:
|
|
292
|
+
command_runner = _account_runner(auth_stdout=_account_text(ACCOUNT_A))
|
|
293
|
+
parent_environment = {
|
|
294
|
+
"GITHUB_DEFAULT_ACCOUNT": ACCOUNT_A,
|
|
295
|
+
"GITHUB_TOKEN": "parent-setting",
|
|
296
|
+
"GH_TOKEN": "parent-setting",
|
|
297
|
+
}
|
|
298
|
+
original_environment = dict(parent_environment)
|
|
299
|
+
assert _run_comment(body_file, command_runner, parent_environment) == 0
|
|
300
|
+
assert parent_environment == original_environment
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def test_process_environment_remains_unchanged(
|
|
304
|
+
body_file: Path, monkeypatch: pytest.MonkeyPatch
|
|
305
|
+
) -> None:
|
|
306
|
+
monkeypatch.setenv("GITHUB_DEFAULT_ACCOUNT", ACCOUNT_A)
|
|
307
|
+
monkeypatch.setenv("GITHUB_TOKEN", "parent-setting")
|
|
308
|
+
monkeypatch.setenv("GH_TOKEN", "parent-setting")
|
|
309
|
+
original_environment = dict(os.environ)
|
|
310
|
+
command_runner = _account_runner(auth_stdout=_account_text(ACCOUNT_A))
|
|
311
|
+
exit_code = pull_request.main(
|
|
312
|
+
_comment_arguments(body_file),
|
|
313
|
+
all_environment=os.environ,
|
|
314
|
+
command_runner=command_runner,
|
|
315
|
+
)
|
|
316
|
+
assert exit_code == 0
|
|
317
|
+
assert dict(os.environ) == original_environment
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
def _concurrent_runner(
|
|
321
|
+
all_environments: list[dict[str, str]],
|
|
322
|
+
all_commands: list[list[str]],
|
|
323
|
+
record_lock: threading.Lock,
|
|
324
|
+
) -> Callable[..., subprocess.CompletedProcess[str]]:
|
|
325
|
+
def run_command(
|
|
326
|
+
all_arguments: list[str], **keyword_arguments: object
|
|
327
|
+
) -> subprocess.CompletedProcess[str]:
|
|
328
|
+
with record_lock:
|
|
329
|
+
all_commands.append(all_arguments)
|
|
330
|
+
if all_arguments[0] == sys.executable:
|
|
331
|
+
return _completion(all_arguments)
|
|
332
|
+
if all_arguments[:3] == ["gh", "auth", "token"]:
|
|
333
|
+
if all_arguments[-1] == ACCOUNT_A:
|
|
334
|
+
return _completion(all_arguments, stdout=_account_text(ACCOUNT_A))
|
|
335
|
+
return _completion(all_arguments, 1)
|
|
336
|
+
child_environment = keyword_arguments["env"]
|
|
337
|
+
assert isinstance(child_environment, dict)
|
|
338
|
+
with record_lock:
|
|
339
|
+
all_environments.append(dict(child_environment))
|
|
340
|
+
return _completion(all_arguments)
|
|
341
|
+
|
|
342
|
+
return run_command
|
|
343
|
+
|
|
344
|
+
|
|
345
|
+
def test_concurrent_accounts_keep_tokens_isolated(body_file: Path) -> None:
|
|
346
|
+
all_environments: list[dict[str, str]] = []
|
|
347
|
+
all_commands: list[list[str]] = []
|
|
348
|
+
command_runner = _concurrent_runner(
|
|
349
|
+
all_environments, all_commands, threading.Lock()
|
|
350
|
+
)
|
|
351
|
+
with ThreadPoolExecutor(max_workers=2) as executor:
|
|
352
|
+
first_run = executor.submit(
|
|
353
|
+
pull_request.main,
|
|
354
|
+
_comment_arguments(body_file),
|
|
355
|
+
all_environment={"GITHUB_DEFAULT_ACCOUNT": ACCOUNT_A},
|
|
356
|
+
command_runner=command_runner,
|
|
357
|
+
)
|
|
358
|
+
second_run = executor.submit(
|
|
359
|
+
pull_request.main,
|
|
360
|
+
_comment_arguments(body_file),
|
|
361
|
+
all_environment={"GITHUB_DEFAULT_ACCOUNT": ACCOUNT_B},
|
|
362
|
+
command_runner=command_runner,
|
|
363
|
+
)
|
|
364
|
+
assert {first_run.result(), second_run.result()} == {0, 1}
|
|
365
|
+
assert len(all_environments) == 1
|
|
366
|
+
assert all_environments[0]["GH_TOKEN"] == _account_text(ACCOUNT_A)
|
|
367
|
+
assert all(
|
|
368
|
+
each_command[:3] != ["gh", "auth", "switch"] for each_command in all_commands
|
|
369
|
+
)
|
|
370
|
+
|
|
371
|
+
|
|
372
|
+
def test_token_never_reaches_captured_output(
|
|
373
|
+
body_file: Path, capsys: pytest.CaptureFixture[str]
|
|
374
|
+
) -> None:
|
|
375
|
+
account_text = _account_text(ACCOUNT_A)
|
|
376
|
+
command_runner = _account_runner(auth_stdout=f"{account_text}\n")
|
|
377
|
+
assert (
|
|
378
|
+
_run_comment(body_file, command_runner, {"GITHUB_DEFAULT_ACCOUNT": ACCOUNT_A})
|
|
379
|
+
== 0
|
|
380
|
+
)
|
|
381
|
+
captured = capsys.readouterr()
|
|
382
|
+
assert account_text not in captured.out + captured.err
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
def test_cli_rejects_inline_body(capsys: pytest.CaptureFixture[str]) -> None:
|
|
386
|
+
command_runner = Mock()
|
|
387
|
+
with pytest.raises(SystemExit) as raised_exit:
|
|
388
|
+
pull_request.main(
|
|
389
|
+
[*COMMENT_CLI[:-1], "--body", "Inline body text."],
|
|
390
|
+
all_environment={},
|
|
391
|
+
command_runner=command_runner,
|
|
392
|
+
)
|
|
393
|
+
captured = capsys.readouterr()
|
|
394
|
+
assert raised_exit.value.code == 2
|
|
395
|
+
assert "--body-file" in captured.err
|
|
396
|
+
command_runner.assert_not_called()
|