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,159 @@
|
|
|
1
|
+
"""Prove retirement and local enforcement through an actual disposable install."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
from test_pre_commit import ALL_RULE_FIXTURES, _git, _stage
|
|
14
|
+
from test_pre_commit import repository_root as repository_root
|
|
15
|
+
|
|
16
|
+
ALL_RETIRED_HOOK_PATHS = (
|
|
17
|
+
"blocking/code_rules_enforcer.py",
|
|
18
|
+
"blocking/tdd_enforcer.py",
|
|
19
|
+
"blocking/windows_rmtree_blocker.py",
|
|
20
|
+
"blocking/state_description_blocker.py",
|
|
21
|
+
"blocking/subprocess_budget_completeness.py",
|
|
22
|
+
"blocking/hook_prose_detector_consistency.py",
|
|
23
|
+
"blocking/workflow_substitution_slot_blocker.py",
|
|
24
|
+
)
|
|
25
|
+
PACKAGE_ROOT = Path(__file__).resolve().parents[2]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _legacy_settings_bytes(managed_root: Path, foreign_command: str) -> bytes:
|
|
29
|
+
original_settings = {
|
|
30
|
+
"permissions": {"allow": ["Read"]},
|
|
31
|
+
"hooks": {"PreToolUse": [{
|
|
32
|
+
"matcher": "Write|Edit|MultiEdit|apply_patch",
|
|
33
|
+
"hooks": [
|
|
34
|
+
{"type": "command", "command": foreign_command, "timeout": 17},
|
|
35
|
+
*({
|
|
36
|
+
"type": "command",
|
|
37
|
+
"command": f"python {managed_root.as_posix()}/hooks/{each_path}",
|
|
38
|
+
"timeout": 10,
|
|
39
|
+
} for each_path in ALL_RETIRED_HOOK_PATHS),
|
|
40
|
+
],
|
|
41
|
+
}]},
|
|
42
|
+
}
|
|
43
|
+
return json.dumps(original_settings, indent=2).encode("utf-8")
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _install_environment(home: Path) -> dict[str, str]:
|
|
47
|
+
environment = {
|
|
48
|
+
each_name: each_value for each_name, each_value in os.environ.items()
|
|
49
|
+
if not each_name.startswith("GIT_")
|
|
50
|
+
}
|
|
51
|
+
environment.update({
|
|
52
|
+
"HOME": str(home),
|
|
53
|
+
"USERPROFILE": str(home),
|
|
54
|
+
"CLAUDE_CONFIG_DIR": str(home / ".claude"),
|
|
55
|
+
"CODEX_HOME": str(home / ".codex"),
|
|
56
|
+
"GIT_CONFIG_GLOBAL": str(home / "gitconfig"),
|
|
57
|
+
"GIT_CONFIG_NOSYSTEM": "1",
|
|
58
|
+
})
|
|
59
|
+
return environment
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _install_twice(home: Path, settings_path: Path) -> bytes:
|
|
63
|
+
all_settings_bytes: list[bytes] = []
|
|
64
|
+
for each_run in range(2):
|
|
65
|
+
completed = subprocess.run(
|
|
66
|
+
["node", str(PACKAGE_ROOT / "bin/install.mjs"), "--only", "core"],
|
|
67
|
+
cwd=PACKAGE_ROOT, env=_install_environment(home),
|
|
68
|
+
capture_output=True, text=True, check=False, timeout=180,
|
|
69
|
+
)
|
|
70
|
+
(home / f"install-{each_run}.log").write_text(
|
|
71
|
+
completed.stdout + completed.stderr, encoding="utf-8"
|
|
72
|
+
)
|
|
73
|
+
assert completed.returncode == 0, completed.stdout + completed.stderr
|
|
74
|
+
all_settings_bytes.append(settings_path.read_bytes())
|
|
75
|
+
assert all_settings_bytes[0] == all_settings_bytes[1]
|
|
76
|
+
return all_settings_bytes[-1]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _assert_retired_registrations(managed_root: Path, settings_bytes: bytes, foreign_command: str) -> None:
|
|
80
|
+
settings = json.loads(settings_bytes)
|
|
81
|
+
all_commands = [
|
|
82
|
+
each_hook["command"]
|
|
83
|
+
for each_groups in settings["hooks"].values()
|
|
84
|
+
for each_group in each_groups
|
|
85
|
+
for each_hook in each_group["hooks"]
|
|
86
|
+
]
|
|
87
|
+
assert all_commands.count(foreign_command) == 1
|
|
88
|
+
assert "Read" in settings["permissions"]["allow"]
|
|
89
|
+
for each_relative_path in ALL_RETIRED_HOOK_PATHS:
|
|
90
|
+
assert not any(each_relative_path in each_command for each_command in all_commands)
|
|
91
|
+
source = PACKAGE_ROOT / "hooks" / each_relative_path
|
|
92
|
+
installed = managed_root / "hooks" / each_relative_path
|
|
93
|
+
assert hashlib.sha256(installed.read_bytes()).digest() == hashlib.sha256(source.read_bytes()).digest()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
@pytest.fixture(scope="module")
|
|
97
|
+
def managed_installation(tmp_path_factory: pytest.TempPathFactory) -> Path:
|
|
98
|
+
"""Install twice into a private home while retaining exact prior settings."""
|
|
99
|
+
home = tmp_path_factory.mktemp("installed-policy")
|
|
100
|
+
managed_root = home / ".claude"
|
|
101
|
+
managed_root.mkdir()
|
|
102
|
+
foreign_command = "python /unmanaged/fixture_notice.py"
|
|
103
|
+
original_bytes = _legacy_settings_bytes(managed_root, foreign_command)
|
|
104
|
+
settings_path = managed_root / "settings.json"
|
|
105
|
+
settings_path.write_bytes(original_bytes)
|
|
106
|
+
backup_directory = home / "before-cutover"
|
|
107
|
+
backup_directory.mkdir()
|
|
108
|
+
(backup_directory / "settings.json").write_bytes(original_bytes)
|
|
109
|
+
installed_settings = _install_twice(home, settings_path)
|
|
110
|
+
_assert_retired_registrations(managed_root, installed_settings, foreign_command)
|
|
111
|
+
assert (backup_directory / "settings.json").read_bytes() == original_bytes
|
|
112
|
+
hook_path = _git(home, "config", "--file", str(home / "gitconfig"), "--get", "core.hooksPath")
|
|
113
|
+
assert Path(hook_path).resolve() == (managed_root / "hooks/git-hooks").resolve()
|
|
114
|
+
return managed_root
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@pytest.mark.parametrize(("rule_id", "relative_path", "content"), ALL_RULE_FIXTURES)
|
|
118
|
+
def test_installed_native_commit_catches_each_retired_file_rule(
|
|
119
|
+
managed_installation: Path,
|
|
120
|
+
repository_root: Path,
|
|
121
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
122
|
+
rule_id: str,
|
|
123
|
+
relative_path: str,
|
|
124
|
+
content: str,
|
|
125
|
+
) -> None:
|
|
126
|
+
_git(repository_root, "config", "core.hooksPath", str(managed_installation / "hooks/git-hooks"))
|
|
127
|
+
monkeypatch.delenv("CLAUDE_CONFIG_DIR", raising=False)
|
|
128
|
+
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(repository_root / "missing-old-gate.py"))
|
|
129
|
+
before = _git(repository_root, "rev-parse", "HEAD")
|
|
130
|
+
_stage(repository_root, relative_path, content)
|
|
131
|
+
completed = subprocess.run(
|
|
132
|
+
["git", "commit", "-m", "invalid fixture"],
|
|
133
|
+
cwd=repository_root, capture_output=True, text=True, check=False, timeout=240,
|
|
134
|
+
)
|
|
135
|
+
assert completed.returncode != 0
|
|
136
|
+
assert rule_id in completed.stdout + completed.stderr
|
|
137
|
+
assert _git(repository_root, "rev-parse", "HEAD") == before
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_installed_linter_and_native_commit_allow_valid_changes(
|
|
141
|
+
managed_installation: Path,
|
|
142
|
+
repository_root: Path,
|
|
143
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
144
|
+
) -> None:
|
|
145
|
+
_git(repository_root, "config", "core.hooksPath", str(managed_installation / "hooks/git-hooks"))
|
|
146
|
+
monkeypatch.delenv("CLAUDE_CONFIG_DIR", raising=False)
|
|
147
|
+
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(repository_root / "missing-old-gate.py"))
|
|
148
|
+
_stage(repository_root, "docs/guide.md", "# Guide\n\nThe API uses port 8080.\n")
|
|
149
|
+
completed = subprocess.run(
|
|
150
|
+
[sys.executable, str(managed_installation / "scripts/cde_lint.py"), "--staged", "--format", "json"],
|
|
151
|
+
cwd=repository_root, capture_output=True, text=True, check=False, timeout=240,
|
|
152
|
+
)
|
|
153
|
+
assert completed.returncode == 0, completed.stdout + completed.stderr
|
|
154
|
+
report = json.loads(completed.stdout)
|
|
155
|
+
assert report["failed_rules"] == []
|
|
156
|
+
assert "terminology-sweep" in report["executed_rules"]
|
|
157
|
+
before = _git(repository_root, "rev-parse", "HEAD")
|
|
158
|
+
_git(repository_root, "commit", "-m", "valid installed fixture")
|
|
159
|
+
assert _git(repository_root, "rev-parse", "HEAD") != before
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""Exercise the native Git commit boundary in disposable repositories."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import shlex
|
|
7
|
+
import subprocess
|
|
8
|
+
import sys
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pre_commit
|
|
12
|
+
import pytest
|
|
13
|
+
from test_pre_commit import _git, _stage
|
|
14
|
+
from test_pre_commit import repository_root as repository_root
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@pytest.fixture()
|
|
18
|
+
def installed_native_hook(
|
|
19
|
+
repository_root: Path,
|
|
20
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
21
|
+
) -> Path:
|
|
22
|
+
"""Install a native fixture shim pointing to the actual pre-commit owner."""
|
|
23
|
+
hook_directory = repository_root / ".git" / "fixture-native-hooks"
|
|
24
|
+
hook_directory.mkdir()
|
|
25
|
+
hook_path = hook_directory / "pre-commit"
|
|
26
|
+
command = " ".join(
|
|
27
|
+
shlex.quote(each_argument)
|
|
28
|
+
for each_argument in (sys.executable, str(Path(pre_commit.__file__).resolve()))
|
|
29
|
+
)
|
|
30
|
+
hook_path.write_text(f"#!/bin/sh\nexec {command}\n", encoding="utf-8")
|
|
31
|
+
hook_path.chmod(0o755)
|
|
32
|
+
_git(repository_root, "config", "core.hooksPath", str(hook_directory))
|
|
33
|
+
monkeypatch.setenv("CODE_RULES_GATE_PATH", str(repository_root / "missing-legacy-gate.py"))
|
|
34
|
+
return repository_root
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_valid_commit_passes_the_installed_native_hook(installed_native_hook: Path) -> None:
|
|
38
|
+
before = _git(installed_native_hook, "rev-parse", "HEAD")
|
|
39
|
+
_stage(installed_native_hook, "docs/config.md", "The API uses port 8080.\n")
|
|
40
|
+
_git(installed_native_hook, "commit", "-m", "valid fixture")
|
|
41
|
+
assert _git(installed_native_hook, "rev-parse", "HEAD") != before
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _assert_write_proceeds(repository_root: Path, relative_path: str, content: str) -> None:
|
|
45
|
+
"""Exercise the actual dispatcher before attempting the staged commit."""
|
|
46
|
+
hooks_root = Path(pre_commit.__file__).resolve().parent.parent
|
|
47
|
+
payload = {
|
|
48
|
+
"tool_name": "Write",
|
|
49
|
+
"cwd": str(repository_root),
|
|
50
|
+
"tool_input": {
|
|
51
|
+
"file_path": str(repository_root / relative_path),
|
|
52
|
+
"content": content,
|
|
53
|
+
},
|
|
54
|
+
}
|
|
55
|
+
edit_result = subprocess.run(
|
|
56
|
+
[sys.executable, str(hooks_root / "blocking/pre_tool_use_dispatcher.py")],
|
|
57
|
+
cwd=repository_root,
|
|
58
|
+
input=json.dumps(payload),
|
|
59
|
+
capture_output=True,
|
|
60
|
+
text=True,
|
|
61
|
+
check=False,
|
|
62
|
+
timeout=90,
|
|
63
|
+
)
|
|
64
|
+
assert edit_result.returncode == 0, edit_result.stderr
|
|
65
|
+
edit_output = json.loads(edit_result.stdout or "{}")
|
|
66
|
+
assert edit_output.get("hookSpecificOutput", {}).get("permissionDecision") != "deny"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_retired_write_check_allows_edit_but_local_commit_catches_violation(
|
|
70
|
+
installed_native_hook: Path,
|
|
71
|
+
) -> None:
|
|
72
|
+
relative_path = "docs/config.md"
|
|
73
|
+
content = "Previously set via env var.\n"
|
|
74
|
+
_assert_write_proceeds(installed_native_hook, relative_path, content)
|
|
75
|
+
before = _git(installed_native_hook, "rev-parse", "HEAD")
|
|
76
|
+
_stage(installed_native_hook, relative_path, content)
|
|
77
|
+
commit_result = subprocess.run(
|
|
78
|
+
["git", "commit", "-m", "invalid fixture"],
|
|
79
|
+
cwd=installed_native_hook,
|
|
80
|
+
capture_output=True,
|
|
81
|
+
text=True,
|
|
82
|
+
check=False,
|
|
83
|
+
timeout=240,
|
|
84
|
+
)
|
|
85
|
+
assert commit_result.returncode != 0
|
|
86
|
+
assert "state-description" in commit_result.stdout + commit_result.stderr
|
|
87
|
+
assert _git(installed_native_hook, "rev-parse", "HEAD") == before
|
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
3
7
|
from pathlib import Path
|
|
4
8
|
|
|
5
9
|
import pre_commit
|
|
6
10
|
import pytest
|
|
7
11
|
|
|
12
|
+
from git_hooks_constants.staged_policy_lint import POLICY_LINT_TIMEOUT_SECONDS
|
|
13
|
+
|
|
8
14
|
|
|
9
15
|
def make_gate_script_returning(exit_code: int, target_path: Path) -> Path:
|
|
10
16
|
target_path.write_text(
|
|
@@ -170,3 +176,243 @@ def test_invoke_gate_uses_resolved_path(
|
|
|
170
176
|
assert exit_code == 0
|
|
171
177
|
executed_path = recorded_path_file.read_text(encoding="utf-8")
|
|
172
178
|
assert executed_path == str(resolved_path)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
RMTREE_FIXTURE_KEYWORD = "ignore_errors"
|
|
182
|
+
ALL_RULE_FIXTURES = (
|
|
183
|
+
(
|
|
184
|
+
"code-rules",
|
|
185
|
+
"src/worker.py",
|
|
186
|
+
"from typing import Any\n\ndef worker() -> Any:\n return None\n",
|
|
187
|
+
),
|
|
188
|
+
(
|
|
189
|
+
"test-pairing",
|
|
190
|
+
"src/feature.py",
|
|
191
|
+
"def work() -> None:\n pass\n",
|
|
192
|
+
),
|
|
193
|
+
(
|
|
194
|
+
"rmtree-safety",
|
|
195
|
+
"src/cleanup.py",
|
|
196
|
+
f"import shutil\nshutil.rmtree('fixture', {RMTREE_FIXTURE_KEYWORD}=True)\n",
|
|
197
|
+
),
|
|
198
|
+
(
|
|
199
|
+
"state-description",
|
|
200
|
+
"docs/config.md",
|
|
201
|
+
"# Config\n\nPreviously set via env var.\n",
|
|
202
|
+
),
|
|
203
|
+
(
|
|
204
|
+
"subprocess-budget",
|
|
205
|
+
"src/timing.py",
|
|
206
|
+
"import subprocess\n"
|
|
207
|
+
"PYTHON_FORMAT_TIMEOUT_SECONDS = 12\n"
|
|
208
|
+
"GIT_CHECK_TIMEOUT_SECONDS = 5\n"
|
|
209
|
+
"def worst_case_python_format_seconds() -> int:\n"
|
|
210
|
+
" fix_phase_seconds = PYTHON_FORMAT_TIMEOUT_SECONDS\n"
|
|
211
|
+
" format_phase_seconds = PYTHON_FORMAT_TIMEOUT_SECONDS\n"
|
|
212
|
+
" return fix_phase_seconds + format_phase_seconds\n"
|
|
213
|
+
"def is_untracked_in_git(file_path: str) -> bool:\n"
|
|
214
|
+
" git_check = subprocess.run(['git', 'ls-files', file_path], "
|
|
215
|
+
"timeout=GIT_CHECK_TIMEOUT_SECONDS)\n"
|
|
216
|
+
" return git_check.returncode != 0\n"
|
|
217
|
+
"def run_format(file_path: str) -> None:\n"
|
|
218
|
+
" subprocess.run(['ruff', 'format', file_path], "
|
|
219
|
+
"timeout=PYTHON_FORMAT_TIMEOUT_SECONDS)\n"
|
|
220
|
+
"def main(file_path: str) -> None:\n"
|
|
221
|
+
" if is_untracked_in_git(file_path):\n"
|
|
222
|
+
" return\n"
|
|
223
|
+
" run_format(file_path)\n",
|
|
224
|
+
),
|
|
225
|
+
(
|
|
226
|
+
"hook-prose-consistency",
|
|
227
|
+
"hooks/hooks_constants/probe_constants.py",
|
|
228
|
+
'CORRECTIVE_MESSAGE = "appears as a path or output-key segment"\n',
|
|
229
|
+
),
|
|
230
|
+
(
|
|
231
|
+
"workflow-substitution",
|
|
232
|
+
"scripts/sample.workflow.js",
|
|
233
|
+
"For EACH candidate i, build a bible dir cand_i per the contract.\n"
|
|
234
|
+
" & ${PY} compose.py --out ${args.work_dir}\\\\cand_i\\\\sample.png "
|
|
235
|
+
"--glow <candidate glow_hex>\n"
|
|
236
|
+
'Return: {key: "cand_i", name, sample_png}\n',
|
|
237
|
+
),
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _git(repository_root: Path, *all_arguments: str) -> str:
|
|
242
|
+
completed = subprocess.run(
|
|
243
|
+
["git", *all_arguments],
|
|
244
|
+
cwd=repository_root,
|
|
245
|
+
check=True,
|
|
246
|
+
capture_output=True,
|
|
247
|
+
text=True,
|
|
248
|
+
timeout=30,
|
|
249
|
+
)
|
|
250
|
+
return completed.stdout.strip()
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
@pytest.fixture()
|
|
254
|
+
def repository_root(
|
|
255
|
+
tmp_path_factory: pytest.TempPathFactory, monkeypatch: pytest.MonkeyPatch,
|
|
256
|
+
) -> Path:
|
|
257
|
+
"""Create a repository with private Git configuration and no inherited hooks."""
|
|
258
|
+
tmp_path = tmp_path_factory.mktemp("policy-fixture")
|
|
259
|
+
home_directory = tmp_path / "home"
|
|
260
|
+
home_directory.mkdir()
|
|
261
|
+
monkeypatch.setenv("HOME", str(home_directory))
|
|
262
|
+
monkeypatch.setenv("USERPROFILE", str(home_directory))
|
|
263
|
+
monkeypatch.setenv("GIT_CONFIG_GLOBAL", os.devnull)
|
|
264
|
+
monkeypatch.setenv("GIT_CONFIG_NOSYSTEM", "1")
|
|
265
|
+
monkeypatch.delenv("GIT_INDEX_FILE", raising=False)
|
|
266
|
+
monkeypatch.delenv("GIT_DIR", raising=False)
|
|
267
|
+
monkeypatch.delenv("GIT_WORK_TREE", raising=False)
|
|
268
|
+
root = tmp_path / "repository"
|
|
269
|
+
root.mkdir()
|
|
270
|
+
_git(root, "init")
|
|
271
|
+
_git(root, "config", "core.hooksPath", str(tmp_path / "disabled-fixture-hooks"))
|
|
272
|
+
_git(root, "config", "user.name", "Fixture")
|
|
273
|
+
_git(root, "config", "user.email", "fixture@example.com")
|
|
274
|
+
_git(root, "commit", "--allow-empty", "-m", "fixture base")
|
|
275
|
+
monkeypatch.chdir(root)
|
|
276
|
+
return root
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _stage(repository_root: Path, relative_path: str, content: str) -> Path:
|
|
280
|
+
file_path = repository_root / relative_path
|
|
281
|
+
file_path.parent.mkdir(parents=True, exist_ok=True)
|
|
282
|
+
file_path.write_text(content, encoding="utf-8")
|
|
283
|
+
_git(repository_root, "add", "--", relative_path)
|
|
284
|
+
return file_path
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def _lint_report(repository_root: Path) -> tuple[int, dict[str, object]]:
|
|
288
|
+
completed = subprocess.run(
|
|
289
|
+
[
|
|
290
|
+
sys.executable,
|
|
291
|
+
str(pre_commit.resolve_policy_lint_script_path()),
|
|
292
|
+
"--staged",
|
|
293
|
+
"--format",
|
|
294
|
+
"json",
|
|
295
|
+
],
|
|
296
|
+
cwd=repository_root,
|
|
297
|
+
check=False,
|
|
298
|
+
capture_output=True,
|
|
299
|
+
text=True,
|
|
300
|
+
timeout=POLICY_LINT_TIMEOUT_SECONDS,
|
|
301
|
+
)
|
|
302
|
+
assert completed.stderr == "", completed.stderr
|
|
303
|
+
return completed.returncode, json.loads(completed.stdout)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
@pytest.mark.parametrize(("rule_id", "relative_path", "content"), ALL_RULE_FIXTURES)
|
|
307
|
+
def test_real_staged_linter_catches_each_replaced_rule(
|
|
308
|
+
repository_root: Path,
|
|
309
|
+
rule_id: str,
|
|
310
|
+
relative_path: str,
|
|
311
|
+
content: str,
|
|
312
|
+
) -> None:
|
|
313
|
+
_stage(repository_root, relative_path, content)
|
|
314
|
+
exit_code, report = _lint_report(repository_root)
|
|
315
|
+
assert exit_code == 1, report
|
|
316
|
+
assert report["failed_rules"] == [], report
|
|
317
|
+
all_diagnostics = report["diagnostics"]
|
|
318
|
+
assert isinstance(all_diagnostics, list)
|
|
319
|
+
assert rule_id in {each["rule_id"] for each in all_diagnostics}, report
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _stage_valid_pair(repository_root: Path, relative_path: str) -> None:
|
|
323
|
+
source_path = Path(relative_path)
|
|
324
|
+
clean_content = {
|
|
325
|
+
".py": (
|
|
326
|
+
"def is_ready() -> bool:\n"
|
|
327
|
+
' """Report readiness.\n\n'
|
|
328
|
+
" Returns:\n True for the fixture.\n"
|
|
329
|
+
' """\n'
|
|
330
|
+
" return True\n"
|
|
331
|
+
),
|
|
332
|
+
".js": "export const isEnabled = true;\n",
|
|
333
|
+
".md": "# Config\n\nThe API uses port 8080.\n",
|
|
334
|
+
}[source_path.suffix]
|
|
335
|
+
_stage(repository_root, relative_path, clean_content)
|
|
336
|
+
paired_name = (
|
|
337
|
+
f"test_{source_path.stem}.py"
|
|
338
|
+
if source_path.suffix == ".py"
|
|
339
|
+
else f"{source_path.stem}.test{source_path.suffix}"
|
|
340
|
+
)
|
|
341
|
+
paired_content = (
|
|
342
|
+
f"from {source_path.stem} import is_ready\n\n"
|
|
343
|
+
"def test_is_ready() -> None:\n assert is_ready()\n"
|
|
344
|
+
if source_path.suffix == ".py"
|
|
345
|
+
else ""
|
|
346
|
+
)
|
|
347
|
+
_stage(repository_root, (source_path.parent / paired_name).as_posix(), paired_content)
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
@pytest.mark.parametrize(("rule_id", "relative_path", "content"), ALL_RULE_FIXTURES)
|
|
351
|
+
def test_valid_paired_content_passes_the_real_local_commit_linter(
|
|
352
|
+
repository_root: Path,
|
|
353
|
+
rule_id: str,
|
|
354
|
+
relative_path: str,
|
|
355
|
+
content: str,
|
|
356
|
+
) -> None:
|
|
357
|
+
_stage_valid_pair(repository_root, relative_path)
|
|
358
|
+
assert pre_commit.run_staged_policy_lint() == 0
|
|
359
|
+
|
|
360
|
+
|
|
361
|
+
def test_unstaged_fix_does_not_hide_staged_violation(repository_root: Path) -> None:
|
|
362
|
+
target = _stage(repository_root, "docs/config.md", "Previously set via env var.\n")
|
|
363
|
+
target.write_text("The API uses port 8080.\n", encoding="utf-8")
|
|
364
|
+
assert pre_commit.run_staged_policy_lint() == 1
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def test_alternate_git_index_is_checked(
|
|
368
|
+
repository_root: Path,
|
|
369
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
370
|
+
) -> None:
|
|
371
|
+
alternate_index = repository_root / ".git" / "alternate-index"
|
|
372
|
+
monkeypatch.setenv("GIT_INDEX_FILE", str(alternate_index))
|
|
373
|
+
_git(repository_root, "read-tree", "HEAD")
|
|
374
|
+
_stage(repository_root, "docs/config.md", "Previously set via env var.\n")
|
|
375
|
+
assert pre_commit.run_staged_policy_lint() == 1
|
|
376
|
+
monkeypatch.delenv("GIT_INDEX_FILE")
|
|
377
|
+
assert pre_commit.run_staged_policy_lint() == 0
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def test_missing_linter_does_not_allow_commit(
|
|
381
|
+
tmp_path: Path,
|
|
382
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
383
|
+
) -> None:
|
|
384
|
+
monkeypatch.setattr(
|
|
385
|
+
pre_commit, "resolve_policy_lint_script_path", lambda: tmp_path / "missing.py"
|
|
386
|
+
)
|
|
387
|
+
assert pre_commit.main() == 2
|
|
388
|
+
|
|
389
|
+
|
|
390
|
+
@pytest.mark.parametrize("exit_code", (1, 2, 3))
|
|
391
|
+
def test_linter_failure_reaches_git_before_legacy_gate(
|
|
392
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
393
|
+
exit_code: int,
|
|
394
|
+
) -> None:
|
|
395
|
+
monkeypatch.setattr(pre_commit, "run_staged_policy_lint", lambda: exit_code)
|
|
396
|
+
all_gate_calls: list[object] = []
|
|
397
|
+
monkeypatch.setattr(
|
|
398
|
+
pre_commit,
|
|
399
|
+
"resolve_gate_script_path",
|
|
400
|
+
lambda: all_gate_calls.append("unexpected gate"),
|
|
401
|
+
)
|
|
402
|
+
assert pre_commit.main() == exit_code
|
|
403
|
+
assert all_gate_calls == []
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def test_timeout_is_an_infrastructure_failure(
|
|
407
|
+
tmp_path: Path,
|
|
408
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
409
|
+
) -> None:
|
|
410
|
+
script_path = tmp_path / "lint.py"
|
|
411
|
+
script_path.write_text("", encoding="utf-8")
|
|
412
|
+
monkeypatch.setattr(pre_commit, "resolve_policy_lint_script_path", lambda: script_path)
|
|
413
|
+
|
|
414
|
+
def timed_out(*all_arguments: object, **all_options: object) -> object:
|
|
415
|
+
raise subprocess.TimeoutExpired("lint fixture", POLICY_LINT_TIMEOUT_SECONDS)
|
|
416
|
+
|
|
417
|
+
monkeypatch.setattr(pre_commit.subprocess, "run", timed_out)
|
|
418
|
+
assert pre_commit.run_staged_policy_lint() == 2
|
package/hooks/hooks.json
CHANGED
|
@@ -10,12 +10,25 @@ no second copy of either.
|
|
|
10
10
|
from __future__ import annotations
|
|
11
11
|
|
|
12
12
|
from hooks_constants.bash_pre_tool_use_dispatcher_constants import (
|
|
13
|
+
ALL_BASH_AND_POWERSHELL_TOOL_NAMES,
|
|
13
14
|
ALL_BASH_ONLY_TOOL_NAMES,
|
|
14
15
|
BashHostedHookEntry,
|
|
15
16
|
)
|
|
16
17
|
|
|
17
|
-
__all__ = [
|
|
18
|
+
__all__ = [
|
|
19
|
+
"ALL_BASH_POST_TOOL_USE_HOSTED_HOOK_ENTRIES",
|
|
20
|
+
"POST_TOOL_USE_HOOK_EVENT_NAME",
|
|
21
|
+
"HOOK_SPECIFIC_OUTPUT_KEY",
|
|
22
|
+
"ADDITIONAL_CONTEXT_KEY",
|
|
23
|
+
"ADDITIONAL_CONTEXT_JOIN_SEPARATOR",
|
|
24
|
+
]
|
|
18
25
|
|
|
19
26
|
ALL_BASH_POST_TOOL_USE_HOSTED_HOOK_ENTRIES: tuple[BashHostedHookEntry, ...] = (
|
|
20
27
|
BashHostedHookEntry("observability/test_failure_recorder.py", ALL_BASH_ONLY_TOOL_NAMES),
|
|
28
|
+
BashHostedHookEntry("advisory/pr_done_reminder.py", ALL_BASH_AND_POWERSHELL_TOOL_NAMES),
|
|
21
29
|
)
|
|
30
|
+
|
|
31
|
+
POST_TOOL_USE_HOOK_EVENT_NAME: str = "PostToolUse"
|
|
32
|
+
HOOK_SPECIFIC_OUTPUT_KEY: str = "hookSpecificOutput"
|
|
33
|
+
ADDITIONAL_CONTEXT_KEY: str = "additionalContext"
|
|
34
|
+
ADDITIONAL_CONTEXT_JOIN_SEPARATOR: str = "\n\n"
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""Constants for the PR done-checklist context reminder.
|
|
2
|
+
|
|
3
|
+
The reminder is a PostToolUse observer on Bash. It never blocks. After a
|
|
4
|
+
successful ``git push`` or ``gh pr create`` it probes the branch's pull request
|
|
5
|
+
through ``gh`` and adds one loud checklist to the agent's context.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"GIT_PROGRAM_NAME",
|
|
12
|
+
"GIT_PUSH_SUBCOMMAND",
|
|
13
|
+
"GH_PROGRAM_NAME",
|
|
14
|
+
"GH_PR_SUBCOMMAND",
|
|
15
|
+
"GH_PR_CREATE_ACTION",
|
|
16
|
+
"ALL_GIT_OPTIONS_WITH_VALUE",
|
|
17
|
+
"ALL_POWERSHELL_PROGRAM_NAMES",
|
|
18
|
+
"ALL_POWERSHELL_COMMAND_FLAGS",
|
|
19
|
+
"ALL_GH_PR_VIEW_ARGUMENTS",
|
|
20
|
+
"GH_PR_VIEW_TIMEOUT_SECONDS",
|
|
21
|
+
"NO_PULL_REQUEST_MARKER",
|
|
22
|
+
"EXIT_CODE_ERROR_PREFIX",
|
|
23
|
+
"DONE_LABEL_NAME",
|
|
24
|
+
"MERGEABLE_CLEAN_VALUE",
|
|
25
|
+
"MERGEABLE_CONFLICTING_VALUE",
|
|
26
|
+
"MERGEABLE_UNKNOWN_VALUE",
|
|
27
|
+
"ALL_FAILING_CHECK_CONCLUSIONS",
|
|
28
|
+
"ALL_PASSING_CHECK_CONCLUSIONS",
|
|
29
|
+
"CHECK_COMPLETED_STATUS",
|
|
30
|
+
"STATUS_CONTEXT_STATE_KEY",
|
|
31
|
+
"REMINDER_HEADER",
|
|
32
|
+
"REMINDER_FOOTER",
|
|
33
|
+
"REMINDER_LINE_SEPARATOR",
|
|
34
|
+
"ALL_REMINDER_HINTS_BY_MERGEABLE",
|
|
35
|
+
"VERDICT_DONE",
|
|
36
|
+
"VERDICT_NOT_DONE",
|
|
37
|
+
"NO_PULL_REQUEST_REMINDER",
|
|
38
|
+
"RECHECK_COMMAND_TEMPLATE",
|
|
39
|
+
"ADD_LABEL_COMMAND_TEMPLATE",
|
|
40
|
+
]
|
|
41
|
+
|
|
42
|
+
GIT_PROGRAM_NAME: str = "git"
|
|
43
|
+
GIT_PUSH_SUBCOMMAND: str = "push"
|
|
44
|
+
GH_PROGRAM_NAME: str = "gh"
|
|
45
|
+
GH_PR_SUBCOMMAND: str = "pr"
|
|
46
|
+
GH_PR_CREATE_ACTION: str = "create"
|
|
47
|
+
ALL_GIT_OPTIONS_WITH_VALUE: frozenset[str] = frozenset({"-C", "-c"})
|
|
48
|
+
ALL_POWERSHELL_PROGRAM_NAMES: frozenset[str] = frozenset(
|
|
49
|
+
{"pwsh", "pwsh.exe", "powershell", "powershell.exe"}
|
|
50
|
+
)
|
|
51
|
+
ALL_POWERSHELL_COMMAND_FLAGS: frozenset[str] = frozenset({"-command", "-c"})
|
|
52
|
+
|
|
53
|
+
ALL_GH_PR_VIEW_ARGUMENTS: tuple[str, ...] = (
|
|
54
|
+
"gh",
|
|
55
|
+
"pr",
|
|
56
|
+
"view",
|
|
57
|
+
"--json",
|
|
58
|
+
"number,url,isDraft,mergeable,mergeStateStatus,statusCheckRollup,labels",
|
|
59
|
+
)
|
|
60
|
+
GH_PR_VIEW_TIMEOUT_SECONDS: int = 20
|
|
61
|
+
NO_PULL_REQUEST_MARKER: str = "no pull requests found"
|
|
62
|
+
EXIT_CODE_ERROR_PREFIX: str = "Error: Exit code "
|
|
63
|
+
|
|
64
|
+
DONE_LABEL_NAME: str = "done"
|
|
65
|
+
|
|
66
|
+
MERGEABLE_CLEAN_VALUE: str = "MERGEABLE"
|
|
67
|
+
MERGEABLE_CONFLICTING_VALUE: str = "CONFLICTING"
|
|
68
|
+
MERGEABLE_UNKNOWN_VALUE: str = "UNKNOWN"
|
|
69
|
+
|
|
70
|
+
ALL_FAILING_CHECK_CONCLUSIONS: frozenset[str] = frozenset(
|
|
71
|
+
{"FAILURE", "ERROR", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE"}
|
|
72
|
+
)
|
|
73
|
+
ALL_PASSING_CHECK_CONCLUSIONS: frozenset[str] = frozenset({"SUCCESS", "NEUTRAL", "SKIPPED"})
|
|
74
|
+
CHECK_COMPLETED_STATUS: str = "COMPLETED"
|
|
75
|
+
STATUS_CONTEXT_STATE_KEY: str = "state"
|
|
76
|
+
|
|
77
|
+
REMINDER_HEADER: str = "=== PR DONE CHECKLIST (context reminder, never a block) ==="
|
|
78
|
+
REMINDER_FOOTER: str = "A PR is done only when every line above is clean."
|
|
79
|
+
REMINDER_LINE_SEPARATOR: str = "\n"
|
|
80
|
+
ALL_REMINDER_HINTS_BY_MERGEABLE: dict[str, str] = {
|
|
81
|
+
MERGEABLE_CLEAN_VALUE: "no conflicts with the base branch",
|
|
82
|
+
MERGEABLE_CONFLICTING_VALUE: (
|
|
83
|
+
"CONFLICTS with the base branch. Merge origin/main into this branch, "
|
|
84
|
+
"resolve every conflict, run the tests, push again."
|
|
85
|
+
),
|
|
86
|
+
MERGEABLE_UNKNOWN_VALUE: "GitHub is still computing. Re-run the re-check command in a minute.",
|
|
87
|
+
}
|
|
88
|
+
VERDICT_DONE: str = "DONE. Add the label now:"
|
|
89
|
+
VERDICT_NOT_DONE: str = "NOT DONE. Do not add the done label yet."
|
|
90
|
+
NO_PULL_REQUEST_REMINDER: str = (
|
|
91
|
+
"=== PR DONE CHECKLIST (context reminder, never a block) ===\n"
|
|
92
|
+
"No open pull request found for this branch. Open a draft PR with gh pr create --draft.\n"
|
|
93
|
+
"A branch is done only when its PR has clean CI, no merge conflicts, and the done label."
|
|
94
|
+
)
|
|
95
|
+
RECHECK_COMMAND_TEMPLATE: str = (
|
|
96
|
+
"gh pr view {number} --json mergeable,mergeStateStatus,statusCheckRollup,labels"
|
|
97
|
+
)
|
|
98
|
+
ADD_LABEL_COMMAND_TEMPLATE: str = "gh pr edit {number} --add-label {label}"
|