claude-dev-env 4.2.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/agents/pr-description-writer.md +2 -2
- package/.agents/agents/test_agent_frontmatter.py +2 -2
- package/.agents/agents/test_pr_description_writer_style.py +1 -3
- package/.agents/skills/e-code-review/SKILL.md +12 -12
- package/.agents/skills/e-code-review/preflight-proposal.contract.test.mjs +20 -0
- package/.agents/skills/e-code-review/reference/fix.md +15 -17
- package/.agents/skills/e-code-review/reference/loop.md +35 -39
- package/.agents/skills/e-code-review/reference/mode-contract.test.mjs +2 -1
- package/.agents/skills/e-code-review/reference/preflight-proposal.md +44 -9
- package/.agents/skills/e-code-review/reference/runner-selection.md +13 -14
- package/.agents/skills/issue-tracker/SKILL.md +7 -0
- package/.agents/skills/pull-request/SKILL.md +216 -0
- package/.agents/skills/pull-request/reference/publication-tasks.md +18 -0
- package/.agents/skills/pull-request/scripts/github_pr_command_constants/__init__.py +1 -0
- package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/__init__.py +1 -0
- package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/constants.py +69 -0
- package/.agents/skills/pull-request/scripts/pull_request.py +265 -0
- package/.agents/skills/pull-request/scripts/recover_legacy_author.py +251 -0
- package/.agents/skills/pull-request/scripts/test_github_pr_command_constants.py +58 -0
- package/.agents/skills/pull-request/scripts/test_pull_request.py +396 -0
- package/.agents/skills/pull-request/scripts/test_recover_legacy_author.py +221 -0
- package/.agents/skills/skill-builder/SKILL.md +3 -196
- package/.agents/skills/test-runner/SKILL.md +69 -0
- package/.agents/skills/test-runner/scripts/preflight_checks.py +365 -0
- package/.agents/skills/test-runner/scripts/run_tests.py +316 -0
- package/.agents/skills/test-runner/scripts/test_preflight_checks.py +185 -0
- package/.agents/skills/test-runner/scripts/test_run_tests.py +281 -0
- package/.agents/skills/test-runner/scripts/test_runner_constants/__init__.py +1 -0
- package/.agents/skills/test-runner/scripts/test_runner_constants/config/__init__.py +1 -0
- package/.agents/skills/test-runner/scripts/test_runner_constants/config/constants.py +96 -0
- package/.agents/skills-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +6 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +64 -19
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +59 -0
- package/bin/ever-shipped-skills.mjs +2 -0
- package/bin/ever-shipped-skills.test.mjs +8 -0
- package/bin/install.agents-home.test.mjs +15 -13
- package/bin/install.mjs +37 -1
- package/bin/install.policy-lint.test.mjs +78 -0
- package/bin/install.prune.test.mjs +149 -20
- package/bin/install.pstack-plugin.test.mjs +114 -0
- package/bin/install.test.mjs +61 -7
- package/bin/refresh-pstack-plugin-skills.test.mjs +211 -0
- package/hooks/advisory/pr_done_reminder.py +342 -0
- package/hooks/advisory/test_pr_done_reminder.py +249 -0
- package/hooks/blocking/bash_post_call_dispatcher.py +53 -6
- package/hooks/blocking/code_rules_paired_test.py +22 -15
- package/hooks/blocking/pii_prevention_blocker.py +32 -51
- package/hooks/blocking/test_bash_post_call_dispatcher.py +55 -17
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +1 -4
- package/hooks/blocking/test_code_rules_command_dispatch.py +2 -2
- package/hooks/blocking/test_code_rules_enforcer_paired_test.py +36 -29
- package/hooks/blocking/test_code_rules_js_resume_task_enumeration.py +4 -4
- package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +5 -5
- package/hooks/blocking/test_pii_prevention_blocker.py +37 -89
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +68 -133
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +20 -51
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +18 -18
- package/hooks/gh_post_body_texts.py +195 -0
- package/hooks/git-hooks/git_hooks_constants/staged_policy_lint.py +12 -0
- package/hooks/git-hooks/pre_commit.py +52 -11
- package/hooks/git-hooks/test_installed_policy_lint.py +159 -0
- package/hooks/git-hooks/test_native_policy_commit.py +87 -0
- package/hooks/git-hooks/test_pre_commit.py +246 -0
- package/hooks/hooks.json +1 -16
- package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +14 -2
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -8
- package/hooks/hooks_constants/gh_post_body_texts_constants.py +12 -0
- package/hooks/hooks_constants/pr_done_reminder_constants.py +98 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -43
- package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +17 -7
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +0 -7
- package/hooks/hooks_constants/test_failure_recorder_constants.py +3 -3
- package/hooks/hooks_constants/test_gh_post_body_texts_constants.py +19 -0
- package/hooks/hooks_constants/test_pr_done_reminder_constants.py +40 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +20 -32
- package/hooks/observability/test_failure_recorder.py +1 -1
- package/hooks/pyproject.toml +1 -1
- package/hooks/test_gh_post_body_texts.py +79 -0
- package/package.json +2 -2
- package/rules/durable-post-artifacts.md +32 -20
- package/rules/gh-cli-conventions.md +23 -13
- package/rules/git-workflow.md +21 -8
- package/scripts/check.ps1 +0 -1
- package/{.agents/skills/run-claude-dev-env/driver.mjs → scripts/ci/installer-lifecycle.mjs} +3 -28
- package/scripts/ci/installer-lifecycle.test.mjs +26 -0
- package/scripts/ci/windows-installer-lifecycle.ps1 +1 -11
- package/scripts/dev_env_scripts_constants/shared_tree_constants.py +4 -5
- package/scripts/durable_post_lint.py +206 -0
- package/scripts/durable_post_lint_config/config/constants.py +96 -0
- package/scripts/durable_post_lint_config/config/test_constants.py +25 -0
- package/scripts/policy_lint/adapter_configuration.py +53 -12
- package/scripts/policy_lint/adapter_detectors.py +13 -6
- package/scripts/policy_lint/adapters.py +7 -4
- package/scripts/policy_lint/config/constants.py +5 -0
- package/scripts/policy_lint/engine.py +48 -4
- package/scripts/policy_lint/model.py +1 -0
- package/scripts/policy_lint/registry.py +1 -1
- package/scripts/policy_lint/selection.py +2 -1
- package/scripts/policy_lint/selection_git.py +2 -0
- package/scripts/refresh_pstack_plugin_skills.mjs +200 -0
- package/scripts/repository_checks/config/constants.py +0 -5
- package/scripts/shared_tree_paths.py +32 -3
- package/scripts/test_durable_post_lint.py +332 -0
- package/scripts/test_shared_tree_paths.py +33 -0
- package/scripts/tests/test_adapter_configuration.py +54 -0
- package/scripts/tests/test_policy_lint_rules_archive.py +120 -0
- package/scripts/tests/test_policy_lint_rules_engine.py +25 -4
- package/scripts/tests/test_policy_lint_rules_installed_loader.py +29 -0
- package/scripts/tests/test_policy_lint_rules_registry.py +227 -13
- package/scripts/tests/test_policy_lint_selection.py +25 -2
- package/scripts/tests/test_policy_lint_selection_native_index.py +30 -0
- package/scripts/tests/test_rules.py +3 -7
- package/.agents/skills/_shared/pr-loop/.claude/CLAUDE.md +0 -1
- package/.agents/skills/_shared/pr-loop/AGENTS.md +0 -1
- package/.agents/skills/_shared/pr-loop/audit-contract.md +0 -5
- package/.agents/skills/_shared/pr-loop/audit-reply-template.md +0 -5
- package/.agents/skills/_shared/pr-loop/code-rules-gate.md +0 -5
- package/.agents/skills/_shared/pr-loop/fix-protocol.md +0 -5
- package/.agents/skills/_shared/pr-loop/gh-payloads.md +0 -5
- package/.agents/skills/_shared/pr-loop/portable-driver.md +0 -149
- package/.agents/skills/_shared/pr-loop/post-audit-thread-contract.md +0 -5
- package/.agents/skills/_shared/pr-loop/precatch-rubric.md +0 -5
- package/.agents/skills/_shared/pr-loop/preflight-proposal.contract.test.mjs +0 -119
- package/.agents/skills/_shared/pr-loop/preflight-proposal.md +0 -5
- package/.agents/skills/_shared/pr-loop/prompts/.claude/CLAUDE.md +0 -1
- package/.agents/skills/_shared/pr-loop/prompts/AGENTS.md +0 -1
- package/.agents/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +0 -324
- package/.agents/skills/_shared/pr-loop/scripts/.claude/CLAUDE.md +0 -1
- package/.agents/skills/_shared/pr-loop/scripts/AGENTS.md +0 -1
- package/.agents/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +0 -29
- package/.agents/skills/_shared/pr-loop/scripts/_cli_utils.py +0 -22
- package/.agents/skills/_shared/pr-loop/scripts/_path_resolver.py +0 -186
- package/.agents/skills/_shared/pr-loop/scripts/_xml_utils.py +0 -20
- package/.agents/skills/_shared/pr-loop/scripts/audit_category_schema.py +0 -355
- package/.agents/skills/_shared/pr-loop/scripts/build_audit_prompt.py +0 -289
- package/.agents/skills/_shared/pr-loop/scripts/build_converge_task_list.py +0 -310
- package/.agents/skills/_shared/pr-loop/scripts/build_fix_prompt.py +0 -292
- package/.agents/skills/_shared/pr-loop/scripts/init_loop_state.py +0 -134
- package/.agents/skills/_shared/pr-loop/scripts/portable_converge_driver.py +0 -1646
- package/.agents/skills/_shared/pr-loop/scripts/preflight_worktree.py +0 -392
- package/.agents/skills/_shared/pr-loop/scripts/select_converge_pacer.py +0 -215
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/.claude/CLAUDE.md +0 -1
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +0 -1
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/__init__.py +0 -0
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +0 -37
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/converge_task_list_constants.py +0 -56
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/handoff_constants.py +0 -45
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/pacer_constants.py +0 -47
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +0 -157
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/portable_driver_constants.py +0 -177
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +0 -70
- package/.agents/skills/_shared/pr-loop/scripts/teardown_worktrees.py +0 -172
- package/.agents/skills/_shared/pr-loop/scripts/test__path_resolver.py +0 -106
- package/.agents/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +0 -94
- package/.agents/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +0 -499
- package/.agents/skills/_shared/pr-loop/scripts/test_build_converge_task_list.py +0 -140
- package/.agents/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +0 -262
- package/.agents/skills/_shared/pr-loop/scripts/test_init_loop_state.py +0 -71
- package/.agents/skills/_shared/pr-loop/scripts/test_portable_converge_driver.py +0 -1119
- package/.agents/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +0 -287
- package/.agents/skills/_shared/pr-loop/scripts/test_select_converge_pacer.py +0 -207
- package/.agents/skills/_shared/pr-loop/scripts/test_teardown_worktrees.py +0 -59
- package/.agents/skills/_shared/pr-loop/scripts/test_write_handoff.py +0 -201
- package/.agents/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +0 -182
- package/.agents/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +0 -206
- package/.agents/skills/_shared/pr-loop/scripts/write_handoff.py +0 -309
- package/.agents/skills/_shared/pr-loop/state-schema.md +0 -5
- package/.agents/skills/_shared/pr-loop/worker-spawn.md +0 -5
- package/.agents/skills/autoconverge/.claude/CLAUDE.md +0 -1
- package/.agents/skills/autoconverge/AGENTS.md +0 -1
- package/.agents/skills/autoconverge/SKILL.md +0 -431
- package/.agents/skills/autoconverge/reference/.claude/CLAUDE.md +0 -1
- package/.agents/skills/autoconverge/reference/AGENTS.md +0 -1
- package/.agents/skills/autoconverge/reference/closing-report.md +0 -93
- package/.agents/skills/autoconverge/reference/convergence-gates.md +0 -31
- package/.agents/skills/autoconverge/reference/convergence.md +0 -219
- package/.agents/skills/autoconverge/reference/copilot-findings.md +0 -51
- package/.agents/skills/autoconverge/reference/gotchas.md +0 -66
- package/.agents/skills/autoconverge/reference/headless-safety.md +0 -44
- package/.agents/skills/autoconverge/reference/multi-pr.md +0 -128
- package/.agents/skills/autoconverge/reference/self-closing-loop.md +0 -84
- package/.agents/skills/autoconverge/reference/stop-conditions.md +0 -127
- package/.agents/skills/autoconverge/test_portable_pacer_gate.py +0 -54
- package/.agents/skills/autoconverge/workflow/.claude/CLAUDE.md +0 -1
- package/.agents/skills/autoconverge/workflow/AGENTS.md +0 -1
- package/.agents/skills/autoconverge/workflow/aggregate_runs.py +0 -371
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/.claude/CLAUDE.md +0 -1
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +0 -1
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/__init__.py +0 -0
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +0 -250
- package/.agents/skills/autoconverge/workflow/converge.clean-audit.test.mjs +0 -665
- package/.agents/skills/autoconverge/workflow/converge.codex-gate.test.mjs +0 -472
- package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +0 -64
- package/.agents/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +0 -1009
- package/.agents/skills/autoconverge/workflow/converge.fix-progress.test.mjs +0 -107
- package/.agents/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +0 -33
- package/.agents/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +0 -15
- package/.agents/skills/autoconverge/workflow/converge.mjs +0 -3132
- package/.agents/skills/autoconverge/workflow/converge.p2-advance.test.mjs +0 -202
- package/.agents/skills/autoconverge/workflow/converge.path-aware.test.mjs +0 -47
- package/.agents/skills/autoconverge/workflow/converge.precatch.test.mjs +0 -152
- package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +0 -116
- package/.agents/skills/autoconverge/workflow/converge_multi.mjs +0 -169
- package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +0 -173
- package/.agents/skills/autoconverge/workflow/convergence_summary.py +0 -110
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +0 -266
- package/.agents/skills/autoconverge/workflow/render_report.py +0 -1024
- package/.agents/skills/autoconverge/workflow/test_aggregate_runs.py +0 -134
- package/.agents/skills/autoconverge/workflow/test_convergence_summary.py +0 -149
- package/.agents/skills/autoconverge/workflow/test_render_report.py +0 -812
- package/.agents/skills/hitl/SKILL.md +0 -45
- package/.agents/skills/pr-cleanup/SKILL.md +0 -140
- package/.agents/skills/pr-cleanup/reference/process-inventory.md +0 -17
- package/.agents/skills/pr-cleanup/reference/task-seeds.md +0 -22
- package/.agents/skills/pr-name-by-capability/SKILL.md +0 -73
- package/.agents/skills/pr-name-by-capability/reference/fetch-commands.md +0 -29
- package/.agents/skills/pr-name-by-capability/reference/offense-examples.md +0 -32
- package/.agents/skills/pr-name-by-capability/reference/report-template.md +0 -29
- package/.agents/skills/pr-name-by-capability/reference/rule-checklist.md +0 -38
- package/.agents/skills/pr-name-by-capability/reference/task-seeds.md +0 -10
- package/.agents/skills/pr-plain-language-cleanup/SKILL.md +0 -93
- package/.agents/skills/pr-refinement/SKILL.md +0 -31
- package/.agents/skills/pr-shared-extraction/SKILL.md +0 -177
- package/.agents/skills/pr-shared-extraction/reference/examples.md +0 -46
- package/.agents/skills/pr-shared-extraction/reference/offense-taxonomy.md +0 -100
- package/.agents/skills/pr-small-cl/SKILL.md +0 -52
- package/.agents/skills/pr-title-description/SKILL.md +0 -96
- package/.agents/skills/prototype/SKILL.md +0 -85
- package/.agents/skills/prototype/reference/honest-limitations.md +0 -23
- package/.agents/skills/prototype/reference/promotion-tasks.md +0 -23
- package/.agents/skills/prototype/scripts/build_sandbox_settings.py +0 -249
- package/.agents/skills/prototype/scripts/conftest.py +0 -15
- package/.agents/skills/prototype/scripts/launch_sandbox.py +0 -205
- package/.agents/skills/prototype/scripts/probe_sandbox_safety.py +0 -311
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/__init__.py +0 -1
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +0 -41
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +0 -23
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +0 -45
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +0 -10
- package/.agents/skills/prototype/scripts/test_build_sandbox_settings.py +0 -275
- package/.agents/skills/prototype/scripts/test_launch_sandbox.py +0 -303
- package/.agents/skills/prototype/scripts/test_probe_sandbox_safety.py +0 -312
- package/.agents/skills/prototype/workflows/promotion.md +0 -25
- package/.agents/skills/prototype/workflows/sandbox.md +0 -35
- package/.agents/skills/rebase/.claude/CLAUDE.md +0 -1
- package/.agents/skills/rebase/AGENTS.md +0 -1
- package/.agents/skills/rebase/SKILL.md +0 -178
- package/.agents/skills/review-router/SKILL.md +0 -51
- package/.agents/skills/review-router/reference/route-policy.json +0 -7
- package/.agents/skills/review-router/reference/task-seeds.md +0 -1
- package/.agents/skills/review-router/scripts/conftest.py +0 -4
- package/.agents/skills/review-router/scripts/review_router.py +0 -50
- package/.agents/skills/review-router/scripts/review_router_cli.py +0 -211
- package/.agents/skills/review-router/scripts/review_router_constants/__init__.py +0 -1
- package/.agents/skills/review-router/scripts/review_router_constants/config/__init__.py +0 -0
- package/.agents/skills/review-router/scripts/review_router_constants/config/constants.py +0 -13
- package/.agents/skills/review-router/scripts/test_review_router.py +0 -23
- package/.agents/skills/review-router/scripts/test_review_router_cli.py +0 -47
- package/.agents/skills/review-tier/SKILL.md +0 -56
- package/.agents/skills/review-tier/reference/task-seeds.md +0 -1
- package/.agents/skills/review-tier/reference/tier-policy.json +0 -9
- package/.agents/skills/review-tier/scripts/conftest.py +0 -4
- package/.agents/skills/review-tier/scripts/review_tier.py +0 -359
- package/.agents/skills/review-tier/scripts/review_tier_constants/__init__.py +0 -1
- package/.agents/skills/review-tier/scripts/review_tier_constants/config/__init__.py +0 -0
- package/.agents/skills/review-tier/scripts/review_tier_constants/config/constants.py +0 -29
- package/.agents/skills/review-tier/scripts/test_generation.py +0 -159
- package/.agents/skills/review-tier/scripts/test_review_tier.py +0 -33
- package/.agents/skills/run-claude-dev-env/SKILL.md +0 -84
- package/.agents/skills/session-log/.claude/CLAUDE.md +0 -1
- package/.agents/skills/session-log/AGENTS.md +0 -1
- package/.agents/skills/session-log/SKILL.md +0 -170
- package/.agents/skills/session-log/templates/frontmatter.md +0 -40
- package/.agents/skills/session-tidy/.claude/CLAUDE.md +0 -1
- package/.agents/skills/session-tidy/AGENTS.md +0 -1
- package/.agents/skills/session-tidy/SKILL.md +0 -191
- package/.agents/skills/skill-builder/references/.claude/CLAUDE.md +0 -1
- package/.agents/skills/skill-builder/references/AGENTS.md +0 -1
- package/.agents/skills/skill-builder/references/delegation-map.md +0 -130
- package/.agents/skills/skill-builder/references/description-field.md +0 -111
- package/.agents/skills/skill-builder/references/deterministic-elements.md +0 -218
- package/.agents/skills/skill-builder/references/progressive-disclosure.md +0 -122
- package/.agents/skills/skill-builder/references/self-audit-checklist.md +0 -126
- package/.agents/skills/skill-builder/references/skill-modularity.md +0 -111
- package/.agents/skills/skill-builder/references/skill-types.md +0 -232
- package/.agents/skills/skill-builder/references/thariq-x-post-skills.json +0 -33
- package/.agents/skills/skill-builder/templates/.claude/CLAUDE.md +0 -1
- package/.agents/skills/skill-builder/templates/AGENTS.md +0 -1
- package/.agents/skills/skill-builder/templates/gap-analysis.md +0 -87
- package/.agents/skills/skill-builder/workflows/.claude/CLAUDE.md +0 -1
- package/.agents/skills/skill-builder/workflows/AGENTS.md +0 -1
- package/.agents/skills/skill-builder/workflows/improve-skill.md +0 -136
- package/.agents/skills/skill-builder/workflows/new-skill.md +0 -179
- package/.agents/skills/skill-builder/workflows/polish-skill.md +0 -139
- package/.agents/skills/source-command-sr-loop/SKILL.md +0 -82
- package/.agents/skills/update/.claude/CLAUDE.md +0 -1
- package/.agents/skills/update/AGENTS.md +0 -1
- package/.agents/skills/update/SKILL.md +0 -190
- package/_shared/pr-loop/AGENTS.md +0 -1
- package/_shared/pr-loop/CLAUDE.md +0 -1
- package/_shared/pr-loop/audit-contract.md +0 -183
- package/_shared/pr-loop/audit-reply-template.md +0 -151
- package/_shared/pr-loop/code-rules-gate.md +0 -65
- package/_shared/pr-loop/fix-protocol.md +0 -60
- package/_shared/pr-loop/gh-payloads.md +0 -75
- package/_shared/pr-loop/post-audit-thread-contract.md +0 -51
- package/_shared/pr-loop/precatch-rubric.md +0 -66
- package/_shared/pr-loop/preflight-proposal.md +0 -56
- package/_shared/pr-loop/scripts/post_audit_thread.py +0 -1242
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/post_audit_thread_constants.py +0 -199
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +0 -1115
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +0 -129
- package/_shared/pr-loop/state-schema.md +0 -45
- package/_shared/pr-loop/worker-spawn.md +0 -189
- package/hooks/_gh_pr_author_swap_utils.py +0 -1225
- package/hooks/blocking/conventional_pr_title_gate.py +0 -443
- package/hooks/blocking/gh_body_arg_blocker.py +0 -176
- package/hooks/blocking/gh_pr_author_enforcer.py +0 -487
- package/hooks/blocking/gh_pr_author_restore.py +0 -105
- package/hooks/blocking/pr_description_writer_gate.py +0 -256
- package/hooks/blocking/test_conventional_pr_title_gate.py +0 -640
- package/hooks/blocking/test_gh_body_arg_blocker.py +0 -441
- package/hooks/blocking/test_gh_pr_author_enforcer.py +0 -1166
- package/hooks/blocking/test_gh_pr_author_restore.py +0 -513
- package/hooks/blocking/test_gh_pr_author_swap_utils.py +0 -910
- package/hooks/blocking/test_pr_description_writer_gate.py +0 -234
- package/hooks/blocking/test_preflight_check.py +0 -343
- package/hooks/blocking/test_test_preflight_check.py +0 -80
- package/hooks/blocking/test_volatile_path_in_post_blocker.py +0 -322
- package/hooks/blocking/volatile_path_in_post_blocker.py +0 -461
- package/hooks/hooks_constants/conventional_pr_title_gate_constants.py +0 -58
- package/hooks/hooks_constants/gh_pr_author_swap_constants.py +0 -76
- package/hooks/hooks_constants/pr_description_writer_gate_constants.py +0 -73
- package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +0 -55
- package/hooks/observability/pr_description_writer_spawn_tracker.py +0 -113
- package/hooks/observability/test_pr_description_writer_spawn_tracker.py +0 -150
- package/hooks/session/gh_pr_author_session_cleanup.py +0 -175
- package/hooks/session/test_gh_pr_author_session_cleanup.py +0 -576
- package/hooks/test__gh_pr_author_swap_utils.py +0 -333
|
@@ -1,16 +1,8 @@
|
|
|
1
|
-
"""
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
text. It also asserts the dispatcher reaches the same decision through its native
|
|
7
|
-
path.
|
|
8
|
-
|
|
9
|
-
The corpus pairs allowing payloads with denying payloads for each hook so the
|
|
10
|
-
equivalence holds across both outcomes. Each payload names an absolute file
|
|
11
|
-
under this repository and carries the repository root as its ``cwd``, so the
|
|
12
|
-
decision belongs to this checkout rather than to the directory pytest runs
|
|
13
|
-
from.
|
|
1
|
+
"""Preserve detector equivalence while retiring its dispatcher registration.
|
|
2
|
+
|
|
3
|
+
The standalone state-description detector remains callable by the policy linter.
|
|
4
|
+
Its function and script agree on valid and invalid inputs; the write dispatcher
|
|
5
|
+
allows these policy findings to reach the staged linter.
|
|
14
6
|
"""
|
|
15
7
|
|
|
16
8
|
from __future__ import annotations
|
|
@@ -195,28 +187,6 @@ def _deny_reason_from_dispatcher(payload_dictionary: dict[str, object]) -> str |
|
|
|
195
187
|
)
|
|
196
188
|
return _deny_reason_from_script_stdout(completed_process.stdout.strip())
|
|
197
189
|
|
|
198
|
-
def _deny_payload_from_dispatcher(payload_dictionary: dict[str, object]) -> dict[str, object]:
|
|
199
|
-
"""Run the dispatcher as a subprocess and return its parsed deny payload.
|
|
200
|
-
|
|
201
|
-
Args:
|
|
202
|
-
payload_dictionary: The payload dict to send as JSON on stdin.
|
|
203
|
-
|
|
204
|
-
Returns:
|
|
205
|
-
The dispatcher's emitted deny JSON parsed into a dict.
|
|
206
|
-
"""
|
|
207
|
-
completed_process = subprocess.run(
|
|
208
|
-
[sys.executable, _DISPATCHER_SCRIPT],
|
|
209
|
-
check=False,
|
|
210
|
-
input=json.dumps(payload_dictionary),
|
|
211
|
-
capture_output=True,
|
|
212
|
-
text=True,
|
|
213
|
-
encoding="utf-8",
|
|
214
|
-
env=_subprocess_environment(),
|
|
215
|
-
)
|
|
216
|
-
parsed_payload = json.loads(completed_process.stdout.strip())
|
|
217
|
-
assert isinstance(parsed_payload, dict)
|
|
218
|
-
return parsed_payload
|
|
219
|
-
|
|
220
190
|
def test_state_description_native_allows_match_script() -> None:
|
|
221
191
|
"""state_description_blocker native allow matches the script's allow."""
|
|
222
192
|
payload_dictionary = _write_payload_dictionary(_MARKDOWN_PATH, _STATE_DESCRIPTION_ALLOW_CONTENT)
|
|
@@ -262,18 +232,17 @@ def test_state_description_native_multi_edit_deny_matches_script() -> None:
|
|
|
262
232
|
assert native_reason == script_reason
|
|
263
233
|
|
|
264
234
|
|
|
265
|
-
def
|
|
266
|
-
"""The
|
|
235
|
+
def test_dispatcher_allows_the_retired_state_description_rule() -> None:
|
|
236
|
+
"""The detector still finds the violation, but the write dispatcher allows it."""
|
|
267
237
|
payload_dictionary = _write_payload_dictionary(_MARKDOWN_PATH, _STATE_DESCRIPTION_DENY_CONTENT)
|
|
268
238
|
native_reason = _evaluate_with_prose_style_opt_in(state_description_blocker.evaluate, payload_dictionary)
|
|
269
239
|
dispatcher_reason = _deny_reason_from_dispatcher(payload_dictionary)
|
|
270
240
|
assert native_reason is not None
|
|
271
|
-
assert dispatcher_reason is
|
|
272
|
-
assert native_reason in dispatcher_reason
|
|
241
|
+
assert dispatcher_reason is None
|
|
273
242
|
|
|
274
243
|
|
|
275
|
-
def
|
|
276
|
-
"""The
|
|
244
|
+
def test_standalone_state_description_retains_additional_context() -> None:
|
|
245
|
+
"""The retained standalone interface still supplies its corrective context."""
|
|
277
246
|
payload_dictionary = _write_payload_dictionary(_MARKDOWN_PATH, _STATE_DESCRIPTION_DENY_CONTENT)
|
|
278
247
|
deny_reason = _evaluate_with_prose_style_opt_in(state_description_blocker.evaluate, payload_dictionary)
|
|
279
248
|
assert deny_reason is not None
|
|
@@ -281,20 +250,20 @@ def test_dispatcher_native_state_description_carries_additional_context() -> Non
|
|
|
281
250
|
standalone_hook_specific = standalone_payload["hookSpecificOutput"]
|
|
282
251
|
assert isinstance(standalone_hook_specific, dict)
|
|
283
252
|
expected_additional_context = standalone_hook_specific["additionalContext"]
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
assert isinstance(
|
|
287
|
-
assert
|
|
253
|
+
script_payload = json.loads(_run_script_subprocess(_STATE_DESCRIPTION_SCRIPT, payload_dictionary))
|
|
254
|
+
script_hook_specific = script_payload.get("hookSpecificOutput", {})
|
|
255
|
+
assert isinstance(script_hook_specific, dict)
|
|
256
|
+
assert script_hook_specific.get("additionalContext") == expected_additional_context
|
|
288
257
|
|
|
289
|
-
def
|
|
290
|
-
"""The
|
|
258
|
+
def test_standalone_state_description_retains_system_message() -> None:
|
|
259
|
+
"""The retained standalone interface still supplies its system message."""
|
|
291
260
|
payload_dictionary = _write_payload_dictionary(_MARKDOWN_PATH, _STATE_DESCRIPTION_DENY_CONTENT)
|
|
292
261
|
deny_reason = _evaluate_with_prose_style_opt_in(state_description_blocker.evaluate, payload_dictionary)
|
|
293
262
|
assert deny_reason is not None
|
|
294
263
|
standalone_payload = state_description_blocker.build_deny_payload(deny_reason)
|
|
295
264
|
expected_system_message = standalone_payload["systemMessage"]
|
|
296
265
|
assert isinstance(expected_system_message, str)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
assert isinstance(
|
|
300
|
-
assert expected_system_message in
|
|
266
|
+
script_payload = json.loads(_run_script_subprocess(_STATE_DESCRIPTION_SCRIPT, payload_dictionary))
|
|
267
|
+
script_system_message = script_payload.get("systemMessage")
|
|
268
|
+
assert isinstance(script_system_message, str)
|
|
269
|
+
assert expected_system_message in script_system_message
|
|
@@ -10,7 +10,6 @@ from __future__ import annotations
|
|
|
10
10
|
from collections.abc import Iterator
|
|
11
11
|
from contextlib import contextmanager
|
|
12
12
|
import json
|
|
13
|
-
import shutil
|
|
14
13
|
import stat
|
|
15
14
|
import subprocess
|
|
16
15
|
import sys
|
|
@@ -21,12 +20,20 @@ import pytest
|
|
|
21
20
|
HOOKS_ROOT = Path(__file__).resolve().parent.parent
|
|
22
21
|
BLOCKING_ROOT = HOOKS_ROOT / "blocking"
|
|
23
22
|
GIT_HOOKS_ROOT = HOOKS_ROOT / "git-hooks"
|
|
24
|
-
PRE_COMMIT_SOURCE_PATH = GIT_HOOKS_ROOT / "pre_commit.py"
|
|
25
|
-
GATE_UTILS_SOURCE_PATH = GIT_HOOKS_ROOT / "gate_utils.py"
|
|
26
|
-
GIT_HOOKS_CONSTANTS_SOURCE_PATH = GIT_HOOKS_ROOT / "git_hooks_constants"
|
|
27
23
|
DISPATCHER_SOURCE_PATH = BLOCKING_ROOT / "bash_pre_tool_use_dispatcher.py"
|
|
28
24
|
GIT_COMMAND_TIMEOUT_SECONDS = 60
|
|
29
|
-
CLEAN_MODULE_SOURCE =
|
|
25
|
+
CLEAN_MODULE_SOURCE = (
|
|
26
|
+
"def add_one(number: int) -> int:\n"
|
|
27
|
+
' """Increase the number.\n\n'
|
|
28
|
+
" Args:\n number: The input number.\n\n"
|
|
29
|
+
" Returns:\n The next number.\n"
|
|
30
|
+
' """\n'
|
|
31
|
+
" return number + 1\n"
|
|
32
|
+
)
|
|
33
|
+
PAIRED_TEST_SOURCE = (
|
|
34
|
+
"from widget import add_one\n\n"
|
|
35
|
+
"def test_add_one() -> None:\n assert add_one(1) == 2\n"
|
|
36
|
+
)
|
|
30
37
|
|
|
31
38
|
|
|
32
39
|
def run_git(
|
|
@@ -69,21 +76,13 @@ def write_gate_script(gate_path: Path, marker_path: Path, exit_code: int) -> Non
|
|
|
69
76
|
|
|
70
77
|
|
|
71
78
|
def install_native_pre_commit(hooks_path: Path) -> None:
|
|
72
|
-
"""
|
|
79
|
+
"""Register the actual package owner with its complete linter dependency tree."""
|
|
73
80
|
hooks_path.mkdir()
|
|
74
|
-
shutil.copyfile(PRE_COMMIT_SOURCE_PATH, hooks_path / "pre_commit.py")
|
|
75
|
-
shutil.copyfile(GATE_UTILS_SOURCE_PATH, hooks_path / "gate_utils.py")
|
|
76
|
-
shutil.copytree(
|
|
77
|
-
GIT_HOOKS_CONSTANTS_SOURCE_PATH,
|
|
78
|
-
hooks_path / "git_hooks_constants",
|
|
79
|
-
)
|
|
80
81
|
pre_commit_path = hooks_path / "pre-commit"
|
|
81
82
|
pre_commit_path.write_text(
|
|
82
83
|
"#!/usr/bin/env python3\n"
|
|
83
84
|
"import sys\n"
|
|
84
|
-
"
|
|
85
|
-
"shim_directory = Path(__file__).resolve().parent\n"
|
|
86
|
-
"sys.path.insert(0, str(shim_directory))\n"
|
|
85
|
+
f"sys.path.insert(0, {str(GIT_HOOKS_ROOT)!r})\n"
|
|
87
86
|
"import pre_commit\n"
|
|
88
87
|
"sys.exit(pre_commit.main())\n",
|
|
89
88
|
encoding="utf-8",
|
|
@@ -133,9 +132,10 @@ def temporary_hooks_path(repository_root: Path, benchmark_hooks_path: Path) -> I
|
|
|
133
132
|
|
|
134
133
|
|
|
135
134
|
def stage_module(repository_root: Path) -> None:
|
|
136
|
-
"""
|
|
135
|
+
"""Stage a valid module and matching test before exercising the legacy gate."""
|
|
137
136
|
(repository_root / "widget.py").write_text(CLEAN_MODULE_SOURCE, encoding="utf-8")
|
|
138
|
-
|
|
137
|
+
(repository_root / "test_widget.py").write_text(PAIRED_TEST_SOURCE, encoding="utf-8")
|
|
138
|
+
run_git(repository_root, "add", "widget.py", "test_widget.py")
|
|
139
139
|
|
|
140
140
|
|
|
141
141
|
def build_bash_payload(repository_root: Path) -> str:
|
|
@@ -216,7 +216,7 @@ def test_temporary_hooks_path_restores_shared_worktree_config_on_timeout(
|
|
|
216
216
|
initialize_repository(tmp_path)
|
|
217
217
|
supported_hooks_path = tmp_path / "supported_hooks"
|
|
218
218
|
supported_hooks_path.mkdir()
|
|
219
|
-
run_git(tmp_path, "config", "
|
|
219
|
+
run_git(tmp_path, "config", "core.hooksPath", str(supported_hooks_path))
|
|
220
220
|
unrelated_worktree = tmp_path.parent / "unrelated_worktree"
|
|
221
221
|
run_git(tmp_path, "worktree", "add", "--detach", str(unrelated_worktree))
|
|
222
222
|
benchmark_hooks_path = tmp_path / "benchmark_hooks"
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"""Extract durable GitHub post body text for privacy checks."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import shlex
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from blocking._gh_body_arg_utils import (
|
|
9
|
+
all_body_flags,
|
|
10
|
+
body_file_flag,
|
|
11
|
+
body_file_short_flag,
|
|
12
|
+
count_extra_tokens_to_skip_for_split_quoted_value,
|
|
13
|
+
get_logical_first_line,
|
|
14
|
+
is_unresolvable_shell_value,
|
|
15
|
+
match_body_file_equals_prefix,
|
|
16
|
+
match_body_flag_equals_prefix,
|
|
17
|
+
strip_surrounding_quotes,
|
|
18
|
+
)
|
|
19
|
+
from hooks_constants.gh_post_body_texts_constants import (
|
|
20
|
+
ALL_GH_POST_SUBCOMMANDS,
|
|
21
|
+
ALL_MCP_BODY_PARAM_NAMES,
|
|
22
|
+
BODY_FILE_ENCODING,
|
|
23
|
+
BODY_FLAG_WITH_VALUE_STEP,
|
|
24
|
+
GH_COMMAND_NAME,
|
|
25
|
+
MINIMUM_POST_SUBCOMMAND_TOKEN_COUNT,
|
|
26
|
+
TOKEN_JOIN_SEPARATOR,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _is_environment_assignment(token: str) -> bool:
|
|
31
|
+
equals_index = token.find("=")
|
|
32
|
+
if equals_index <= 0:
|
|
33
|
+
return False
|
|
34
|
+
name_part = token[:equals_index]
|
|
35
|
+
if not (name_part[0].isalpha() or name_part[0] == "_"):
|
|
36
|
+
return False
|
|
37
|
+
return all(each_character.isalnum() or each_character == "_" for each_character in name_part)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _tokens_name_gh_post_command(all_command_tokens: list[str]) -> bool:
|
|
41
|
+
command_index = 0
|
|
42
|
+
while command_index < len(all_command_tokens) and _is_environment_assignment(
|
|
43
|
+
all_command_tokens[command_index]
|
|
44
|
+
):
|
|
45
|
+
command_index += 1
|
|
46
|
+
if command_index >= len(all_command_tokens):
|
|
47
|
+
return False
|
|
48
|
+
if all_command_tokens[command_index] != GH_COMMAND_NAME:
|
|
49
|
+
return False
|
|
50
|
+
remaining_tokens = all_command_tokens[command_index + 1 :]
|
|
51
|
+
if len(remaining_tokens) < MINIMUM_POST_SUBCOMMAND_TOKEN_COUNT:
|
|
52
|
+
return False
|
|
53
|
+
post_noun, post_verb = remaining_tokens[:MINIMUM_POST_SUBCOMMAND_TOKEN_COUNT]
|
|
54
|
+
return post_verb in ALL_GH_POST_SUBCOMMANDS.get(post_noun, frozenset())
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def _reassemble_split_quoted_value(
|
|
58
|
+
value_token: str,
|
|
59
|
+
all_following_tokens: list[str],
|
|
60
|
+
) -> tuple[str, int]:
|
|
61
|
+
extra_token_count = count_extra_tokens_to_skip_for_split_quoted_value(
|
|
62
|
+
all_following_tokens, value_token
|
|
63
|
+
)
|
|
64
|
+
if not extra_token_count:
|
|
65
|
+
return strip_surrounding_quotes(value_token), 0
|
|
66
|
+
joined_value = (
|
|
67
|
+
value_token
|
|
68
|
+
+ TOKEN_JOIN_SEPARATOR
|
|
69
|
+
+ TOKEN_JOIN_SEPARATOR.join(all_following_tokens[:extra_token_count])
|
|
70
|
+
)
|
|
71
|
+
return strip_surrounding_quotes(joined_value), extra_token_count
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def _extract_flag_value_at(
|
|
75
|
+
all_command_tokens: list[str],
|
|
76
|
+
token_index: int,
|
|
77
|
+
) -> tuple[bool, str, int] | None:
|
|
78
|
+
current_token = all_command_tokens[token_index]
|
|
79
|
+
following_tokens = all_command_tokens[token_index + 1 :]
|
|
80
|
+
body_prefix = match_body_flag_equals_prefix(current_token)
|
|
81
|
+
if body_prefix is not None:
|
|
82
|
+
value, extra_count = _reassemble_split_quoted_value(
|
|
83
|
+
current_token[len(body_prefix) :], following_tokens
|
|
84
|
+
)
|
|
85
|
+
return False, value, token_index + 1 + extra_count
|
|
86
|
+
body_file_prefix = match_body_file_equals_prefix(current_token)
|
|
87
|
+
if body_file_prefix is not None:
|
|
88
|
+
value, extra_count = _reassemble_split_quoted_value(
|
|
89
|
+
current_token[len(body_file_prefix) :], following_tokens
|
|
90
|
+
)
|
|
91
|
+
return True, value, token_index + 1 + extra_count
|
|
92
|
+
if current_token in all_body_flags and following_tokens:
|
|
93
|
+
value, extra_count = _reassemble_split_quoted_value(
|
|
94
|
+
following_tokens[0], following_tokens[1:]
|
|
95
|
+
)
|
|
96
|
+
return False, value, token_index + BODY_FLAG_WITH_VALUE_STEP + extra_count
|
|
97
|
+
if current_token in (body_file_flag, body_file_short_flag) and following_tokens:
|
|
98
|
+
value, extra_count = _reassemble_split_quoted_value(
|
|
99
|
+
following_tokens[0], following_tokens[1:]
|
|
100
|
+
)
|
|
101
|
+
return True, value, token_index + BODY_FLAG_WITH_VALUE_STEP + extra_count
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _collect_body_flag_values(
|
|
106
|
+
all_command_tokens: list[str],
|
|
107
|
+
) -> tuple[list[str], list[str]]:
|
|
108
|
+
all_inline_bodies: list[str] = []
|
|
109
|
+
all_body_file_paths: list[str] = []
|
|
110
|
+
token_index = 0
|
|
111
|
+
while token_index < len(all_command_tokens):
|
|
112
|
+
extraction = _extract_flag_value_at(all_command_tokens, token_index)
|
|
113
|
+
if extraction is None:
|
|
114
|
+
token_index += 1
|
|
115
|
+
continue
|
|
116
|
+
is_body_file, value, token_index = extraction
|
|
117
|
+
if is_body_file:
|
|
118
|
+
all_body_file_paths.append(value)
|
|
119
|
+
else:
|
|
120
|
+
all_inline_bodies.append(value)
|
|
121
|
+
return all_inline_bodies, all_body_file_paths
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _read_body_file(
|
|
125
|
+
body_file_path: str,
|
|
126
|
+
working_directory: str | None,
|
|
127
|
+
) -> str | None:
|
|
128
|
+
if is_unresolvable_shell_value(body_file_path):
|
|
129
|
+
return None
|
|
130
|
+
resolved_path = Path(body_file_path)
|
|
131
|
+
if not resolved_path.is_absolute() and working_directory:
|
|
132
|
+
resolved_path = Path(working_directory) / body_file_path
|
|
133
|
+
try:
|
|
134
|
+
return resolved_path.read_text(encoding=BODY_FILE_ENCODING)
|
|
135
|
+
except (OSError, UnicodeError):
|
|
136
|
+
return None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _body_texts_from_tokens(
|
|
140
|
+
all_command_tokens: list[str],
|
|
141
|
+
working_directory: str | None,
|
|
142
|
+
) -> tuple[list[str], str | None]:
|
|
143
|
+
if not _tokens_name_gh_post_command(all_command_tokens):
|
|
144
|
+
return [], None
|
|
145
|
+
all_body_texts, all_body_file_paths = _collect_body_flag_values(all_command_tokens)
|
|
146
|
+
for each_path in all_body_file_paths:
|
|
147
|
+
file_text = _read_body_file(each_path, working_directory)
|
|
148
|
+
if file_text is None:
|
|
149
|
+
return [], (
|
|
150
|
+
"BLOCKED [pii_prevention_blocker]: durable post uses --body-file "
|
|
151
|
+
f"but '{each_path}' could not be read for PII scanning. Use an "
|
|
152
|
+
"absolute path, ensure the file exists, or pass --body text."
|
|
153
|
+
)
|
|
154
|
+
all_body_texts.append(file_text)
|
|
155
|
+
return all_body_texts, None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def extract_gh_post_body_texts_for_privacy_gate(
|
|
159
|
+
command: str,
|
|
160
|
+
working_directory: str | None = None,
|
|
161
|
+
) -> tuple[list[str], str | None]:
|
|
162
|
+
"""Extract GitHub post text and reject unreadable declared body files.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
command: Raw GitHub CLI command.
|
|
166
|
+
working_directory: Base directory for relative body-file paths.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Body texts and no error, or no texts and a rejection message without body text.
|
|
170
|
+
"""
|
|
171
|
+
logical_line = get_logical_first_line(command)
|
|
172
|
+
if not logical_line:
|
|
173
|
+
return [], None
|
|
174
|
+
try:
|
|
175
|
+
all_command_tokens = shlex.split(logical_line, posix=False)
|
|
176
|
+
except ValueError:
|
|
177
|
+
return [], None
|
|
178
|
+
return _body_texts_from_tokens(all_command_tokens, working_directory)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def extract_mcp_body_texts(all_tool_input: dict[str, object]) -> list[str]:
|
|
182
|
+
"""Extract body and comment strings from a GitHub tool request.
|
|
183
|
+
|
|
184
|
+
Args:
|
|
185
|
+
all_tool_input: GitHub tool input mapping.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
Nonempty body and comment values.
|
|
189
|
+
"""
|
|
190
|
+
all_body_texts: list[str] = []
|
|
191
|
+
for each_name in ALL_MCP_BODY_PARAM_NAMES:
|
|
192
|
+
candidate_text = all_tool_input.get(each_name)
|
|
193
|
+
if isinstance(candidate_text, str) and candidate_text:
|
|
194
|
+
all_body_texts.append(candidate_text)
|
|
195
|
+
return all_body_texts
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Constants for the native pre-commit policy-lint step."""
|
|
2
|
+
|
|
3
|
+
POLICY_LINT_PACKAGE_PARENT_INDEX = 2
|
|
4
|
+
POLICY_LINT_SCRIPT_RELATIVE_PATH = "scripts/cde_lint.py"
|
|
5
|
+
POLICY_LINT_STAGED_ARGUMENT = "--staged"
|
|
6
|
+
POLICY_LINT_TIMEOUT_SECONDS = 240
|
|
7
|
+
POLICY_LINT_INFRASTRUCTURE_EXIT_CODE = 2
|
|
8
|
+
POLICY_LINT_UNAVAILABLE_MESSAGE = (
|
|
9
|
+
"cde lint is missing from this hook installation; reinstall claude-dev-env "
|
|
10
|
+
"before committing"
|
|
11
|
+
)
|
|
12
|
+
POLICY_LINT_FAILED_MESSAGE = "cde lint could not complete: {error}"
|
|
@@ -1,17 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""Run staged policy lint, then the existing staged CODE_RULES gate.
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
4
|
+
Git invokes this module through the installed native pre-commit shim. Policy
|
|
5
|
+
lint reads the Git index, including GIT_INDEX_FILE when Git supplies an
|
|
6
|
+
alternate index. The existing gate still covers checks whose replacement
|
|
7
|
+
coverage is unproven.
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
Exit codes:
|
|
12
|
-
0 - staged changes pass the gate (or the gate is not installed locally).
|
|
13
|
-
1 - staged changes introduce one or more blocking violations.
|
|
14
|
-
2 - unexpected invocation failure (e.g., subprocess could not launch).
|
|
9
|
+
A missing linter, launch failure, or timeout is an infrastructure failure.
|
|
10
|
+
Linter diagnostics and failed-rule statuses reach Git.
|
|
15
11
|
"""
|
|
16
12
|
|
|
17
13
|
from __future__ import annotations
|
|
@@ -27,6 +23,43 @@ from git_hooks_constants import (
|
|
|
27
23
|
IMMEDIATE_SCOPE_ARGUMENT,
|
|
28
24
|
INVOKE_GATE_FAILURE_MESSAGE,
|
|
29
25
|
)
|
|
26
|
+
from git_hooks_constants.staged_policy_lint import (
|
|
27
|
+
POLICY_LINT_FAILED_MESSAGE,
|
|
28
|
+
POLICY_LINT_INFRASTRUCTURE_EXIT_CODE,
|
|
29
|
+
POLICY_LINT_PACKAGE_PARENT_INDEX,
|
|
30
|
+
POLICY_LINT_SCRIPT_RELATIVE_PATH,
|
|
31
|
+
POLICY_LINT_STAGED_ARGUMENT,
|
|
32
|
+
POLICY_LINT_TIMEOUT_SECONDS,
|
|
33
|
+
POLICY_LINT_UNAVAILABLE_MESSAGE,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def resolve_policy_lint_script_path() -> Path:
|
|
38
|
+
"""Return the policy-linter entry point in this package or managed home."""
|
|
39
|
+
package_root = Path(__file__).resolve().parents[POLICY_LINT_PACKAGE_PARENT_INDEX]
|
|
40
|
+
return package_root / POLICY_LINT_SCRIPT_RELATIVE_PATH
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def run_staged_policy_lint() -> int:
|
|
44
|
+
"""Run the staged linter with Git's environment and working directory.
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
The linter status, or the infrastructure status when it cannot run.
|
|
48
|
+
"""
|
|
49
|
+
try:
|
|
50
|
+
script_path = resolve_policy_lint_script_path()
|
|
51
|
+
if not script_path.is_file():
|
|
52
|
+
sys.stderr.write(POLICY_LINT_UNAVAILABLE_MESSAGE + "\n")
|
|
53
|
+
return POLICY_LINT_INFRASTRUCTURE_EXIT_CODE
|
|
54
|
+
completion = subprocess.run(
|
|
55
|
+
[sys.executable, str(script_path.resolve(strict=True)), POLICY_LINT_STAGED_ARGUMENT],
|
|
56
|
+
check=False,
|
|
57
|
+
timeout=POLICY_LINT_TIMEOUT_SECONDS,
|
|
58
|
+
)
|
|
59
|
+
except (OSError, subprocess.TimeoutExpired) as launch_error:
|
|
60
|
+
sys.stderr.write(POLICY_LINT_FAILED_MESSAGE.format(error=launch_error) + "\n")
|
|
61
|
+
return POLICY_LINT_INFRASTRUCTURE_EXIT_CODE
|
|
62
|
+
return completion.returncode
|
|
30
63
|
|
|
31
64
|
|
|
32
65
|
def invoke_gate(gate_script_path: Path) -> int:
|
|
@@ -50,6 +83,14 @@ def invoke_gate(gate_script_path: Path) -> int:
|
|
|
50
83
|
|
|
51
84
|
|
|
52
85
|
def main() -> int:
|
|
86
|
+
"""Run local staged policy lint before the existing native commit checks.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
The first failing check's status, or zero when the checks pass.
|
|
90
|
+
"""
|
|
91
|
+
lint_exit_code = run_staged_policy_lint()
|
|
92
|
+
if lint_exit_code != 0:
|
|
93
|
+
return lint_exit_code
|
|
53
94
|
gate_script_not_found_message = GATE_SCRIPT_NOT_FOUND_MESSAGE
|
|
54
95
|
gate_script_path, exact_allowed_path = resolve_gate_script_path()
|
|
55
96
|
if not is_safe_regular_file(gate_script_path, exact_allowed_path):
|
|
@@ -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
|