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,359 +0,0 @@
|
|
|
1
|
-
"""Build canonical pull-request evidence and select a review tier."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
import hashlib
|
|
6
|
-
import json
|
|
7
|
-
import os
|
|
8
|
-
import subprocess
|
|
9
|
-
from collections.abc import Mapping, Sequence
|
|
10
|
-
from pathlib import Path
|
|
11
|
-
|
|
12
|
-
from review_tier_constants.config.constants import (
|
|
13
|
-
ALL_DEPENDENCY_MARKERS,
|
|
14
|
-
ALL_HARD_TRIGGER_MARKERS,
|
|
15
|
-
ALL_PACKAGE_ROOT_NAMES,
|
|
16
|
-
ALL_PUBLIC_API_MARKERS,
|
|
17
|
-
ALL_SOURCE_SUFFIXES,
|
|
18
|
-
ALL_STATUS_ARGUMENTS,
|
|
19
|
-
ALL_STATUS_DOMAINS,
|
|
20
|
-
ALL_TIER_ORDER,
|
|
21
|
-
ALL_UNTRACKED_ARGUMENTS,
|
|
22
|
-
AMBIGUOUS_BASE_REF,
|
|
23
|
-
DEFAULT_BRANCH_FALLBACK,
|
|
24
|
-
GIT_COMMAND_FAILED,
|
|
25
|
-
GIT_HEAD_REF,
|
|
26
|
-
INVALID_BASE_REF,
|
|
27
|
-
JSON_INDENT,
|
|
28
|
-
MALFORMED_TIER_POLICY,
|
|
29
|
-
MAX_AXIS_VALUE,
|
|
30
|
-
MIN_NONEMPTY_RISK,
|
|
31
|
-
PATH_SEPARATOR,
|
|
32
|
-
PLUGIN_DATA_ENVIRONMENT,
|
|
33
|
-
REMOTE_HEAD_REF,
|
|
34
|
-
ROOT_PACKAGE,
|
|
35
|
-
ROUTING_STATE_ROOT_TRACKED,
|
|
36
|
-
STATUS_CODE_LENGTH,
|
|
37
|
-
STATUS_PREFIX_LENGTH,
|
|
38
|
-
UNAPPROVED_TIER_DOWNGRADE,
|
|
39
|
-
UNKNOWN_TIER,
|
|
40
|
-
)
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
def router_state_root(repo_root: str | Path, all_environment: Mapping[str, str] | None = None) -> Path:
|
|
44
|
-
"""Return the canonical router state root for a repository."""
|
|
45
|
-
selected_environment = all_environment or os.environ
|
|
46
|
-
plugin_root = Path(selected_environment.get(PLUGIN_DATA_ENVIRONMENT, Path(repo_root).resolve() / ".claude-plugin-data"))
|
|
47
|
-
return (plugin_root / "review-routing" / "v1").resolve()
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
def router_state_directory(repo_root: str | Path, all_environment: Mapping[str, str] | None = None) -> Path:
|
|
51
|
-
"""Return the repository-specific directory under the router state root."""
|
|
52
|
-
resolved_root = Path(repo_root).resolve()
|
|
53
|
-
worktree_key = hashlib.sha256(str(resolved_root).encode()).hexdigest()
|
|
54
|
-
return router_state_root(resolved_root, all_environment=all_environment) / worktree_key
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def router_state_root_id(state_root: str | Path) -> str:
|
|
58
|
-
"""Return the stable identity digest for a canonical router state root."""
|
|
59
|
-
return hashlib.sha256(str(Path(state_root).resolve()).casefold().encode()).hexdigest()
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
def _is_inside_state_root(repo_root: Path, relative_path: str, state_root: Path) -> bool:
|
|
63
|
-
return (repo_root / relative_path).resolve().is_relative_to(state_root)
|
|
64
|
-
|
|
65
|
-
def _git(root: Path, all_arguments: Sequence[str], is_required: bool) -> bytes:
|
|
66
|
-
command = ["git", *all_arguments]
|
|
67
|
-
completed_process = subprocess.run(command, cwd=root, check=is_required, capture_output=True)
|
|
68
|
-
if completed_process.returncode and is_required:
|
|
69
|
-
raise ValueError(GIT_COMMAND_FAILED)
|
|
70
|
-
return completed_process.stdout
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
def _resolve_base(root: Path, requested_base: str | None) -> tuple[str, str]:
|
|
74
|
-
if requested_base:
|
|
75
|
-
candidate = _git(root, ["rev-parse", "--verify", requested_base], is_required=False).decode().strip()
|
|
76
|
-
if not candidate:
|
|
77
|
-
raise ValueError(INVALID_BASE_REF)
|
|
78
|
-
return requested_base, candidate
|
|
79
|
-
remote_head = _git(root, ["symbolic-ref", "--quiet", REMOTE_HEAD_REF], is_required=False).decode().strip()
|
|
80
|
-
if remote_head:
|
|
81
|
-
candidate = _git(root, ["rev-parse", "--verify", remote_head], is_required=False).decode().strip()
|
|
82
|
-
if candidate:
|
|
83
|
-
return remote_head, candidate
|
|
84
|
-
fallback = _git(root, ["rev-parse", "--verify", DEFAULT_BRANCH_FALLBACK], is_required=False).decode().strip()
|
|
85
|
-
if fallback:
|
|
86
|
-
return DEFAULT_BRANCH_FALLBACK, fallback
|
|
87
|
-
raise ValueError(AMBIGUOUS_BASE_REF)
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
def _status_domains(root: Path, state_root: Path | None = None) -> dict[str, set[str]]:
|
|
91
|
-
all_status_lines = _git(root, list(ALL_STATUS_ARGUMENTS), is_required=True).decode(errors="surrogateescape").split("\0")
|
|
92
|
-
domains = {domain: set() for domain in ALL_STATUS_DOMAINS}
|
|
93
|
-
for each_status_line in all_status_lines:
|
|
94
|
-
if len(each_status_line) < STATUS_PREFIX_LENGTH:
|
|
95
|
-
continue
|
|
96
|
-
path = each_status_line[STATUS_PREFIX_LENGTH:]
|
|
97
|
-
if state_root is not None and _is_inside_state_root(root, path, state_root):
|
|
98
|
-
continue
|
|
99
|
-
if each_status_line[:STATUS_CODE_LENGTH] == "??":
|
|
100
|
-
domains["untracked"].add(path)
|
|
101
|
-
else:
|
|
102
|
-
if each_status_line[0] != " ":
|
|
103
|
-
domains["staged"].add(path)
|
|
104
|
-
if each_status_line[1] != " ":
|
|
105
|
-
domains["unstaged"].add(path)
|
|
106
|
-
return domains
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def _untracked_paths(root: Path, state_root: Path | None = None) -> list[str]:
|
|
110
|
-
untracked_listing = _git(root, ALL_UNTRACKED_ARGUMENTS, is_required=True)
|
|
111
|
-
all_paths = [each_path.decode(errors="surrogateescape") for each_path in untracked_listing.split(b"\0") if each_path]
|
|
112
|
-
return [each_path for each_path in all_paths if state_root is None or not _is_inside_state_root(root, each_path, state_root)]
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def _record_content(root: Path, merge_base: str, relative_path: str) -> tuple[bytes, str]:
|
|
116
|
-
file_path = root / relative_path
|
|
117
|
-
if file_path.is_symlink():
|
|
118
|
-
content = b""
|
|
119
|
-
identity = hashlib.sha256(os.readlink(file_path).encode()).hexdigest()
|
|
120
|
-
elif file_path.is_file():
|
|
121
|
-
content = file_path.read_bytes()
|
|
122
|
-
identity = hashlib.sha256(content).hexdigest()
|
|
123
|
-
else:
|
|
124
|
-
content = _git(root, ["show", f"{merge_base}:{relative_path}"], is_required=True)
|
|
125
|
-
identity = hashlib.sha256(content).hexdigest()
|
|
126
|
-
return content, identity
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
def _annotate_record(root: Path, merge_base: str, relative_path: str, all_record: dict[str, object], all_domains: dict[str, set[str]]) -> None:
|
|
130
|
-
content, identity = _record_content(root, merge_base, relative_path)
|
|
131
|
-
lower_path = relative_path.lower()
|
|
132
|
-
all_record.update({"domains": sorted([each_domain for each_domain, all_paths in all_domains.items() if relative_path in all_paths] + (["committed"] if all_record["status"] != "?" else [])), "operation_by_domain": {}, "content_sha256": identity, "binary": b"\0" in content, "added": 0, "deleted": 0, "package": _package_key(relative_path), "public_api": int(any(marker.lower() in lower_path for marker in ALL_PUBLIC_API_MARKERS)), "dependency": int(any(marker.lower() in lower_path for marker in ALL_DEPENDENCY_MARKERS)), "call_path": int(lower_path.endswith(ALL_SOURCE_SUFFIXES))})
|
|
133
|
-
all_record["operation_by_domain"] = {each_domain: ("A" if all_record["status"] == "?" else all_record["status"]) for each_domain in all_record["domains"]}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
def _effective_records(root: Path, merge_base: str, all_domains: dict[str, set[str]], state_root: Path | None = None) -> list[dict[str, object]]:
|
|
137
|
-
status_bytes = _git(root, ["diff", "--name-status", "-z", merge_base, "--"], is_required=True)
|
|
138
|
-
status_tokens = status_bytes.split(b"\0")
|
|
139
|
-
records_by_path: dict[str, dict[str, object]] = {}
|
|
140
|
-
all_untracked_paths = _untracked_paths(root, state_root)
|
|
141
|
-
index = 0
|
|
142
|
-
while index < len(status_tokens) - 1:
|
|
143
|
-
status = status_tokens[index].decode(errors="surrogateescape")
|
|
144
|
-
index += 1
|
|
145
|
-
if not status:
|
|
146
|
-
continue
|
|
147
|
-
prior_path = None
|
|
148
|
-
if status[0] in {"R", "C"}:
|
|
149
|
-
prior_path = status_tokens[index].decode(errors="surrogateescape")
|
|
150
|
-
index += 1
|
|
151
|
-
current_path = status_tokens[index].decode(errors="surrogateescape")
|
|
152
|
-
index += 1
|
|
153
|
-
records_by_path[current_path] = {"path": current_path, "prior_path": prior_path, "status": status[0]}
|
|
154
|
-
for each_path in all_untracked_paths:
|
|
155
|
-
records_by_path.setdefault(each_path, {"path": each_path, "prior_path": None, "status": "?"})
|
|
156
|
-
if state_root is not None:
|
|
157
|
-
tracked_state_paths = [each_path for each_path in records_by_path if _is_inside_state_root(root, each_path, state_root) and records_by_path[each_path]["status"] != "?"]
|
|
158
|
-
if tracked_state_paths:
|
|
159
|
-
raise ValueError(ROUTING_STATE_ROOT_TRACKED)
|
|
160
|
-
records_by_path = {each_path: record for each_path, record in records_by_path.items() if not _is_inside_state_root(root, each_path, state_root)}
|
|
161
|
-
for each_path, each_record in records_by_path.items():
|
|
162
|
-
_annotate_record(root, merge_base, each_path, each_record, all_domains)
|
|
163
|
-
numstat = _git(root, ["diff", "--numstat", "-z", merge_base, "--"], is_required=True).split(b"\0")
|
|
164
|
-
index = 0
|
|
165
|
-
while index + 1 < len(numstat):
|
|
166
|
-
fields = numstat[index].split(b"\t")
|
|
167
|
-
index += 1
|
|
168
|
-
if len(fields) < STATUS_CODE_LENGTH:
|
|
169
|
-
continue
|
|
170
|
-
if len(fields) == STATUS_CODE_LENGTH:
|
|
171
|
-
if index >= len(numstat):
|
|
172
|
-
continue
|
|
173
|
-
path = numstat[index].decode(errors="surrogateescape")
|
|
174
|
-
index += 1
|
|
175
|
-
else:
|
|
176
|
-
path = fields[2].decode(errors="surrogateescape")
|
|
177
|
-
record = records_by_path.get(path)
|
|
178
|
-
if record is None:
|
|
179
|
-
continue
|
|
180
|
-
record["added"] = int(fields[0]) if fields[0].isdigit() else 0
|
|
181
|
-
record["deleted"] = int(fields[1]) if fields[1].isdigit() else 0
|
|
182
|
-
record["binary"] = fields[0] == b"-" or fields[1] == b"-"
|
|
183
|
-
for each_path in all_untracked_paths:
|
|
184
|
-
record = records_by_path[each_path]
|
|
185
|
-
content = (root / each_path).read_bytes() if (root / each_path).is_file() else b""
|
|
186
|
-
if b"\0" not in content:
|
|
187
|
-
record["added"] = content.count(b"\n")
|
|
188
|
-
return [records_by_path[each_path] for each_path in sorted(records_by_path)]
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
def _committed_paths(root: Path, merge_base: str) -> set[str]:
|
|
192
|
-
committed_paths = _git(root, ["diff", "--name-only", "-z", f"{merge_base}..{GIT_HEAD_REF}"], is_required=True).decode(errors="surrogateescape")
|
|
193
|
-
return {path for path in committed_paths.split("\0") if path}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
def _hard_triggers(all_paths: set[str]) -> list[str]:
|
|
197
|
-
lower_paths = {path.lower() for path in all_paths}
|
|
198
|
-
return [name for name, markers in ALL_HARD_TRIGGER_MARKERS.items() if any(marker in path for path in lower_paths for marker in markers)]
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
def inventory_generation(repo_root: str | Path, base_ref: str | None = None, state_root: str | Path | None = None) -> dict[str, object]:
|
|
202
|
-
"""Return one hashed inventory covering committed and working-tree changes.
|
|
203
|
-
|
|
204
|
-
Args:
|
|
205
|
-
repo_root: Repository to inspect.
|
|
206
|
-
base_ref: Optional validated base reference.
|
|
207
|
-
Returns:
|
|
208
|
-
Canonical evidence and domain metadata.
|
|
209
|
-
"""
|
|
210
|
-
root = Path(repo_root).resolve()
|
|
211
|
-
state_root = Path(state_root).resolve() if state_root is not None else None
|
|
212
|
-
head = _git(root, ["rev-parse", "--verify", GIT_HEAD_REF], is_required=True).decode().strip()
|
|
213
|
-
requested_base, base = _resolve_base(root, base_ref)
|
|
214
|
-
merge_base = _git(root, ["merge-base", base, head], is_required=True)
|
|
215
|
-
merge_base = merge_base.decode().strip()
|
|
216
|
-
domains = _status_domains(root, state_root=state_root)
|
|
217
|
-
committed = _committed_paths(root, merge_base)
|
|
218
|
-
all_paths = committed | set().union(*domains.values())
|
|
219
|
-
records = _effective_records(root, merge_base, domains, state_root=state_root)
|
|
220
|
-
packages = {str(record["package"]) for record in records}
|
|
221
|
-
public_api = int(any(record["public_api"] for record in records))
|
|
222
|
-
dependencies = int(any(record["dependency"] for record in records))
|
|
223
|
-
triggers = _hard_triggers(all_paths)
|
|
224
|
-
patch_bytes = _git(root, ["diff", "--binary", "--full-index", "--no-ext-diff", merge_base, "--"], is_required=True)
|
|
225
|
-
untracked_identities = [{"path": each_path, "sha256": record["content_sha256"]} for each_path, record in ((record["path"], record) for record in records) if "untracked" in record["domains"]]
|
|
226
|
-
base_source = "explicit" if base_ref else ("remote_default" if requested_base == REMOTE_HEAD_REF else "configured_fallback")
|
|
227
|
-
canonical = json.dumps({"base_ref": requested_base, "base_source": base_source, "base_sha": base, "merge_base_sha": merge_base, "head_sha": head, "domains": {each_domain: sorted(all_paths) for each_domain, all_paths in domains.items()}, "files": records, "untracked_identities": sorted(untracked_identities, key=lambda record: record["path"])}, sort_keys=True, separators=(",", ":")).encode() + patch_bytes
|
|
228
|
-
digest = hashlib.sha256(canonical).hexdigest()
|
|
229
|
-
return {"paths": [record["path"] for record in records], "files_by_domain": records, "committed": sorted(committed), "staged": sorted(domains["staged"]), "unstaged": sorted(domains["unstaged"]), "untracked": sorted(domains["untracked"]), "base_ref": requested_base, "base_source": base_source, "base_sha": base, "merge_base_sha": merge_base, "head_sha": head, "merge_base": merge_base, "HEAD": head, "inventory_hash": digest, "diff_hash": digest, "packages": len(packages), "risk": MIN_NONEMPTY_RISK if records else 0, "public_api": public_api, "dependencies": dependencies, "hard_triggers": triggers}
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
def _package_key(path: str) -> str:
|
|
233
|
-
parts = path.split("/")
|
|
234
|
-
if len(parts) > 1 and parts[0] in ALL_PACKAGE_ROOT_NAMES:
|
|
235
|
-
return PATH_SEPARATOR.join(parts[:2])
|
|
236
|
-
return parts[0] if len(parts) > 1 else ROOT_PACKAGE
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
def build_generation(repo_root: str | Path, requested_override: str | None = None, card_path: str | Path | None = None, base_ref: str | None = None, all_policy: Mapping[str, object] | None = None, state_root: str | Path | None = None) -> dict[str, object]:
|
|
240
|
-
"""Build and optionally persist one immutable tier card.
|
|
241
|
-
|
|
242
|
-
Args:
|
|
243
|
-
repo_root: Repository to inspect.
|
|
244
|
-
requested_override: Optional requested tier.
|
|
245
|
-
card_path: Optional destination for the card.
|
|
246
|
-
base_ref: Optional base reference.
|
|
247
|
-
all_policy: Optional tier policy.
|
|
248
|
-
Returns:
|
|
249
|
-
The generated decision card.
|
|
250
|
-
"""
|
|
251
|
-
inventory = inventory_generation(repo_root, base_ref=base_ref, state_root=state_root)
|
|
252
|
-
card = build_decision(inventory, str(inventory["base_ref"]), str(inventory["HEAD"]), str(Path(repo_root).resolve()), str(inventory["inventory_hash"]), requested_override=requested_override, all_policy=all_policy)
|
|
253
|
-
card["inventory_hash"] = inventory["inventory_hash"]
|
|
254
|
-
if card_path is not None:
|
|
255
|
-
destination = Path(card_path)
|
|
256
|
-
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
257
|
-
with destination.open("x", encoding="utf-8") as card_file:
|
|
258
|
-
json.dump(card, card_file, sort_keys=True, indent=JSON_INDENT)
|
|
259
|
-
card_file.write("\n")
|
|
260
|
-
return card
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
def load_policy() -> dict[str, object]:
|
|
264
|
-
"""Load and validate versioned tier policy data.
|
|
265
|
-
|
|
266
|
-
Args:
|
|
267
|
-
Returns:
|
|
268
|
-
Parsed policy mapping.
|
|
269
|
-
Raises:
|
|
270
|
-
ValueError: If the policy schema is invalid.
|
|
271
|
-
"""
|
|
272
|
-
policy_path = Path(__file__).parent.parent / "reference" / "tier-policy.json"
|
|
273
|
-
policy = json.loads(policy_path.read_text(encoding="utf-8"))
|
|
274
|
-
if policy.get("version") != 1 or tuple(policy.get("tiers", ())) != ALL_TIER_ORDER or set(policy.get("thresholds", {})) != set(ALL_TIER_ORDER):
|
|
275
|
-
raise ValueError(MALFORMED_TIER_POLICY)
|
|
276
|
-
return policy
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
def policy_hash(all_policy: Mapping[str, object]) -> str:
|
|
280
|
-
"""Return the stable policy digest used by decision records.
|
|
281
|
-
|
|
282
|
-
Args:
|
|
283
|
-
all_policy: Parsed tier policy.
|
|
284
|
-
Returns:
|
|
285
|
-
Stable SHA-256 digest.
|
|
286
|
-
"""
|
|
287
|
-
return canonical_json_hash(all_policy)
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
def canonical_json_hash(all_record: Mapping[str, object]) -> str:
|
|
291
|
-
"""Return the stable SHA-256 digest for a JSON-compatible mapping.
|
|
292
|
-
|
|
293
|
-
Args:
|
|
294
|
-
all_record: Mapping to encode.
|
|
295
|
-
Returns:
|
|
296
|
-
Stable SHA-256 digest.
|
|
297
|
-
"""
|
|
298
|
-
encoded_record = json.dumps(all_record, sort_keys=True, separators=(",", ":")).encode()
|
|
299
|
-
return hashlib.sha256(encoded_record).hexdigest()
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
def calculate_tier(all_evidence: Mapping[str, object], all_policy: Mapping[str, object] | None = None) -> str:
|
|
303
|
-
"""Calculate a tier from six bounded axes and hard triggers.
|
|
304
|
-
|
|
305
|
-
Args:
|
|
306
|
-
all_evidence: Bounded change evidence.
|
|
307
|
-
all_policy: Optional tier policy.
|
|
308
|
-
Returns:
|
|
309
|
-
Automatic tier identifier.
|
|
310
|
-
"""
|
|
311
|
-
selected_policy = all_policy or load_policy()
|
|
312
|
-
if set(all_evidence.get("hard_triggers", ())).intersection(selected_policy["hard_triggers"]):
|
|
313
|
-
return "T3"
|
|
314
|
-
score = sum(min(MAX_AXIS_VALUE, int(all_evidence.get(axis, 0))) * int(weight) for axis, weight in selected_policy["axis_weights"].items())
|
|
315
|
-
if score >= selected_policy["thresholds"]["T3"]:
|
|
316
|
-
return "T3"
|
|
317
|
-
if score >= selected_policy["thresholds"]["T2"]:
|
|
318
|
-
return "T2"
|
|
319
|
-
return "T1"
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
def effective_tier(calculated_tier: str, requested_override: str | None, all_policy: Mapping[str, object] | None = None) -> str:
|
|
323
|
-
"""Apply an approved explicit tier override.
|
|
324
|
-
|
|
325
|
-
Args:
|
|
326
|
-
calculated_tier: Automatic tier.
|
|
327
|
-
requested_override: Optional requested tier.
|
|
328
|
-
all_policy: Optional tier policy.
|
|
329
|
-
Returns:
|
|
330
|
-
Effective tier identifier.
|
|
331
|
-
Raises:
|
|
332
|
-
ValueError: If either tier is invalid or the downgrade is unapproved.
|
|
333
|
-
"""
|
|
334
|
-
selected_policy = all_policy or load_policy()
|
|
335
|
-
if calculated_tier not in ALL_TIER_ORDER or requested_override not in (*ALL_TIER_ORDER, None):
|
|
336
|
-
raise ValueError(UNKNOWN_TIER)
|
|
337
|
-
if requested_override is None or requested_override == calculated_tier or requested_override > calculated_tier:
|
|
338
|
-
return requested_override or calculated_tier
|
|
339
|
-
if requested_override in selected_policy["approved_downgrades"]:
|
|
340
|
-
return requested_override
|
|
341
|
-
raise ValueError(UNAPPROVED_TIER_DOWNGRADE)
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
def build_decision(all_change: Mapping[str, object], base: str, commit: str, worktree: str, diff_hash: str, requested_override: str | None = None, all_policy: Mapping[str, object] | None = None) -> dict[str, object]:
|
|
345
|
-
"""Build a serializable tier card with evidence and policy identity.
|
|
346
|
-
|
|
347
|
-
Args:
|
|
348
|
-
all_change: Canonical inventory evidence.
|
|
349
|
-
base: Resolved base reference.
|
|
350
|
-
commit: Current HEAD.
|
|
351
|
-
worktree: Repository path.
|
|
352
|
-
diff_hash: Canonical evidence digest.
|
|
353
|
-
requested_override: Optional requested tier.
|
|
354
|
-
Returns:
|
|
355
|
-
Serializable decision card.
|
|
356
|
-
"""
|
|
357
|
-
selected_policy = all_policy or load_policy()
|
|
358
|
-
calculated = calculate_tier(all_change, all_policy=selected_policy)
|
|
359
|
-
return {"calculated_tier": calculated, "requested_override": requested_override, "effective_tier": effective_tier(calculated, requested_override, all_policy=selected_policy), "evidence": dict(all_change), "policy_hash": policy_hash(selected_policy), "base_ref": base, "base_source": all_change.get("base_source"), "merge_base": all_change.get("merge_base"), "HEAD": commit, "worktree": str(Path(worktree).resolve()), "diff_hash": diff_hash}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
"""Review-tier constants package."""
|
|
File without changes
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
"""Constants for review_tier_constants."""
|
|
2
|
-
|
|
3
|
-
DEFAULT_BRANCH_FALLBACK = "main"
|
|
4
|
-
GIT_HEAD_REF = "HEAD"
|
|
5
|
-
REMOTE_HEAD_REF = "refs/remotes/origin/HEAD"
|
|
6
|
-
MAX_AXIS_VALUE = 2
|
|
7
|
-
MIN_NONEMPTY_RISK = 1
|
|
8
|
-
GIT_COMMAND_FAILED = "GIT_COMMAND_FAILED"
|
|
9
|
-
INVALID_BASE_REF = "INVALID_BASE_REF"
|
|
10
|
-
AMBIGUOUS_BASE_REF = "AMBIGUOUS_BASE_REF"
|
|
11
|
-
MALFORMED_TIER_POLICY = "MALFORMED_TIER_POLICY"
|
|
12
|
-
UNKNOWN_TIER = "UNKNOWN_TIER"
|
|
13
|
-
UNAPPROVED_TIER_DOWNGRADE = "UNAPPROVED_TIER_DOWNGRADE"
|
|
14
|
-
ALL_TIER_ORDER = ("T1", "T2", "T3")
|
|
15
|
-
ALL_PUBLIC_API_MARKERS = ("__init__.py", "api.py", "cli.py", "SKILL.md")
|
|
16
|
-
ALL_DEPENDENCY_MARKERS = ("package.json", "requirements", "pyproject.toml", "poetry.lock")
|
|
17
|
-
ALL_HARD_TRIGGER_MARKERS = {"security": ("security", "auth", "permission"), "migration": ("migration", "migrations"), "public-api": ("api.py", "__init__.py"), "release": ("release", "version")}
|
|
18
|
-
ALL_STATUS_DOMAINS = ("staged", "unstaged", "untracked")
|
|
19
|
-
JSON_INDENT = 2
|
|
20
|
-
ALL_STATUS_ARGUMENTS = ("status", "--porcelain=v1", "-z", "--untracked-files=all")
|
|
21
|
-
ROOT_PACKAGE = "."
|
|
22
|
-
STATUS_PREFIX_LENGTH = 3
|
|
23
|
-
STATUS_CODE_LENGTH = 2
|
|
24
|
-
ALL_PACKAGE_ROOT_NAMES = ("packages", "apps", "libs", "services")
|
|
25
|
-
PATH_SEPARATOR = "/"
|
|
26
|
-
ALL_SOURCE_SUFFIXES = (".py", ".js", ".ts")
|
|
27
|
-
ALL_UNTRACKED_ARGUMENTS = ("ls-files", "--others", "--exclude-standard", "-z")
|
|
28
|
-
ROUTING_STATE_ROOT_TRACKED = "ROUTING_STATE_ROOT_TRACKED"
|
|
29
|
-
PLUGIN_DATA_ENVIRONMENT = "CLAUDE_PLUGIN_DATA"
|
|
@@ -1,159 +0,0 @@
|
|
|
1
|
-
import subprocess
|
|
2
|
-
from pathlib import Path
|
|
3
|
-
|
|
4
|
-
import pytest
|
|
5
|
-
from review_tier import (
|
|
6
|
-
build_generation,
|
|
7
|
-
inventory_generation,
|
|
8
|
-
router_state_directory,
|
|
9
|
-
router_state_root,
|
|
10
|
-
router_state_root_id,
|
|
11
|
-
)
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def test_generation_uses_live_git_surface(tmp_path: Path) -> None:
|
|
15
|
-
subprocess.run(["git", "init", "-q", "-b", "main"], cwd=tmp_path, check=True)
|
|
16
|
-
(tmp_path / "tracked.py").write_text("x = 1\n", encoding="utf-8")
|
|
17
|
-
subprocess.run(["git", "add", "tracked.py"], cwd=tmp_path, check=True)
|
|
18
|
-
subprocess.run(["git", "-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-qm", "init"], cwd=tmp_path, check=True)
|
|
19
|
-
(tmp_path / "untracked.py").write_text("y = 2\n", encoding="utf-8")
|
|
20
|
-
inventory = inventory_generation(tmp_path)
|
|
21
|
-
assert "untracked.py" in inventory["untracked"]
|
|
22
|
-
card = build_generation(tmp_path, card_path=tmp_path / "card.json")
|
|
23
|
-
assert card["inventory_hash"] == inventory["inventory_hash"]
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
def test_router_state_root_id_is_stable_for_normalized_roots(tmp_path: Path) -> None:
|
|
27
|
-
(tmp_path / "data").mkdir()
|
|
28
|
-
(tmp_path / "data-link").symlink_to(tmp_path / "data", target_is_directory=True)
|
|
29
|
-
state_root = router_state_root(tmp_path, {"CLAUDE_PLUGIN_DATA": str(tmp_path / "data" / ".." / "data")})
|
|
30
|
-
symlink_root = router_state_root(tmp_path, {"CLAUDE_PLUGIN_DATA": str(tmp_path / "data-link")})
|
|
31
|
-
assert state_root == symlink_root
|
|
32
|
-
assert router_state_root_id(state_root) == router_state_root_id(symlink_root)
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
def test_router_state_directory_is_scoped_under_the_router_root(tmp_path: Path) -> None:
|
|
36
|
-
state_directory = router_state_directory(tmp_path)
|
|
37
|
-
custom_environment = {"CLAUDE_PLUGIN_DATA": str(tmp_path / "custom-data")}
|
|
38
|
-
custom_state_directory = router_state_directory(tmp_path, custom_environment)
|
|
39
|
-
|
|
40
|
-
assert state_directory.parent == router_state_root(tmp_path)
|
|
41
|
-
assert custom_state_directory == router_state_root(tmp_path, custom_environment) / state_directory.name
|
|
42
|
-
assert custom_state_directory != state_directory
|
|
43
|
-
assert len(state_directory.name) == 64
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
def test_generation_records_committed_and_working_domains_once(tmp_path: Path) -> None:
|
|
47
|
-
repository = tmp_path / "repository"
|
|
48
|
-
repository.mkdir()
|
|
49
|
-
subprocess.run(["git", "init", "-q", "-b", "main"], cwd=repository, check=True)
|
|
50
|
-
subprocess.run(["git", "config", "user.name", "Test"], cwd=repository, check=True)
|
|
51
|
-
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=repository, check=True)
|
|
52
|
-
tracked = repository / "packages" / "feature.py"
|
|
53
|
-
tracked.parent.mkdir()
|
|
54
|
-
tracked.write_text("one\n", encoding="utf-8")
|
|
55
|
-
subprocess.run(["git", "add", "."], cwd=repository, check=True)
|
|
56
|
-
subprocess.run(["git", "commit", "-qm", "initial"], cwd=repository, check=True)
|
|
57
|
-
initial_commit = subprocess.run(["git", "rev-parse", "HEAD"], cwd=repository, check=True, capture_output=True, text=True).stdout.strip()
|
|
58
|
-
tracked.write_text("one\nbase\n", encoding="utf-8")
|
|
59
|
-
subprocess.run(["git", "add", str(tracked)], cwd=repository, check=True)
|
|
60
|
-
subprocess.run(["git", "commit", "-qm", "feature"], cwd=repository, check=True)
|
|
61
|
-
tracked.write_text("one\ntwo\n", encoding="utf-8")
|
|
62
|
-
subprocess.run(["git", "add", str(tracked)], cwd=repository, check=True)
|
|
63
|
-
tracked.write_text("one\ntwo\nthree\n", encoding="utf-8")
|
|
64
|
-
(repository / "new.py").write_text("new\n", encoding="utf-8")
|
|
65
|
-
inventory = inventory_generation(repository, initial_commit)
|
|
66
|
-
records = {record["path"]: record["domains"] for record in inventory["files_by_domain"]}
|
|
67
|
-
assert records["packages/feature.py"] == ["committed", "staged", "unstaged"]
|
|
68
|
-
assert records["new.py"] == ["untracked"]
|
|
69
|
-
assert inventory["base_ref"] and inventory["merge_base"] and inventory["HEAD"]
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
@pytest.mark.parametrize("base_ref", ["missing", "HEAD~99"])
|
|
73
|
-
def test_invalid_base_writes_no_card(tmp_path: Path, base_ref: str) -> None:
|
|
74
|
-
repository = tmp_path / "repository"
|
|
75
|
-
repository.mkdir()
|
|
76
|
-
subprocess.run(["git", "init", "-q", "-b", "main"], cwd=repository, check=True)
|
|
77
|
-
(repository / "seed.txt").write_text("seed\n", encoding="utf-8")
|
|
78
|
-
subprocess.run(["git", "add", "seed.txt"], cwd=repository, check=True)
|
|
79
|
-
subprocess.run(["git", "-c", "user.name=Test", "-c", "user.email=test@example.com", "commit", "-qm", "initial"], cwd=repository, check=True)
|
|
80
|
-
with pytest.raises(ValueError, match="INVALID_BASE_REF"):
|
|
81
|
-
build_generation(repository, base_ref=base_ref, card_path=tmp_path / "card.json")
|
|
82
|
-
assert not (tmp_path / "card.json").exists()
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
def _git_repository(root: Path) -> None:
|
|
86
|
-
subprocess.run(["git", "init", "-q", "-b", "main"], cwd=root, check=True)
|
|
87
|
-
subprocess.run(["git", "config", "user.name", "Test"], cwd=root, check=True)
|
|
88
|
-
subprocess.run(["git", "config", "user.email", "test@example.com"], cwd=root, check=True)
|
|
89
|
-
(root / "seed.txt").write_text("seed\n", encoding="utf-8")
|
|
90
|
-
subprocess.run(["git", "add", "."], cwd=root, check=True)
|
|
91
|
-
subprocess.run(["git", "commit", "-qm", "initial"], cwd=root, check=True)
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
def test_diff_hash_changes_when_same_path_content_changes(tmp_path: Path) -> None:
|
|
95
|
-
_git_repository(tmp_path)
|
|
96
|
-
tracked_path = tmp_path / "seed.txt"
|
|
97
|
-
first_hash = inventory_generation(tmp_path)["diff_hash"]
|
|
98
|
-
tracked_path.write_text("changed\n", encoding="utf-8")
|
|
99
|
-
assert inventory_generation(tmp_path)["diff_hash"] != first_hash
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
def test_inventory_preserves_rename_prior_path_and_operations(tmp_path: Path) -> None:
|
|
103
|
-
_git_repository(tmp_path)
|
|
104
|
-
old_path = tmp_path / "old name.txt"
|
|
105
|
-
old_path.write_text("content\n", encoding="utf-8")
|
|
106
|
-
subprocess.run(["git", "add", "."], cwd=tmp_path, check=True)
|
|
107
|
-
subprocess.run(["git", "commit", "-qm", "rename source"], cwd=tmp_path, check=True)
|
|
108
|
-
new_path = tmp_path / "new name.txt"
|
|
109
|
-
old_path.rename(new_path)
|
|
110
|
-
subprocess.run(["git", "add", "-A"], cwd=tmp_path, check=True)
|
|
111
|
-
records = {record["path"]: record for record in inventory_generation(tmp_path)["files_by_domain"]}
|
|
112
|
-
assert records["new name.txt"]["prior_path"] == "old name.txt"
|
|
113
|
-
assert records["new name.txt"]["operation_by_domain"]["staged"] == "R"
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
def test_inventory_preserves_delete_binary_space_unicode_and_symlink_identity(tmp_path: Path) -> None:
|
|
117
|
-
_git_repository(tmp_path)
|
|
118
|
-
deleted = tmp_path / "deleted file.txt"
|
|
119
|
-
deleted.write_text("delete me\n", encoding="utf-8")
|
|
120
|
-
binary = tmp_path / "binary.bin"
|
|
121
|
-
binary.write_bytes(b"\0\xff")
|
|
122
|
-
unicode_path = tmp_path / "café.txt"
|
|
123
|
-
unicode_path.write_text("unicode\n", encoding="utf-8")
|
|
124
|
-
link = tmp_path / "link.txt"
|
|
125
|
-
link.symlink_to("seed.txt")
|
|
126
|
-
subprocess.run(["git", "add", "."], cwd=tmp_path, check=True)
|
|
127
|
-
subprocess.run(["git", "commit", "-qm", "surface"], cwd=tmp_path, check=True)
|
|
128
|
-
surface_commit = subprocess.run(["git", "rev-parse", "HEAD"], cwd=tmp_path, check=True, capture_output=True, text=True).stdout.strip()
|
|
129
|
-
deleted.unlink()
|
|
130
|
-
binary.write_bytes(b"\0\x00changed")
|
|
131
|
-
unicode_path.write_text("unicode changed\n", encoding="utf-8")
|
|
132
|
-
link.unlink()
|
|
133
|
-
link.symlink_to("café.txt")
|
|
134
|
-
subprocess.run(["git", "add", "-u"], cwd=tmp_path, check=True)
|
|
135
|
-
subprocess.run(["git", "add", "-A"], cwd=tmp_path, check=True)
|
|
136
|
-
records = {record["path"]: record for record in inventory_generation(tmp_path, surface_commit)["files_by_domain"]}
|
|
137
|
-
assert records["deleted file.txt"]["content_sha256"] != "0" * 64
|
|
138
|
-
assert records["binary.bin"]["binary"] is True
|
|
139
|
-
assert records["café.txt"]["path"] == "café.txt"
|
|
140
|
-
assert records["link.txt"]["content_sha256"]
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
def test_package_count_uses_distinct_monorepo_package_keys(tmp_path: Path) -> None:
|
|
144
|
-
_git_repository(tmp_path)
|
|
145
|
-
for path in ("packages/one/a.py", "packages/one/b.py", "apps/two/c.py", "root.py"):
|
|
146
|
-
target = tmp_path / path
|
|
147
|
-
target.parent.mkdir(parents=True, exist_ok=True)
|
|
148
|
-
target.write_text("x\n", encoding="utf-8")
|
|
149
|
-
assert inventory_generation(tmp_path)["packages"] == 3
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
def test_explicit_and_default_base_provenance_fields_are_exact(tmp_path: Path) -> None:
|
|
153
|
-
_git_repository(tmp_path)
|
|
154
|
-
default_inventory = inventory_generation(tmp_path)
|
|
155
|
-
explicit_inventory = inventory_generation(tmp_path, "HEAD")
|
|
156
|
-
assert default_inventory["base_source"] == "configured_fallback"
|
|
157
|
-
assert explicit_inventory["base_source"] == "explicit"
|
|
158
|
-
for field in ("base_ref", "base_source", "base_sha", "merge_base_sha", "head_sha"):
|
|
159
|
-
assert default_inventory[field]
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
from review_tier import (
|
|
2
|
-
build_decision,
|
|
3
|
-
calculate_tier,
|
|
4
|
-
canonical_json_hash,
|
|
5
|
-
effective_tier,
|
|
6
|
-
load_policy,
|
|
7
|
-
policy_hash,
|
|
8
|
-
)
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
def test_public_policy_functions_return_behavior() -> None:
|
|
12
|
-
tier_policy = load_policy()
|
|
13
|
-
assert policy_hash(tier_policy)
|
|
14
|
-
assert canonical_json_hash(tier_policy) == policy_hash(tier_policy)
|
|
15
|
-
decision = build_decision({}, "base", "head", ".", "digest", requested_override="T1")
|
|
16
|
-
assert decision["effective_tier"] == "T1"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def test_tier_boundaries_and_hard_trigger() -> None:
|
|
20
|
-
assert calculate_tier({"packages": 0, "risk": 0}) == "T1"
|
|
21
|
-
assert calculate_tier({"hard_triggers": ["security"]}) == "T3"
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
def test_all_axes_accept_zero_one_two_boundaries() -> None:
|
|
25
|
-
axes = ("packages", "risk", "public_api", "dependencies")
|
|
26
|
-
assert calculate_tier({axis: 0 for axis in axes}) == "T1"
|
|
27
|
-
assert calculate_tier({axis: 1 for axis in axes}) == "T3"
|
|
28
|
-
assert calculate_tier({axis: 2 for axis in axes}) == "T3"
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def test_upward_and_approved_downward_override() -> None:
|
|
32
|
-
assert effective_tier("T1", "T3") == "T3"
|
|
33
|
-
assert effective_tier("T3", "T1") == "T1"
|