claude-dev-env 4.3.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/agents/pr-description-writer.md +2 -2
- package/.agents/agents/test_agent_frontmatter.py +2 -2
- package/.agents/agents/test_pr_description_writer_style.py +1 -3
- package/.agents/skills/e-code-review/SKILL.md +12 -12
- package/.agents/skills/e-code-review/preflight-proposal.contract.test.mjs +20 -0
- package/.agents/skills/e-code-review/reference/fix.md +15 -17
- package/.agents/skills/e-code-review/reference/loop.md +35 -39
- package/.agents/skills/e-code-review/reference/mode-contract.test.mjs +2 -1
- package/.agents/skills/e-code-review/reference/preflight-proposal.md +44 -9
- package/.agents/skills/e-code-review/reference/runner-selection.md +13 -14
- package/.agents/skills/issue-tracker/SKILL.md +5 -4
- package/.agents/skills/pull-request/SKILL.md +11 -13
- package/.agents/skills/pull-request/reference/publication-tasks.md +3 -3
- package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/constants.py +9 -1
- package/.agents/skills/pull-request/scripts/pull_request.py +33 -44
- package/.agents/skills/pull-request/scripts/test_github_pr_command_constants.py +9 -0
- package/.agents/skills/pull-request/scripts/test_pull_request.py +1 -3
- package/.agents/skills/skill-builder/SKILL.md +3 -196
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +6 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +64 -19
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +59 -0
- package/bin/install.agents-home.test.mjs +15 -13
- package/bin/install.mjs +28 -0
- package/bin/install.policy-lint.test.mjs +78 -0
- package/bin/install.prune.test.mjs +59 -20
- package/bin/install.pstack-plugin.test.mjs +114 -0
- package/bin/install.test.mjs +37 -0
- package/bin/refresh-pstack-plugin-skills.test.mjs +211 -0
- package/hooks/advisory/pr_done_reminder.py +342 -0
- package/hooks/advisory/test_pr_done_reminder.py +249 -0
- package/hooks/blocking/bash_post_call_dispatcher.py +53 -6
- package/hooks/blocking/code_rules_paired_test.py +22 -15
- package/hooks/blocking/test_bash_post_call_dispatcher.py +65 -4
- package/hooks/blocking/test_code_rules_enforcer_paired_test.py +36 -29
- package/hooks/blocking/test_code_rules_js_resume_task_enumeration.py +4 -4
- package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +5 -5
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +68 -133
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +20 -51
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +18 -18
- package/hooks/git-hooks/git_hooks_constants/staged_policy_lint.py +12 -0
- package/hooks/git-hooks/pre_commit.py +52 -11
- package/hooks/git-hooks/test_installed_policy_lint.py +159 -0
- package/hooks/git-hooks/test_native_policy_commit.py +87 -0
- package/hooks/git-hooks/test_pre_commit.py +246 -0
- package/hooks/hooks.json +1 -1
- package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +14 -1
- package/hooks/hooks_constants/pr_done_reminder_constants.py +98 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -43
- package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +19 -2
- package/hooks/hooks_constants/test_pr_done_reminder_constants.py +40 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +20 -32
- package/package.json +2 -2
- package/rules/durable-post-artifacts.md +2 -1
- package/rules/git-workflow.md +6 -6
- package/scripts/check.ps1 +0 -1
- package/{.agents/skills/run-claude-dev-env/driver.mjs → scripts/ci/installer-lifecycle.mjs} +3 -28
- package/scripts/ci/installer-lifecycle.test.mjs +26 -0
- package/scripts/ci/windows-installer-lifecycle.ps1 +1 -11
- package/scripts/dev_env_scripts_constants/shared_tree_constants.py +4 -5
- package/{.agents/skills/_shared/pr-loop/scripts → scripts}/durable_post_lint.py +5 -41
- package/{.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/durable_post_lint_constants.py → scripts/durable_post_lint_config/config/constants.py} +0 -10
- package/{.agents/skills/_shared/pr-loop/scripts/test_durable_post_lint_constants.py → scripts/durable_post_lint_config/config/test_constants.py} +1 -1
- package/scripts/policy_lint/adapter_detectors.py +13 -6
- package/scripts/policy_lint/adapters.py +7 -4
- package/scripts/policy_lint/config/constants.py +5 -0
- package/scripts/policy_lint/engine.py +48 -4
- package/scripts/policy_lint/model.py +1 -0
- package/scripts/policy_lint/registry.py +1 -1
- package/scripts/policy_lint/selection.py +2 -1
- package/scripts/policy_lint/selection_git.py +2 -0
- package/scripts/refresh_pstack_plugin_skills.mjs +200 -0
- package/scripts/shared_tree_paths.py +32 -3
- package/scripts/test_shared_tree_paths.py +33 -0
- package/scripts/tests/test_policy_lint_rules_archive.py +120 -0
- package/scripts/tests/test_policy_lint_rules_engine.py +25 -4
- package/scripts/tests/test_policy_lint_rules_installed_loader.py +29 -0
- package/scripts/tests/test_policy_lint_rules_registry.py +227 -13
- package/scripts/tests/test_policy_lint_selection.py +25 -2
- package/scripts/tests/test_policy_lint_selection_native_index.py +30 -0
- package/scripts/tests/test_rules.py +3 -7
- package/.agents/skills/_shared/pr-loop/.claude/CLAUDE.md +0 -1
- package/.agents/skills/_shared/pr-loop/AGENTS.md +0 -1
- package/.agents/skills/_shared/pr-loop/audit-contract.md +0 -5
- package/.agents/skills/_shared/pr-loop/audit-reply-template.md +0 -5
- package/.agents/skills/_shared/pr-loop/code-rules-gate.md +0 -5
- package/.agents/skills/_shared/pr-loop/fix-protocol.md +0 -5
- package/.agents/skills/_shared/pr-loop/gh-payloads.md +0 -5
- package/.agents/skills/_shared/pr-loop/portable-driver.md +0 -149
- package/.agents/skills/_shared/pr-loop/post-audit-thread-contract.md +0 -5
- package/.agents/skills/_shared/pr-loop/precatch-rubric.md +0 -5
- package/.agents/skills/_shared/pr-loop/preflight-proposal.contract.test.mjs +0 -119
- package/.agents/skills/_shared/pr-loop/preflight-proposal.md +0 -5
- package/.agents/skills/_shared/pr-loop/prompts/.claude/CLAUDE.md +0 -1
- package/.agents/skills/_shared/pr-loop/prompts/AGENTS.md +0 -1
- package/.agents/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +0 -324
- package/.agents/skills/_shared/pr-loop/scripts/.claude/CLAUDE.md +0 -1
- package/.agents/skills/_shared/pr-loop/scripts/AGENTS.md +0 -1
- package/.agents/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +0 -29
- package/.agents/skills/_shared/pr-loop/scripts/_cli_utils.py +0 -22
- package/.agents/skills/_shared/pr-loop/scripts/_path_resolver.py +0 -186
- package/.agents/skills/_shared/pr-loop/scripts/_xml_utils.py +0 -20
- package/.agents/skills/_shared/pr-loop/scripts/audit_category_schema.py +0 -355
- package/.agents/skills/_shared/pr-loop/scripts/build_audit_prompt.py +0 -289
- package/.agents/skills/_shared/pr-loop/scripts/build_converge_task_list.py +0 -310
- package/.agents/skills/_shared/pr-loop/scripts/build_fix_prompt.py +0 -292
- package/.agents/skills/_shared/pr-loop/scripts/init_loop_state.py +0 -134
- package/.agents/skills/_shared/pr-loop/scripts/portable_converge_driver.py +0 -1646
- package/.agents/skills/_shared/pr-loop/scripts/preflight_worktree.py +0 -392
- package/.agents/skills/_shared/pr-loop/scripts/select_converge_pacer.py +0 -215
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/.claude/CLAUDE.md +0 -1
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +0 -1
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/__init__.py +0 -0
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +0 -37
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/converge_task_list_constants.py +0 -56
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/handoff_constants.py +0 -45
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/pacer_constants.py +0 -47
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +0 -157
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/portable_driver_constants.py +0 -177
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +0 -70
- package/.agents/skills/_shared/pr-loop/scripts/teardown_worktrees.py +0 -172
- package/.agents/skills/_shared/pr-loop/scripts/test__path_resolver.py +0 -106
- package/.agents/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +0 -94
- package/.agents/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +0 -499
- package/.agents/skills/_shared/pr-loop/scripts/test_build_converge_task_list.py +0 -140
- package/.agents/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +0 -262
- package/.agents/skills/_shared/pr-loop/scripts/test_init_loop_state.py +0 -71
- package/.agents/skills/_shared/pr-loop/scripts/test_portable_converge_driver.py +0 -1119
- package/.agents/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +0 -287
- package/.agents/skills/_shared/pr-loop/scripts/test_select_converge_pacer.py +0 -207
- package/.agents/skills/_shared/pr-loop/scripts/test_teardown_worktrees.py +0 -59
- package/.agents/skills/_shared/pr-loop/scripts/test_write_handoff.py +0 -201
- package/.agents/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +0 -182
- package/.agents/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +0 -206
- package/.agents/skills/_shared/pr-loop/scripts/write_handoff.py +0 -309
- package/.agents/skills/_shared/pr-loop/state-schema.md +0 -5
- package/.agents/skills/_shared/pr-loop/worker-spawn.md +0 -5
- package/.agents/skills/autoconverge/.claude/CLAUDE.md +0 -1
- package/.agents/skills/autoconverge/AGENTS.md +0 -1
- package/.agents/skills/autoconverge/SKILL.md +0 -431
- package/.agents/skills/autoconverge/reference/.claude/CLAUDE.md +0 -1
- package/.agents/skills/autoconverge/reference/AGENTS.md +0 -1
- package/.agents/skills/autoconverge/reference/closing-report.md +0 -93
- package/.agents/skills/autoconverge/reference/convergence-gates.md +0 -31
- package/.agents/skills/autoconverge/reference/convergence.md +0 -219
- package/.agents/skills/autoconverge/reference/copilot-findings.md +0 -51
- package/.agents/skills/autoconverge/reference/gotchas.md +0 -66
- package/.agents/skills/autoconverge/reference/headless-safety.md +0 -44
- package/.agents/skills/autoconverge/reference/multi-pr.md +0 -128
- package/.agents/skills/autoconverge/reference/self-closing-loop.md +0 -84
- package/.agents/skills/autoconverge/reference/stop-conditions.md +0 -127
- package/.agents/skills/autoconverge/test_portable_pacer_gate.py +0 -54
- package/.agents/skills/autoconverge/workflow/.claude/CLAUDE.md +0 -1
- package/.agents/skills/autoconverge/workflow/AGENTS.md +0 -1
- package/.agents/skills/autoconverge/workflow/aggregate_runs.py +0 -371
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/.claude/CLAUDE.md +0 -1
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +0 -1
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/__init__.py +0 -0
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +0 -250
- package/.agents/skills/autoconverge/workflow/converge.clean-audit.test.mjs +0 -665
- package/.agents/skills/autoconverge/workflow/converge.codex-gate.test.mjs +0 -472
- package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +0 -64
- package/.agents/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +0 -1009
- package/.agents/skills/autoconverge/workflow/converge.fix-progress.test.mjs +0 -107
- package/.agents/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +0 -33
- package/.agents/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +0 -15
- package/.agents/skills/autoconverge/workflow/converge.mjs +0 -3132
- package/.agents/skills/autoconverge/workflow/converge.p2-advance.test.mjs +0 -202
- package/.agents/skills/autoconverge/workflow/converge.path-aware.test.mjs +0 -47
- package/.agents/skills/autoconverge/workflow/converge.precatch.test.mjs +0 -152
- package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +0 -116
- package/.agents/skills/autoconverge/workflow/converge_multi.mjs +0 -169
- package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +0 -173
- package/.agents/skills/autoconverge/workflow/convergence_summary.py +0 -110
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +0 -266
- package/.agents/skills/autoconverge/workflow/render_report.py +0 -1024
- package/.agents/skills/autoconverge/workflow/test_aggregate_runs.py +0 -134
- package/.agents/skills/autoconverge/workflow/test_convergence_summary.py +0 -149
- package/.agents/skills/autoconverge/workflow/test_render_report.py +0 -812
- package/.agents/skills/hitl/SKILL.md +0 -45
- package/.agents/skills/pr-cleanup/SKILL.md +0 -140
- package/.agents/skills/pr-cleanup/reference/process-inventory.md +0 -17
- package/.agents/skills/pr-cleanup/reference/task-seeds.md +0 -22
- package/.agents/skills/pr-name-by-capability/SKILL.md +0 -73
- package/.agents/skills/pr-name-by-capability/reference/fetch-commands.md +0 -29
- package/.agents/skills/pr-name-by-capability/reference/offense-examples.md +0 -32
- package/.agents/skills/pr-name-by-capability/reference/report-template.md +0 -29
- package/.agents/skills/pr-name-by-capability/reference/rule-checklist.md +0 -38
- package/.agents/skills/pr-name-by-capability/reference/task-seeds.md +0 -10
- package/.agents/skills/pr-plain-language-cleanup/SKILL.md +0 -93
- package/.agents/skills/pr-refinement/SKILL.md +0 -31
- package/.agents/skills/pr-shared-extraction/SKILL.md +0 -177
- package/.agents/skills/pr-shared-extraction/reference/examples.md +0 -46
- package/.agents/skills/pr-shared-extraction/reference/offense-taxonomy.md +0 -100
- package/.agents/skills/pr-small-cl/SKILL.md +0 -52
- package/.agents/skills/pr-title-description/SKILL.md +0 -96
- package/.agents/skills/prototype/SKILL.md +0 -85
- package/.agents/skills/prototype/reference/honest-limitations.md +0 -23
- package/.agents/skills/prototype/reference/promotion-tasks.md +0 -23
- package/.agents/skills/prototype/scripts/build_sandbox_settings.py +0 -249
- package/.agents/skills/prototype/scripts/conftest.py +0 -15
- package/.agents/skills/prototype/scripts/launch_sandbox.py +0 -205
- package/.agents/skills/prototype/scripts/probe_sandbox_safety.py +0 -311
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/__init__.py +0 -1
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +0 -41
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +0 -23
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +0 -45
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +0 -10
- package/.agents/skills/prototype/scripts/test_build_sandbox_settings.py +0 -275
- package/.agents/skills/prototype/scripts/test_launch_sandbox.py +0 -303
- package/.agents/skills/prototype/scripts/test_probe_sandbox_safety.py +0 -312
- package/.agents/skills/prototype/workflows/promotion.md +0 -25
- package/.agents/skills/prototype/workflows/sandbox.md +0 -35
- package/.agents/skills/rebase/.claude/CLAUDE.md +0 -1
- package/.agents/skills/rebase/AGENTS.md +0 -1
- package/.agents/skills/rebase/SKILL.md +0 -178
- package/.agents/skills/review-router/SKILL.md +0 -51
- package/.agents/skills/review-router/reference/route-policy.json +0 -7
- package/.agents/skills/review-router/reference/task-seeds.md +0 -1
- package/.agents/skills/review-router/scripts/conftest.py +0 -4
- package/.agents/skills/review-router/scripts/review_router.py +0 -50
- package/.agents/skills/review-router/scripts/review_router_cli.py +0 -211
- package/.agents/skills/review-router/scripts/review_router_constants/__init__.py +0 -1
- package/.agents/skills/review-router/scripts/review_router_constants/config/__init__.py +0 -0
- package/.agents/skills/review-router/scripts/review_router_constants/config/constants.py +0 -13
- package/.agents/skills/review-router/scripts/test_review_router.py +0 -23
- package/.agents/skills/review-router/scripts/test_review_router_cli.py +0 -47
- package/.agents/skills/review-tier/SKILL.md +0 -56
- package/.agents/skills/review-tier/reference/task-seeds.md +0 -1
- package/.agents/skills/review-tier/reference/tier-policy.json +0 -9
- package/.agents/skills/review-tier/scripts/conftest.py +0 -4
- package/.agents/skills/review-tier/scripts/review_tier.py +0 -359
- package/.agents/skills/review-tier/scripts/review_tier_constants/__init__.py +0 -1
- package/.agents/skills/review-tier/scripts/review_tier_constants/config/__init__.py +0 -0
- package/.agents/skills/review-tier/scripts/review_tier_constants/config/constants.py +0 -29
- package/.agents/skills/review-tier/scripts/test_generation.py +0 -159
- package/.agents/skills/review-tier/scripts/test_review_tier.py +0 -33
- package/.agents/skills/run-claude-dev-env/SKILL.md +0 -84
- package/.agents/skills/session-log/.claude/CLAUDE.md +0 -1
- package/.agents/skills/session-log/AGENTS.md +0 -1
- package/.agents/skills/session-log/SKILL.md +0 -170
- package/.agents/skills/session-log/templates/frontmatter.md +0 -40
- package/.agents/skills/session-tidy/.claude/CLAUDE.md +0 -1
- package/.agents/skills/session-tidy/AGENTS.md +0 -1
- package/.agents/skills/session-tidy/SKILL.md +0 -191
- package/.agents/skills/skill-builder/references/.claude/CLAUDE.md +0 -1
- package/.agents/skills/skill-builder/references/AGENTS.md +0 -1
- package/.agents/skills/skill-builder/references/delegation-map.md +0 -130
- package/.agents/skills/skill-builder/references/description-field.md +0 -111
- package/.agents/skills/skill-builder/references/deterministic-elements.md +0 -218
- package/.agents/skills/skill-builder/references/progressive-disclosure.md +0 -122
- package/.agents/skills/skill-builder/references/self-audit-checklist.md +0 -126
- package/.agents/skills/skill-builder/references/skill-modularity.md +0 -111
- package/.agents/skills/skill-builder/references/skill-types.md +0 -232
- package/.agents/skills/skill-builder/references/thariq-x-post-skills.json +0 -33
- package/.agents/skills/skill-builder/templates/.claude/CLAUDE.md +0 -1
- package/.agents/skills/skill-builder/templates/AGENTS.md +0 -1
- package/.agents/skills/skill-builder/templates/gap-analysis.md +0 -87
- package/.agents/skills/skill-builder/workflows/.claude/CLAUDE.md +0 -1
- package/.agents/skills/skill-builder/workflows/AGENTS.md +0 -1
- package/.agents/skills/skill-builder/workflows/improve-skill.md +0 -136
- package/.agents/skills/skill-builder/workflows/new-skill.md +0 -179
- package/.agents/skills/skill-builder/workflows/polish-skill.md +0 -139
- package/.agents/skills/source-command-sr-loop/SKILL.md +0 -82
- package/.agents/skills/update/.claude/CLAUDE.md +0 -1
- package/.agents/skills/update/AGENTS.md +0 -1
- package/.agents/skills/update/SKILL.md +0 -190
- package/_shared/pr-loop/AGENTS.md +0 -1
- package/_shared/pr-loop/CLAUDE.md +0 -1
- package/_shared/pr-loop/audit-contract.md +0 -183
- package/_shared/pr-loop/audit-reply-template.md +0 -151
- package/_shared/pr-loop/code-rules-gate.md +0 -65
- package/_shared/pr-loop/fix-protocol.md +0 -60
- package/_shared/pr-loop/gh-payloads.md +0 -75
- package/_shared/pr-loop/post-audit-thread-contract.md +0 -51
- package/_shared/pr-loop/precatch-rubric.md +0 -66
- package/_shared/pr-loop/preflight-proposal.md +0 -56
- package/_shared/pr-loop/scripts/post_audit_thread.py +0 -1242
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/post_audit_thread_constants.py +0 -199
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +0 -1115
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +0 -129
- package/_shared/pr-loop/state-schema.md +0 -45
- package/_shared/pr-loop/worker-spawn.md +0 -189
- /package/{.agents/skills/_shared/pr-loop/scripts → scripts}/test_durable_post_lint.py +0 -0
|
@@ -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:
|
|
@@ -215,12 +215,27 @@ def _collect_referenced_identifiers(source: str) -> set[str]:
|
|
|
215
215
|
return all_referenced_identifiers
|
|
216
216
|
|
|
217
217
|
|
|
218
|
+
def _sibling_test_paths(stem_test_path: Path) -> list[Path]:
|
|
219
|
+
"""Return the capped, sorted test files beside the stem test, stem excluded."""
|
|
220
|
+
suite_directory = stem_test_path.parent
|
|
221
|
+
stem_path_key = os.path.normcase(str(stem_test_path.resolve()))
|
|
222
|
+
all_sibling_paths: list[Path] = []
|
|
223
|
+
for each_glob in ALL_TEST_FILENAME_GLOBS:
|
|
224
|
+
all_sibling_paths.extend(
|
|
225
|
+
each_path
|
|
226
|
+
for each_path in sorted(suite_directory.glob(each_glob))
|
|
227
|
+
if os.path.normcase(str(each_path.resolve())) != stem_path_key
|
|
228
|
+
)
|
|
229
|
+
return all_sibling_paths[:MAX_TEST_FILES_SCANNED]
|
|
230
|
+
|
|
231
|
+
|
|
218
232
|
def _suite_referenced_identifiers(stem_test_path: Path) -> set[str]:
|
|
219
233
|
"""Return identifiers referenced across every test file beside the stem test.
|
|
220
234
|
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
each one references.
|
|
235
|
+
Reads the stem-matched test file first, then scans every other ``test_*.py``
|
|
236
|
+
and ``*_test.py`` file in its directory, bounded by the scan cap, and unions
|
|
237
|
+
the identifiers each one references. The stem test always counts, so a
|
|
238
|
+
directory with more test files than the cap cannot hide it.
|
|
224
239
|
|
|
225
240
|
Args:
|
|
226
241
|
stem_test_path: The stem-matched test file whose directory is scanned.
|
|
@@ -228,12 +243,9 @@ def _suite_referenced_identifiers(stem_test_path: Path) -> set[str]:
|
|
|
228
243
|
Returns:
|
|
229
244
|
The union of referenced identifiers across the scanned test files.
|
|
230
245
|
"""
|
|
231
|
-
|
|
232
|
-
all_test_file_paths: list[Path] = []
|
|
233
|
-
for each_glob in ALL_TEST_FILENAME_GLOBS:
|
|
234
|
-
all_test_file_paths.extend(sorted(suite_directory.glob(each_glob)))
|
|
246
|
+
all_test_file_paths = [stem_test_path, *_sibling_test_paths(stem_test_path)]
|
|
235
247
|
all_referenced_identifiers: set[str] = set()
|
|
236
|
-
for each_test_file_path in all_test_file_paths
|
|
248
|
+
for each_test_file_path in all_test_file_paths:
|
|
237
249
|
try:
|
|
238
250
|
test_source = each_test_file_path.read_text(encoding="utf-8")
|
|
239
251
|
except (OSError, UnicodeDecodeError):
|
|
@@ -364,16 +376,12 @@ def _production_stem_from_test_filename(test_filename: str) -> str | None:
|
|
|
364
376
|
if test_filename.startswith(STEM_TEST_FILENAME_PREFIX) and test_filename.endswith(
|
|
365
377
|
PYTHON_SOURCE_SUFFIX
|
|
366
378
|
):
|
|
367
|
-
production_stem = test_filename[
|
|
368
|
-
len(STEM_TEST_FILENAME_PREFIX) : -len(PYTHON_SOURCE_SUFFIX)
|
|
369
|
-
]
|
|
379
|
+
production_stem = test_filename[len(STEM_TEST_FILENAME_PREFIX) : -len(PYTHON_SOURCE_SUFFIX)]
|
|
370
380
|
return production_stem or None
|
|
371
381
|
return None
|
|
372
382
|
|
|
373
383
|
|
|
374
|
-
def _paired_production_module_path(
|
|
375
|
-
test_path: Path, production_stem: str
|
|
376
|
-
) -> Path | None:
|
|
384
|
+
def _paired_production_module_path(test_path: Path, production_stem: str) -> Path | None:
|
|
377
385
|
"""Return the production module a stem-matched test file pairs with, or None.
|
|
378
386
|
|
|
379
387
|
Mirrors ``_stem_matched_test_path`` in reverse for the two common layouts: a
|
|
@@ -515,4 +523,3 @@ def check_test_file_omits_module_public_function(
|
|
|
515
523
|
if len(all_omission_violations) >= MAX_PAIRED_TEST_COVERAGE_ISSUES:
|
|
516
524
|
break
|
|
517
525
|
return all_omission_violations
|
|
518
|
-
|
|
@@ -15,16 +15,26 @@ from pathlib import Path
|
|
|
15
15
|
|
|
16
16
|
import pytest
|
|
17
17
|
|
|
18
|
-
from bash_post_call_dispatcher import
|
|
18
|
+
from bash_post_call_dispatcher import (
|
|
19
|
+
additional_context_from_hook_output,
|
|
20
|
+
dispatch,
|
|
21
|
+
main,
|
|
22
|
+
select_applicable_entries,
|
|
23
|
+
)
|
|
19
24
|
from hooks_constants.bash_pre_tool_use_dispatcher_constants import BASH_TOOL_NAME
|
|
20
25
|
from hooks_constants.hosted_hook_runner import HostedHookRun
|
|
21
26
|
from tdd_enforcer_parts import content_hash_store
|
|
22
27
|
|
|
28
|
+
_ALL_EXPECTED_ROSTER_PATHS = [
|
|
29
|
+
"observability/test_failure_recorder.py",
|
|
30
|
+
"advisory/pr_done_reminder.py",
|
|
31
|
+
]
|
|
23
32
|
|
|
24
|
-
|
|
33
|
+
|
|
34
|
+
def test_select_applicable_entries_returns_the_hosted_hooks_for_bash() -> None:
|
|
25
35
|
all_entries = select_applicable_entries(BASH_TOOL_NAME)
|
|
26
36
|
all_script_paths = [each_entry.script_relative_path for each_entry in all_entries]
|
|
27
|
-
assert all_script_paths ==
|
|
37
|
+
assert all_script_paths == _ALL_EXPECTED_ROSTER_PATHS
|
|
28
38
|
|
|
29
39
|
|
|
30
40
|
def test_select_applicable_entries_returns_none_for_a_write_tool() -> None:
|
|
@@ -45,7 +55,58 @@ def test_dispatch_runs_every_hosted_hook_in_roster_order(
|
|
|
45
55
|
|
|
46
56
|
dispatch('{"tool_name": "Bash"}', BASH_TOOL_NAME)
|
|
47
57
|
|
|
48
|
-
assert all_call_paths == [
|
|
58
|
+
assert all_call_paths == [Path(each_path).name for each_path in _ALL_EXPECTED_ROSTER_PATHS]
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_dispatch_forwards_hosted_hook_additional_context_as_one_payload(
|
|
62
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
63
|
+
) -> None:
|
|
64
|
+
context_by_hook = {
|
|
65
|
+
"test_failure_recorder.py": "",
|
|
66
|
+
"pr_done_reminder.py": json.dumps(
|
|
67
|
+
{"hookSpecificOutput": {"hookEventName": "PostToolUse", "additionalContext": "PR #42"}}
|
|
68
|
+
),
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
def _fake_run_hook(script_path: str, payload_text: str) -> HostedHookRun:
|
|
72
|
+
del payload_text
|
|
73
|
+
return HostedHookRun(
|
|
74
|
+
captured_stdout=context_by_hook[Path(script_path).name], did_crash=False
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
monkeypatch.setattr("bash_post_call_dispatcher.run_hook_capturing_output", _fake_run_hook)
|
|
78
|
+
|
|
79
|
+
dispatch('{"tool_name": "Bash"}', BASH_TOOL_NAME)
|
|
80
|
+
|
|
81
|
+
emitted = json.loads(capsys.readouterr().out)
|
|
82
|
+
assert emitted == {
|
|
83
|
+
"hookSpecificOutput": {"hookEventName": "PostToolUse", "additionalContext": "PR #42"}
|
|
84
|
+
}
|
|
85
|
+
assert "decision" not in emitted
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def test_dispatch_writes_nothing_when_no_hosted_hook_printed_context(
|
|
89
|
+
monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
|
|
90
|
+
) -> None:
|
|
91
|
+
def _fake_run_hook(script_path: str, payload_text: str) -> HostedHookRun:
|
|
92
|
+
del script_path, payload_text
|
|
93
|
+
return HostedHookRun(captured_stdout="", did_crash=False)
|
|
94
|
+
|
|
95
|
+
monkeypatch.setattr("bash_post_call_dispatcher.run_hook_capturing_output", _fake_run_hook)
|
|
96
|
+
|
|
97
|
+
dispatch('{"tool_name": "Bash"}', BASH_TOOL_NAME)
|
|
98
|
+
|
|
99
|
+
assert capsys.readouterr().out == ""
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
@pytest.mark.parametrize(
|
|
103
|
+
"captured_stdout",
|
|
104
|
+
["", " ", "not json", "[]", '{"decision": "block"}', '{"hookSpecificOutput": {}}'],
|
|
105
|
+
)
|
|
106
|
+
def test_additional_context_from_hook_output_ignores_non_context_output(
|
|
107
|
+
captured_stdout: str,
|
|
108
|
+
) -> None:
|
|
109
|
+
assert additional_context_from_hook_output(captured_stdout) is None
|
|
49
110
|
|
|
50
111
|
|
|
51
112
|
def test_main_exits_zero_and_writes_nothing_to_stdout(
|
|
@@ -19,6 +19,10 @@ _BLOCKING_DIRECTORY = Path(__file__).resolve().parent
|
|
|
19
19
|
if str(_BLOCKING_DIRECTORY) not in sys.path:
|
|
20
20
|
sys.path.insert(0, str(_BLOCKING_DIRECTORY))
|
|
21
21
|
|
|
22
|
+
from hooks_constants.paired_test_coverage_constants import ( # noqa: E402
|
|
23
|
+
MAX_TEST_FILES_SCANNED,
|
|
24
|
+
)
|
|
25
|
+
|
|
22
26
|
from code_rules_paired_test import ( # noqa: E402
|
|
23
27
|
check_public_function_missing_paired_test,
|
|
24
28
|
check_test_file_omits_module_public_function,
|
|
@@ -26,9 +30,7 @@ from code_rules_paired_test import ( # noqa: E402
|
|
|
26
30
|
|
|
27
31
|
_TWO_PUBLIC_FUNCTIONS = "def alpha():\n return 1\n\n\ndef beta():\n return 2\n"
|
|
28
32
|
_THREE_PUBLIC_FUNCTIONS = (
|
|
29
|
-
"def alpha():\n return 1\n\n\n"
|
|
30
|
-
"def beta():\n return 2\n\n\n"
|
|
31
|
-
"def gamma():\n return 3\n"
|
|
33
|
+
"def alpha():\n return 1\n\n\ndef beta():\n return 2\n\n\ndef gamma():\n return 3\n"
|
|
32
34
|
)
|
|
33
35
|
|
|
34
36
|
|
|
@@ -145,6 +147,28 @@ def test_counts_coverage_across_sibling_test_files(
|
|
|
145
147
|
assert all_issues == []
|
|
146
148
|
|
|
147
149
|
|
|
150
|
+
def test_reads_the_stem_test_when_the_suite_exceeds_the_scan_cap(
|
|
151
|
+
neutral_package_directory: Path,
|
|
152
|
+
) -> None:
|
|
153
|
+
filler_body = "def test_nothing():\n assert True\n"
|
|
154
|
+
_write_test_file(
|
|
155
|
+
neutral_package_directory,
|
|
156
|
+
"test_a0000.py",
|
|
157
|
+
"from pkg.mod import alpha\n\ndef test_alpha():\n assert alpha() == 1\n",
|
|
158
|
+
)
|
|
159
|
+
for each_index in range(1, MAX_TEST_FILES_SCANNED + 1):
|
|
160
|
+
_write_test_file(neutral_package_directory, f"test_a{each_index:04d}.py", filler_body)
|
|
161
|
+
_write_test_file(
|
|
162
|
+
neutral_package_directory,
|
|
163
|
+
"test_mod.py",
|
|
164
|
+
"from pkg.mod import beta\n\ndef test_beta():\n assert beta() == 2\n",
|
|
165
|
+
)
|
|
166
|
+
all_issues = check_public_function_missing_paired_test(
|
|
167
|
+
_TWO_PUBLIC_FUNCTIONS, str(neutral_package_directory / "mod.py")
|
|
168
|
+
)
|
|
169
|
+
assert all_issues == []
|
|
170
|
+
|
|
171
|
+
|
|
148
172
|
def test_main_and_private_functions_are_never_required(
|
|
149
173
|
neutral_package_directory: Path,
|
|
150
174
|
) -> None:
|
|
@@ -183,8 +207,7 @@ def _write_module(package_directory: Path, body: str) -> None:
|
|
|
183
207
|
|
|
184
208
|
|
|
185
209
|
_SUITE_COVERS_ALPHA_BETA = (
|
|
186
|
-
"from pkg.mod import alpha, beta\n\n"
|
|
187
|
-
"def test_pair():\n assert alpha() == 1 and beta() == 2\n"
|
|
210
|
+
"from pkg.mod import alpha, beta\n\ndef test_pair():\n assert alpha() == 1 and beta() == 2\n"
|
|
188
211
|
)
|
|
189
212
|
|
|
190
213
|
|
|
@@ -193,9 +216,7 @@ def test_flags_module_public_function_when_test_suite_omits_it(
|
|
|
193
216
|
) -> None:
|
|
194
217
|
_write_module(neutral_package_directory, _THREE_PUBLIC_FUNCTIONS)
|
|
195
218
|
test_path = str(neutral_package_directory / "tests" / "test_mod.py")
|
|
196
|
-
all_issues = check_test_file_omits_module_public_function(
|
|
197
|
-
_SUITE_COVERS_ALPHA_BETA, test_path
|
|
198
|
-
)
|
|
219
|
+
all_issues = check_test_file_omits_module_public_function(_SUITE_COVERS_ALPHA_BETA, test_path)
|
|
199
220
|
assert len(all_issues) == 1
|
|
200
221
|
assert "gamma" in all_issues[0]
|
|
201
222
|
assert "mod.py" in all_issues[0]
|
|
@@ -231,9 +252,7 @@ def test_skips_test_file_with_no_paired_production_module(
|
|
|
231
252
|
neutral_package_directory: Path,
|
|
232
253
|
) -> None:
|
|
233
254
|
test_path = str(neutral_package_directory / "tests" / "test_mod.py")
|
|
234
|
-
all_issues = check_test_file_omits_module_public_function(
|
|
235
|
-
_SUITE_COVERS_ALPHA_BETA, test_path
|
|
236
|
-
)
|
|
255
|
+
all_issues = check_test_file_omits_module_public_function(_SUITE_COVERS_ALPHA_BETA, test_path)
|
|
237
256
|
assert all_issues == []
|
|
238
257
|
|
|
239
258
|
|
|
@@ -242,9 +261,7 @@ def test_ignores_a_non_stem_matched_written_file(
|
|
|
242
261
|
) -> None:
|
|
243
262
|
_write_module(neutral_package_directory, _THREE_PUBLIC_FUNCTIONS)
|
|
244
263
|
helper_path = str(neutral_package_directory / "helper.py")
|
|
245
|
-
all_issues = check_test_file_omits_module_public_function(
|
|
246
|
-
_SUITE_COVERS_ALPHA_BETA, helper_path
|
|
247
|
-
)
|
|
264
|
+
all_issues = check_test_file_omits_module_public_function(_SUITE_COVERS_ALPHA_BETA, helper_path)
|
|
248
265
|
assert all_issues == []
|
|
249
266
|
|
|
250
267
|
|
|
@@ -274,9 +291,7 @@ def test_judges_post_edit_content_over_stale_on_disk_test(
|
|
|
274
291
|
"from pkg.mod import alpha, beta, gamma\n\n"
|
|
275
292
|
"def test_all():\n assert alpha() and beta() and gamma()\n"
|
|
276
293
|
)
|
|
277
|
-
all_issues = check_test_file_omits_module_public_function(
|
|
278
|
-
post_edit_covers_all, test_path
|
|
279
|
-
)
|
|
294
|
+
all_issues = check_test_file_omits_module_public_function(post_edit_covers_all, test_path)
|
|
280
295
|
assert all_issues == []
|
|
281
296
|
|
|
282
297
|
|
|
@@ -290,9 +305,7 @@ def test_counts_coverage_across_sibling_test_files_on_test_write(
|
|
|
290
305
|
"from pkg.mod import gamma\n\ndef test_gamma():\n assert gamma() == 3\n",
|
|
291
306
|
)
|
|
292
307
|
test_path = str(neutral_package_directory / "tests" / "test_mod.py")
|
|
293
|
-
all_issues = check_test_file_omits_module_public_function(
|
|
294
|
-
_SUITE_COVERS_ALPHA_BETA, test_path
|
|
295
|
-
)
|
|
308
|
+
all_issues = check_test_file_omits_module_public_function(_SUITE_COVERS_ALPHA_BETA, test_path)
|
|
296
309
|
assert all_issues == []
|
|
297
310
|
|
|
298
311
|
|
|
@@ -301,9 +314,7 @@ def test_test_side_message_names_module_and_function_without_line_prefix(
|
|
|
301
314
|
) -> None:
|
|
302
315
|
_write_module(neutral_package_directory, _THREE_PUBLIC_FUNCTIONS)
|
|
303
316
|
test_path = str(neutral_package_directory / "tests" / "test_mod.py")
|
|
304
|
-
all_issues = check_test_file_omits_module_public_function(
|
|
305
|
-
_SUITE_COVERS_ALPHA_BETA, test_path
|
|
306
|
-
)
|
|
317
|
+
all_issues = check_test_file_omits_module_public_function(_SUITE_COVERS_ALPHA_BETA, test_path)
|
|
307
318
|
assert len(all_issues) == 1
|
|
308
319
|
assert not all_issues[0].startswith("Line ")
|
|
309
320
|
assert "mod.py" in all_issues[0]
|
|
@@ -315,9 +326,7 @@ def test_test_side_omission_blocks_on_any_test_file_edit(
|
|
|
315
326
|
) -> None:
|
|
316
327
|
_write_module(neutral_package_directory, _THREE_PUBLIC_FUNCTIONS)
|
|
317
328
|
test_path = str(neutral_package_directory / "tests" / "test_mod.py")
|
|
318
|
-
all_issues = check_test_file_omits_module_public_function(
|
|
319
|
-
_SUITE_COVERS_ALPHA_BETA, test_path
|
|
320
|
-
)
|
|
329
|
+
all_issues = check_test_file_omits_module_public_function(_SUITE_COVERS_ALPHA_BETA, test_path)
|
|
321
330
|
assert len(all_issues) == 1
|
|
322
331
|
assert "gamma" in all_issues[0]
|
|
323
332
|
|
|
@@ -333,7 +342,5 @@ def test_skips_when_paired_production_module_is_exempt(
|
|
|
333
342
|
"from pkg import alpha, beta, gamma\n\n"
|
|
334
343
|
"def test_all():\n assert alpha() and beta() and gamma()\n"
|
|
335
344
|
)
|
|
336
|
-
all_issues = check_test_file_omits_module_public_function(
|
|
337
|
-
suite_covers_all, beside_test_path
|
|
338
|
-
)
|
|
345
|
+
all_issues = check_test_file_omits_module_public_function(suite_covers_all, beside_test_path)
|
|
339
346
|
assert all_issues == []
|
|
@@ -17,8 +17,8 @@ import pathlib
|
|
|
17
17
|
import sys
|
|
18
18
|
|
|
19
19
|
_PACKAGE_ROOT = pathlib.Path(__file__).resolve().parents[2]
|
|
20
|
-
|
|
21
|
-
_PACKAGE_ROOT
|
|
20
|
+
_ARCHIVED_CONVERGE_MJS = (
|
|
21
|
+
_PACKAGE_ROOT.parents[1] / "skill-archive" / "autoconverge" / "workflow" / "converge.mjs"
|
|
22
22
|
)
|
|
23
23
|
|
|
24
24
|
_HOOK_DIRECTORY = pathlib.Path(__file__).parent
|
|
@@ -750,8 +750,8 @@ def should_still_flag_real_dispatch_alongside_task_substring_identifier() -> Non
|
|
|
750
750
|
assert "spawnVerifierAgent" in issues[0]
|
|
751
751
|
|
|
752
752
|
|
|
753
|
-
def
|
|
754
|
-
converge_source =
|
|
753
|
+
def should_not_flag_archived_converge_regression_fixture() -> None:
|
|
754
|
+
converge_source = _ARCHIVED_CONVERGE_MJS.read_text(encoding="utf-8")
|
|
755
755
|
issues = check_js_resume_task_enumeration_coverage(
|
|
756
756
|
converge_source, "skills/autoconverge/workflow/converge.mjs"
|
|
757
757
|
)
|