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,342 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PostToolUse context reminder: the PR done checklist after a push.
|
|
3
|
+
|
|
4
|
+
This hook never blocks. It watches every Bash call finish and, after a
|
|
5
|
+
successful ``git push`` or ``gh pr create``, probes the branch's pull request
|
|
6
|
+
through ``gh pr view`` and adds one loud checklist to the agent's context::
|
|
7
|
+
|
|
8
|
+
=== PR DONE CHECKLIST (context reminder, never a block) ===
|
|
9
|
+
PR #42 https://github.com/acme/widgets/pull/42
|
|
10
|
+
Mergeable: CONFLICTING (CONFLICTS with the base branch. Merge origin/main ...)
|
|
11
|
+
CI checks: 1 failing, 2 pending, 4 total
|
|
12
|
+
Draft: yes
|
|
13
|
+
Label done: not set
|
|
14
|
+
Verdict: NOT DONE. Do not add the done label yet.
|
|
15
|
+
Re-check: gh pr view 42 --json mergeable,mergeStateStatus,statusCheckRollup,labels
|
|
16
|
+
A PR is done only when every line above is clean.
|
|
17
|
+
|
|
18
|
+
Why a reminder and not a gate: agents keep working. The rule "a PR is done
|
|
19
|
+
only when GitHub reports it mergeable and CI is clean" arrives as live state
|
|
20
|
+
at the exact moment the agent just pushed, so no instruction file has to be
|
|
21
|
+
remembered. A block would stall the run; this only informs it.
|
|
22
|
+
|
|
23
|
+
Quiet branches: a non-Bash tool, a command that is not a push or a PR
|
|
24
|
+
creation, a push whose harness-reported exit status is non-zero, or a ``gh``
|
|
25
|
+
failure other than "no pull requests found" each emit nothing.
|
|
26
|
+
|
|
27
|
+
Hosted by ``blocking/bash_post_call_dispatcher.py``, which forwards the
|
|
28
|
+
``hookSpecificOutput.additionalContext`` this hook prints.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import json
|
|
34
|
+
import shlex
|
|
35
|
+
import subprocess
|
|
36
|
+
import sys
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
_hooks_root_directory = str(Path(__file__).resolve().parent.parent)
|
|
41
|
+
if _hooks_root_directory not in sys.path:
|
|
42
|
+
sys.path.insert(0, _hooks_root_directory)
|
|
43
|
+
from hooks_constants.bash_pre_tool_use_dispatcher_constants import (
|
|
44
|
+
ALL_BASH_AND_POWERSHELL_TOOL_NAMES,
|
|
45
|
+
)
|
|
46
|
+
from hooks_constants.pr_done_reminder_constants import (
|
|
47
|
+
ADD_LABEL_COMMAND_TEMPLATE,
|
|
48
|
+
ALL_FAILING_CHECK_CONCLUSIONS,
|
|
49
|
+
ALL_PASSING_CHECK_CONCLUSIONS,
|
|
50
|
+
CHECK_COMPLETED_STATUS,
|
|
51
|
+
DONE_LABEL_NAME,
|
|
52
|
+
EXIT_CODE_ERROR_PREFIX,
|
|
53
|
+
GH_PR_CREATE_ACTION,
|
|
54
|
+
GH_PR_SUBCOMMAND,
|
|
55
|
+
ALL_GH_PR_VIEW_ARGUMENTS,
|
|
56
|
+
ALL_GIT_OPTIONS_WITH_VALUE,
|
|
57
|
+
ALL_POWERSHELL_COMMAND_FLAGS,
|
|
58
|
+
ALL_POWERSHELL_PROGRAM_NAMES,
|
|
59
|
+
GH_PR_VIEW_TIMEOUT_SECONDS,
|
|
60
|
+
GH_PROGRAM_NAME,
|
|
61
|
+
GIT_PROGRAM_NAME,
|
|
62
|
+
GIT_PUSH_SUBCOMMAND,
|
|
63
|
+
MERGEABLE_CLEAN_VALUE,
|
|
64
|
+
MERGEABLE_UNKNOWN_VALUE,
|
|
65
|
+
NO_PULL_REQUEST_MARKER,
|
|
66
|
+
NO_PULL_REQUEST_REMINDER,
|
|
67
|
+
RECHECK_COMMAND_TEMPLATE,
|
|
68
|
+
REMINDER_FOOTER,
|
|
69
|
+
REMINDER_HEADER,
|
|
70
|
+
REMINDER_LINE_SEPARATOR,
|
|
71
|
+
ALL_REMINDER_HINTS_BY_MERGEABLE,
|
|
72
|
+
STATUS_CONTEXT_STATE_KEY,
|
|
73
|
+
VERDICT_DONE,
|
|
74
|
+
VERDICT_NOT_DONE,
|
|
75
|
+
)
|
|
76
|
+
from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
|
|
77
|
+
from hooks_constants.shell_command_segments import (
|
|
78
|
+
effective_leading_program,
|
|
79
|
+
split_into_segments,
|
|
80
|
+
token_basename,
|
|
81
|
+
)
|
|
82
|
+
except ImportError as import_error:
|
|
83
|
+
raise ImportError(
|
|
84
|
+
"pr_done_reminder: cannot import its dependencies; "
|
|
85
|
+
"ensure the hooks directory is importable."
|
|
86
|
+
) from import_error
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _command_tokens(command: str) -> list[str]:
|
|
90
|
+
try:
|
|
91
|
+
return shlex.split(command, posix=True)
|
|
92
|
+
except ValueError:
|
|
93
|
+
return command.split()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _segment_program_and_arguments(all_segment_tokens: list[str]) -> tuple[str, list[str]]:
|
|
97
|
+
program_token = effective_leading_program(all_segment_tokens)
|
|
98
|
+
if program_token is None:
|
|
99
|
+
return "", []
|
|
100
|
+
program_index = all_segment_tokens.index(program_token)
|
|
101
|
+
return token_basename(program_token), all_segment_tokens[program_index + 1 :]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _git_subcommand(all_arguments: list[str]) -> str | None:
|
|
105
|
+
"""Return git's subcommand after its global options.
|
|
106
|
+
|
|
107
|
+
``git -C <path> push`` and ``git -c <key=value> push`` carry an option
|
|
108
|
+
value before the subcommand; ``--git-dir=<path>`` carries none. The first
|
|
109
|
+
token left after those is the subcommand, so ``git stash push`` yields
|
|
110
|
+
``stash`` and ``git log --grep push`` yields ``log``.
|
|
111
|
+
"""
|
|
112
|
+
should_skip_next_token = False
|
|
113
|
+
for each_argument in all_arguments:
|
|
114
|
+
if should_skip_next_token:
|
|
115
|
+
should_skip_next_token = False
|
|
116
|
+
continue
|
|
117
|
+
if each_argument in ALL_GIT_OPTIONS_WITH_VALUE:
|
|
118
|
+
should_skip_next_token = True
|
|
119
|
+
continue
|
|
120
|
+
if each_argument.startswith("-"):
|
|
121
|
+
continue
|
|
122
|
+
return each_argument
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _is_git_push(program_name: str, all_arguments: list[str]) -> bool:
|
|
127
|
+
return (
|
|
128
|
+
program_name == GIT_PROGRAM_NAME and _git_subcommand(all_arguments) == GIT_PUSH_SUBCOMMAND
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _is_gh_pr_create(program_name: str, all_arguments: list[str]) -> bool:
|
|
133
|
+
return program_name == GH_PROGRAM_NAME and all_arguments[:2] == [
|
|
134
|
+
GH_PR_SUBCOMMAND,
|
|
135
|
+
GH_PR_CREATE_ACTION,
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _powershell_inner_commands(all_command_tokens: list[str]) -> list[str]:
|
|
140
|
+
"""Return the script texts every ``pwsh -Command "..."`` wrapper runs.
|
|
141
|
+
|
|
142
|
+
Read from the raw shlex tokens, before segment splitting: the quoted
|
|
143
|
+
script is one token here, and splitting it on ``;`` first would cut a
|
|
144
|
+
``git add -A; git push`` script in half.
|
|
145
|
+
"""
|
|
146
|
+
all_inner_commands: list[str] = []
|
|
147
|
+
has_seen_powershell_program = False
|
|
148
|
+
for each_token_index, each_token in enumerate(all_command_tokens[:-1]):
|
|
149
|
+
if token_basename(each_token) in ALL_POWERSHELL_PROGRAM_NAMES:
|
|
150
|
+
has_seen_powershell_program = True
|
|
151
|
+
continue
|
|
152
|
+
if has_seen_powershell_program and each_token.lower() in ALL_POWERSHELL_COMMAND_FLAGS:
|
|
153
|
+
all_inner_commands.append(all_command_tokens[each_token_index + 1])
|
|
154
|
+
has_seen_powershell_program = False
|
|
155
|
+
return all_inner_commands
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def command_triggers_reminder(command: str) -> bool:
|
|
159
|
+
"""Return True when any segment of the command is a git push or a gh pr create.
|
|
160
|
+
|
|
161
|
+
A ``pwsh -Command "..."`` wrapper is unwrapped and its inner text scanned
|
|
162
|
+
the same way, since the shell rule routes Bash-tool commands through pwsh.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
command: The Bash command text the agent ran.
|
|
166
|
+
"""
|
|
167
|
+
all_command_tokens = _command_tokens(command)
|
|
168
|
+
for each_inner_command in _powershell_inner_commands(all_command_tokens):
|
|
169
|
+
if command_triggers_reminder(each_inner_command):
|
|
170
|
+
return True
|
|
171
|
+
for each_segment in split_into_segments(all_command_tokens):
|
|
172
|
+
program_name, all_arguments = _segment_program_and_arguments(each_segment)
|
|
173
|
+
if _is_git_push(program_name, all_arguments) or _is_gh_pr_create(
|
|
174
|
+
program_name, all_arguments
|
|
175
|
+
):
|
|
176
|
+
return True
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _harness_reported_failure(tool_response: object) -> bool:
|
|
181
|
+
return isinstance(tool_response, str) and tool_response.startswith(EXIT_CODE_ERROR_PREFIX)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _check_outcome(all_check_fields: dict[str, object]) -> str:
|
|
185
|
+
"""Return failing, pending, or passing for one statusCheckRollup entry.
|
|
186
|
+
|
|
187
|
+
A CheckRun entry carries ``status`` and ``conclusion``; a StatusContext
|
|
188
|
+
entry carries ``state``. Both shapes appear in the same rollup list.
|
|
189
|
+
"""
|
|
190
|
+
state_text = str(all_check_fields.get(STATUS_CONTEXT_STATE_KEY) or "").upper()
|
|
191
|
+
if state_text:
|
|
192
|
+
if state_text in ALL_FAILING_CHECK_CONCLUSIONS:
|
|
193
|
+
return "failing"
|
|
194
|
+
return "passing" if state_text in ALL_PASSING_CHECK_CONCLUSIONS else "pending"
|
|
195
|
+
if str(all_check_fields.get("status") or "").upper() != CHECK_COMPLETED_STATUS:
|
|
196
|
+
return "pending"
|
|
197
|
+
conclusion_text = str(all_check_fields.get("conclusion") or "").upper()
|
|
198
|
+
return "failing" if conclusion_text in ALL_FAILING_CHECK_CONCLUSIONS else "passing"
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _check_counts(all_pr_fields: dict[str, object]) -> dict[str, int]:
|
|
202
|
+
all_checks = all_pr_fields.get("statusCheckRollup")
|
|
203
|
+
all_check_counts = {"failing": 0, "pending": 0, "passing": 0, "total": 0}
|
|
204
|
+
if not isinstance(all_checks, list):
|
|
205
|
+
return all_check_counts
|
|
206
|
+
for each_check_fields in all_checks:
|
|
207
|
+
if isinstance(each_check_fields, dict):
|
|
208
|
+
all_check_counts[_check_outcome(each_check_fields)] += 1
|
|
209
|
+
all_check_counts["total"] += 1
|
|
210
|
+
return all_check_counts
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _checks_line(all_check_counts: dict[str, int]) -> str:
|
|
214
|
+
if all_check_counts["total"] == 0:
|
|
215
|
+
return "none reported"
|
|
216
|
+
return (
|
|
217
|
+
f"{all_check_counts['failing']} failing, "
|
|
218
|
+
f"{all_check_counts['pending']} pending, "
|
|
219
|
+
f"{all_check_counts['total']} total"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _has_done_label(all_pr_fields: dict[str, object]) -> bool:
|
|
224
|
+
all_labels = all_pr_fields.get("labels")
|
|
225
|
+
if not isinstance(all_labels, list):
|
|
226
|
+
return False
|
|
227
|
+
return any(
|
|
228
|
+
isinstance(each_label, dict) and each_label.get("name") == DONE_LABEL_NAME
|
|
229
|
+
for each_label in all_labels
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _is_done(mergeable_value: str, all_check_counts: dict[str, int]) -> bool:
|
|
234
|
+
return (
|
|
235
|
+
mergeable_value == MERGEABLE_CLEAN_VALUE
|
|
236
|
+
and all_check_counts["failing"] == 0
|
|
237
|
+
and all_check_counts["pending"] == 0
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _verdict_lines(number: object, is_done: bool) -> list[str]:
|
|
242
|
+
"""Return the verdict line, the add-label command on DONE, and the re-check line."""
|
|
243
|
+
all_lines = [f"Verdict: {VERDICT_DONE if is_done else VERDICT_NOT_DONE}"]
|
|
244
|
+
if is_done:
|
|
245
|
+
add_label_command = ADD_LABEL_COMMAND_TEMPLATE.format(number=number, label=DONE_LABEL_NAME)
|
|
246
|
+
all_lines.append(f"Add label: {add_label_command}")
|
|
247
|
+
all_lines.append(f"Re-check: {RECHECK_COMMAND_TEMPLATE.format(number=number)}")
|
|
248
|
+
return all_lines
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def build_reminder_context(all_pr_fields: dict[str, object]) -> str:
|
|
252
|
+
"""Build the checklist text from a ``gh pr view --json`` object.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
all_pr_fields: The parsed pull request JSON with number, url, isDraft,
|
|
256
|
+
mergeable, mergeStateStatus, statusCheckRollup, and labels.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
The multi-line checklist ending in a DONE or NOT DONE verdict.
|
|
260
|
+
"""
|
|
261
|
+
number = all_pr_fields.get("number", "?")
|
|
262
|
+
mergeable_value = str(all_pr_fields.get("mergeable") or MERGEABLE_UNKNOWN_VALUE).upper()
|
|
263
|
+
mergeable_hint = ALL_REMINDER_HINTS_BY_MERGEABLE.get(
|
|
264
|
+
mergeable_value, ALL_REMINDER_HINTS_BY_MERGEABLE[MERGEABLE_UNKNOWN_VALUE]
|
|
265
|
+
)
|
|
266
|
+
all_check_counts = _check_counts(all_pr_fields)
|
|
267
|
+
all_lines = [
|
|
268
|
+
REMINDER_HEADER,
|
|
269
|
+
f"PR #{number} {all_pr_fields.get('url', '')}",
|
|
270
|
+
f"Mergeable: {mergeable_value} ({mergeable_hint})",
|
|
271
|
+
f"CI checks: {_checks_line(all_check_counts)}",
|
|
272
|
+
f"Draft: {'yes' if all_pr_fields.get('isDraft') else 'no'}",
|
|
273
|
+
f"Label done: {'set' if _has_done_label(all_pr_fields) else 'not set'}",
|
|
274
|
+
*_verdict_lines(number, _is_done(mergeable_value, all_check_counts)),
|
|
275
|
+
REMINDER_FOOTER,
|
|
276
|
+
]
|
|
277
|
+
return REMINDER_LINE_SEPARATOR.join(all_lines)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _probe_pull_request(cwd: str) -> str | None:
|
|
281
|
+
"""Return the reminder text from a live ``gh pr view``, or None to stay quiet."""
|
|
282
|
+
try:
|
|
283
|
+
completed_process = subprocess.run(
|
|
284
|
+
list(ALL_GH_PR_VIEW_ARGUMENTS),
|
|
285
|
+
cwd=cwd or None,
|
|
286
|
+
capture_output=True,
|
|
287
|
+
text=True,
|
|
288
|
+
encoding="utf-8",
|
|
289
|
+
errors="replace",
|
|
290
|
+
timeout=GH_PR_VIEW_TIMEOUT_SECONDS,
|
|
291
|
+
check=False,
|
|
292
|
+
)
|
|
293
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
294
|
+
return None
|
|
295
|
+
if completed_process.returncode != 0:
|
|
296
|
+
combined_output = f"{completed_process.stdout}\n{completed_process.stderr}".lower()
|
|
297
|
+
return NO_PULL_REQUEST_REMINDER if NO_PULL_REQUEST_MARKER in combined_output else None
|
|
298
|
+
try:
|
|
299
|
+
all_pr_fields = json.loads(completed_process.stdout)
|
|
300
|
+
except json.JSONDecodeError:
|
|
301
|
+
return None
|
|
302
|
+
if not isinstance(all_pr_fields, dict):
|
|
303
|
+
return None
|
|
304
|
+
return build_reminder_context(all_pr_fields)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _emit_context(context_text: str) -> None:
|
|
308
|
+
payload = {
|
|
309
|
+
"hookSpecificOutput": {
|
|
310
|
+
"hookEventName": "PostToolUse",
|
|
311
|
+
"additionalContext": context_text,
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
sys.stdout.write(json.dumps(payload))
|
|
315
|
+
sys.stdout.flush()
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def main() -> None:
|
|
319
|
+
"""Add the PR done checklist to context after a successful push or PR creation.
|
|
320
|
+
|
|
321
|
+
Reads the PostToolUse payload from stdin. Every quiet branch returns with
|
|
322
|
+
no output, so this hook can never alter or block a tool call.
|
|
323
|
+
"""
|
|
324
|
+
hook_payload = read_hook_input_dictionary_from_stdin()
|
|
325
|
+
if (
|
|
326
|
+
hook_payload is None
|
|
327
|
+
or hook_payload.get("tool_name") not in ALL_BASH_AND_POWERSHELL_TOOL_NAMES
|
|
328
|
+
):
|
|
329
|
+
return
|
|
330
|
+
tool_input = hook_payload.get("tool_input")
|
|
331
|
+
command = tool_input.get("command", "") if isinstance(tool_input, dict) else ""
|
|
332
|
+
if not isinstance(command, str) or not command_triggers_reminder(command):
|
|
333
|
+
return
|
|
334
|
+
if _harness_reported_failure(hook_payload.get("tool_response")):
|
|
335
|
+
return
|
|
336
|
+
context_text = _probe_pull_request(str(hook_payload.get("cwd") or ""))
|
|
337
|
+
if context_text is not None:
|
|
338
|
+
_emit_context(context_text)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
if __name__ == "__main__":
|
|
342
|
+
main()
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
"""Behavior tests for the PR done-checklist context reminder.
|
|
2
|
+
|
|
3
|
+
The hook is a PostToolUse observer on Bash. It never blocks. These tests pin
|
|
4
|
+
three things: which commands wake it, what the checklist says for each live
|
|
5
|
+
PR state, and that a probe failure stays quiet. The ``gh`` probe is faked at
|
|
6
|
+
the subprocess boundary so no test touches the network.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import io
|
|
12
|
+
import json
|
|
13
|
+
import subprocess
|
|
14
|
+
import sys
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
import pytest
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
advisory_directory = str(Path(__file__).resolve().parent)
|
|
21
|
+
if advisory_directory not in sys.path:
|
|
22
|
+
sys.path.insert(0, advisory_directory)
|
|
23
|
+
import pr_done_reminder
|
|
24
|
+
except ImportError as import_error:
|
|
25
|
+
raise ImportError(
|
|
26
|
+
"test_pr_done_reminder: cannot import its sibling modules; "
|
|
27
|
+
"ensure the advisory directory is importable."
|
|
28
|
+
) from import_error
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
_CLEAN_PR_OBJECT: dict[str, object] = {
|
|
32
|
+
"number": 42,
|
|
33
|
+
"url": "https://github.com/acme/widgets/pull/42",
|
|
34
|
+
"isDraft": True,
|
|
35
|
+
"mergeable": "MERGEABLE",
|
|
36
|
+
"mergeStateStatus": "CLEAN",
|
|
37
|
+
"statusCheckRollup": [
|
|
38
|
+
{"name": "pytest", "status": "COMPLETED", "conclusion": "SUCCESS"},
|
|
39
|
+
{"context": "lint", "state": "SUCCESS"},
|
|
40
|
+
],
|
|
41
|
+
"labels": [],
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _payload(command: str, tool_response: object = None) -> str:
|
|
46
|
+
response: object = {"stdout": "", "stderr": ""} if tool_response is None else tool_response
|
|
47
|
+
return json.dumps(
|
|
48
|
+
{
|
|
49
|
+
"session_id": "reminder-session",
|
|
50
|
+
"cwd": "C:/repo",
|
|
51
|
+
"tool_name": "Bash",
|
|
52
|
+
"tool_input": {"command": command},
|
|
53
|
+
"tool_response": response,
|
|
54
|
+
}
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _fake_gh(pr_object: dict[str, object] | None, stderr: str = "") -> object:
|
|
59
|
+
def _run(*args: object, **kwargs: object) -> subprocess.CompletedProcess[str]:
|
|
60
|
+
del args, kwargs
|
|
61
|
+
if pr_object is None:
|
|
62
|
+
return subprocess.CompletedProcess([], 1, stdout="", stderr=stderr)
|
|
63
|
+
return subprocess.CompletedProcess([], 0, stdout=json.dumps(pr_object), stderr="")
|
|
64
|
+
|
|
65
|
+
return _run
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _run_main(
|
|
69
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
70
|
+
capsys: pytest.CaptureFixture[str],
|
|
71
|
+
payload_text: str,
|
|
72
|
+
fake_gh: object,
|
|
73
|
+
) -> str:
|
|
74
|
+
monkeypatch.setattr(pr_done_reminder.subprocess, "run", fake_gh)
|
|
75
|
+
monkeypatch.setattr(sys, "stdin", io.StringIO(payload_text))
|
|
76
|
+
pr_done_reminder.main()
|
|
77
|
+
return capsys.readouterr().out
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _additional_context(stdout_text: str) -> str:
|
|
81
|
+
return str(json.loads(stdout_text)["hookSpecificOutput"]["additionalContext"])
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@pytest.mark.parametrize(
|
|
85
|
+
"command",
|
|
86
|
+
[
|
|
87
|
+
"git push",
|
|
88
|
+
"git push -u origin feat/x",
|
|
89
|
+
"git -C C:/repo push origin HEAD",
|
|
90
|
+
"gh pr create --draft --title t --body-file b.md",
|
|
91
|
+
"git add -A && git commit -F m.txt && git push",
|
|
92
|
+
"git -c push.default=current push",
|
|
93
|
+
"git --git-dir=C:/repo/.git push",
|
|
94
|
+
'pwsh -NoProfile -Command "git push -u origin feat/x"',
|
|
95
|
+
"pwsh -NoProfile -Command 'gh pr create --draft --fill'",
|
|
96
|
+
'powershell -Command "git add -A; git push"',
|
|
97
|
+
],
|
|
98
|
+
)
|
|
99
|
+
def test_should_wake_on_git_push_and_gh_pr_create(command: str) -> None:
|
|
100
|
+
assert pr_done_reminder.command_triggers_reminder(command) is True
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@pytest.mark.parametrize(
|
|
104
|
+
"command",
|
|
105
|
+
[
|
|
106
|
+
"git status",
|
|
107
|
+
"git pull",
|
|
108
|
+
"git stash push -u -q -- packages",
|
|
109
|
+
"git log --grep push",
|
|
110
|
+
"git commit -m push",
|
|
111
|
+
"gh pr view 42",
|
|
112
|
+
"gh pr ready --undo",
|
|
113
|
+
"echo git push",
|
|
114
|
+
'pwsh -NoProfile -Command "git status"',
|
|
115
|
+
"pytest tests/",
|
|
116
|
+
],
|
|
117
|
+
)
|
|
118
|
+
def test_should_stay_quiet_for_other_commands(command: str) -> None:
|
|
119
|
+
assert pr_done_reminder.command_triggers_reminder(command) is False
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_should_say_done_when_mergeable_and_checks_pass() -> None:
|
|
123
|
+
context = pr_done_reminder.build_reminder_context(_CLEAN_PR_OBJECT)
|
|
124
|
+
|
|
125
|
+
assert context.startswith("=== PR DONE CHECKLIST")
|
|
126
|
+
assert "never a block" in context
|
|
127
|
+
assert "DONE. Add the label now:" in context
|
|
128
|
+
assert "gh pr edit 42 --add-label done" in context
|
|
129
|
+
assert "NOT DONE" not in context
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def test_should_keep_the_checklist_order_verdict_then_recheck_then_footer() -> None:
|
|
133
|
+
all_lines = pr_done_reminder.build_reminder_context(_CLEAN_PR_OBJECT).split("\n")
|
|
134
|
+
|
|
135
|
+
all_labels = [each_line.split(":")[0] for each_line in all_lines[1:-1]]
|
|
136
|
+
assert all_labels == [
|
|
137
|
+
"PR #42 https",
|
|
138
|
+
"Mergeable",
|
|
139
|
+
"CI checks",
|
|
140
|
+
"Draft",
|
|
141
|
+
"Label done",
|
|
142
|
+
"Verdict",
|
|
143
|
+
"Add label",
|
|
144
|
+
"Re-check",
|
|
145
|
+
]
|
|
146
|
+
assert all_lines[-1] == "A PR is done only when every line above is clean."
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_should_say_not_done_and_name_the_conflict_fix() -> None:
|
|
150
|
+
conflicted = {**_CLEAN_PR_OBJECT, "mergeable": "CONFLICTING", "mergeStateStatus": "DIRTY"}
|
|
151
|
+
|
|
152
|
+
context = pr_done_reminder.build_reminder_context(conflicted)
|
|
153
|
+
|
|
154
|
+
assert "NOT DONE" in context
|
|
155
|
+
assert "CONFLICTS with the base branch" in context
|
|
156
|
+
assert "gh pr view 42 --json mergeable" in context
|
|
157
|
+
assert "gh pr edit 42 --add-label" not in context
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def test_should_count_failing_and_pending_checks() -> None:
|
|
161
|
+
mixed_checks = {
|
|
162
|
+
**_CLEAN_PR_OBJECT,
|
|
163
|
+
"statusCheckRollup": [
|
|
164
|
+
{"name": "pytest", "status": "COMPLETED", "conclusion": "FAILURE"},
|
|
165
|
+
{"name": "mypy", "status": "IN_PROGRESS", "conclusion": ""},
|
|
166
|
+
{"context": "lint", "state": "PENDING"},
|
|
167
|
+
{"context": "docs", "state": "SUCCESS"},
|
|
168
|
+
],
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
context = pr_done_reminder.build_reminder_context(mixed_checks)
|
|
172
|
+
|
|
173
|
+
assert "1 failing" in context
|
|
174
|
+
assert "2 pending" in context
|
|
175
|
+
assert "4 total" in context
|
|
176
|
+
assert "NOT DONE" in context
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_should_tell_the_agent_to_wait_when_github_is_still_computing() -> None:
|
|
180
|
+
unknown = {**_CLEAN_PR_OBJECT, "mergeable": "UNKNOWN", "mergeStateStatus": "UNKNOWN"}
|
|
181
|
+
|
|
182
|
+
context = pr_done_reminder.build_reminder_context(unknown)
|
|
183
|
+
|
|
184
|
+
assert "still computing" in context
|
|
185
|
+
assert "NOT DONE" in context
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_should_report_the_done_label_when_already_set() -> None:
|
|
189
|
+
labeled = {**_CLEAN_PR_OBJECT, "labels": [{"name": "done"}]}
|
|
190
|
+
|
|
191
|
+
context = pr_done_reminder.build_reminder_context(labeled)
|
|
192
|
+
|
|
193
|
+
assert "Label done: set" in context
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def test_main_should_emit_additional_context_after_a_successful_push(
|
|
197
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
198
|
+
) -> None:
|
|
199
|
+
stdout_text = _run_main(monkeypatch, capsys, _payload("git push"), _fake_gh(_CLEAN_PR_OBJECT))
|
|
200
|
+
|
|
201
|
+
payload = json.loads(stdout_text)
|
|
202
|
+
assert payload["hookSpecificOutput"]["hookEventName"] == "PostToolUse"
|
|
203
|
+
assert "permissionDecision" not in payload["hookSpecificOutput"]
|
|
204
|
+
assert "decision" not in payload
|
|
205
|
+
assert "PR #42" in _additional_context(stdout_text)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def test_main_should_also_serve_the_powershell_tool(
|
|
209
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
210
|
+
) -> None:
|
|
211
|
+
payload = json.loads(_payload("git push"))
|
|
212
|
+
payload["tool_name"] = "PowerShell"
|
|
213
|
+
|
|
214
|
+
stdout_text = _run_main(monkeypatch, capsys, json.dumps(payload), _fake_gh(_CLEAN_PR_OBJECT))
|
|
215
|
+
|
|
216
|
+
assert "PR #42" in _additional_context(stdout_text)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_main_should_stay_quiet_when_the_push_failed(
|
|
220
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
221
|
+
) -> None:
|
|
222
|
+
failed_push = _payload("git push", "Error: Exit code 1\nrejected")
|
|
223
|
+
|
|
224
|
+
assert _run_main(monkeypatch, capsys, failed_push, _fake_gh(_CLEAN_PR_OBJECT)) == ""
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def test_main_should_stay_quiet_for_a_non_trigger_command(
|
|
228
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
229
|
+
) -> None:
|
|
230
|
+
assert _run_main(monkeypatch, capsys, _payload("git status"), _fake_gh(_CLEAN_PR_OBJECT)) == ""
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def test_main_should_remind_to_open_a_pr_when_the_branch_has_none(
|
|
234
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
235
|
+
) -> None:
|
|
236
|
+
no_pr = _fake_gh(None, stderr='no pull requests found for branch "feat/x"')
|
|
237
|
+
|
|
238
|
+
context = _additional_context(_run_main(monkeypatch, capsys, _payload("git push"), no_pr))
|
|
239
|
+
|
|
240
|
+
assert "No open pull request found" in context
|
|
241
|
+
assert "gh pr create --draft" in context
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def test_main_should_stay_quiet_on_any_other_gh_failure(
|
|
245
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
246
|
+
) -> None:
|
|
247
|
+
gh_down = _fake_gh(None, stderr="error connecting to api.github.com")
|
|
248
|
+
|
|
249
|
+
assert _run_main(monkeypatch, capsys, _payload("git push"), gh_down) == ""
|
|
@@ -14,9 +14,11 @@ collision, and needs no change to that shared, out-of-scope test file.
|
|
|
14
14
|
Reads the tool payload from stdin once, selects the hosted hooks applicable to
|
|
15
15
|
the payload's tool name, and runs each one in-process via the shared
|
|
16
16
|
hosted-hook runner. A hosted PostToolUse hook here carries no permission
|
|
17
|
-
decision -- it is
|
|
18
|
-
|
|
19
|
-
hosted hook
|
|
17
|
+
decision -- it is an observer, not a gate -- so this dispatcher runs the whole
|
|
18
|
+
roster unconditionally and never blocks. The one thing it forwards is context:
|
|
19
|
+
a hosted hook may print a ``hookSpecificOutput.additionalContext`` string (the
|
|
20
|
+
PR done reminder does), and the dispatcher joins every such string into one
|
|
21
|
+
PostToolUse payload. A hook that prints nothing adds nothing.
|
|
20
22
|
|
|
21
23
|
A single hosted hook crash fails open: ``run_hook_capturing_output`` isolates
|
|
22
24
|
it, so it contributes nothing and does not stop the remaining hosted hooks.
|
|
@@ -27,11 +29,17 @@ imports load once into this process rather than once per hook.
|
|
|
27
29
|
|
|
28
30
|
from __future__ import annotations
|
|
29
31
|
|
|
32
|
+
import json
|
|
33
|
+
import sys
|
|
30
34
|
|
|
31
35
|
import _path_setup # noqa: F401
|
|
32
36
|
|
|
33
37
|
from hooks_constants.bash_post_call_dispatcher_constants import (
|
|
38
|
+
ADDITIONAL_CONTEXT_JOIN_SEPARATOR,
|
|
39
|
+
ADDITIONAL_CONTEXT_KEY,
|
|
34
40
|
ALL_BASH_POST_TOOL_USE_HOSTED_HOOK_ENTRIES,
|
|
41
|
+
HOOK_SPECIFIC_OUTPUT_KEY,
|
|
42
|
+
POST_TOOL_USE_HOOK_EVENT_NAME,
|
|
35
43
|
)
|
|
36
44
|
from hooks_constants.bash_pre_tool_use_dispatcher_constants import BashHostedHookEntry
|
|
37
45
|
from hooks_constants.hosted_hook_runner import (
|
|
@@ -50,15 +58,54 @@ def select_applicable_entries(tool_name: str) -> list[BashHostedHookEntry]:
|
|
|
50
58
|
]
|
|
51
59
|
|
|
52
60
|
|
|
61
|
+
def additional_context_from_hook_output(captured_stdout: str) -> str | None:
|
|
62
|
+
"""Return the additionalContext string a hosted hook printed, or None.
|
|
63
|
+
|
|
64
|
+
Empty output, non-JSON output, and JSON without a non-empty
|
|
65
|
+
``hookSpecificOutput.additionalContext`` string all return None.
|
|
66
|
+
"""
|
|
67
|
+
if not captured_stdout.strip():
|
|
68
|
+
return None
|
|
69
|
+
try:
|
|
70
|
+
hook_output = json.loads(captured_stdout)
|
|
71
|
+
except json.JSONDecodeError:
|
|
72
|
+
return None
|
|
73
|
+
if not isinstance(hook_output, dict):
|
|
74
|
+
return None
|
|
75
|
+
hook_specific_output = hook_output.get(HOOK_SPECIFIC_OUTPUT_KEY)
|
|
76
|
+
if not isinstance(hook_specific_output, dict):
|
|
77
|
+
return None
|
|
78
|
+
additional_context = hook_specific_output.get(ADDITIONAL_CONTEXT_KEY)
|
|
79
|
+
if not isinstance(additional_context, str) or not additional_context.strip():
|
|
80
|
+
return None
|
|
81
|
+
return additional_context
|
|
82
|
+
|
|
83
|
+
|
|
53
84
|
def dispatch(payload_text: str, tool_name: str) -> None:
|
|
54
|
-
"""Run every hosted hook applicable to tool_name,
|
|
85
|
+
"""Run every hosted hook applicable to tool_name, then forward their context.
|
|
55
86
|
|
|
56
87
|
Each hosted hook's crash is isolated by ``run_hook_capturing_output``, so
|
|
57
|
-
one hook failing never stops a later one in the same roster.
|
|
88
|
+
one hook failing never stops a later one in the same roster. Every
|
|
89
|
+
additionalContext string the hooks printed is joined into one PostToolUse
|
|
90
|
+
payload on stdout; when no hook printed one, nothing is written.
|
|
58
91
|
"""
|
|
92
|
+
all_additional_context: list[str] = []
|
|
59
93
|
for each_entry in select_applicable_entries(tool_name):
|
|
60
94
|
script_path = resolved_hook_script_path(each_entry.script_relative_path)
|
|
61
|
-
run_hook_capturing_output(script_path, payload_text)
|
|
95
|
+
hook_run = run_hook_capturing_output(script_path, payload_text)
|
|
96
|
+
additional_context = additional_context_from_hook_output(hook_run.captured_stdout)
|
|
97
|
+
if additional_context is not None:
|
|
98
|
+
all_additional_context.append(additional_context)
|
|
99
|
+
if not all_additional_context:
|
|
100
|
+
return
|
|
101
|
+
payload = {
|
|
102
|
+
HOOK_SPECIFIC_OUTPUT_KEY: {
|
|
103
|
+
"hookEventName": POST_TOOL_USE_HOOK_EVENT_NAME,
|
|
104
|
+
ADDITIONAL_CONTEXT_KEY: ADDITIONAL_CONTEXT_JOIN_SEPARATOR.join(all_additional_context),
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
sys.stdout.write(json.dumps(payload))
|
|
108
|
+
sys.stdout.flush()
|
|
62
109
|
|
|
63
110
|
|
|
64
111
|
def main() -> None:
|