claude-dev-env 4.3.0 → 7.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/agents/pr-description-writer.md +2 -2
- package/.agents/agents/test_agent_frontmatter.py +2 -2
- package/.agents/agents/test_pr_description_writer_style.py +1 -3
- package/.agents/skills/e-code-review/SKILL.md +12 -12
- package/.agents/skills/e-code-review/preflight-proposal.contract.test.mjs +20 -0
- package/.agents/skills/e-code-review/reference/fix.md +15 -17
- package/.agents/skills/e-code-review/reference/loop.md +35 -39
- package/.agents/skills/e-code-review/reference/mode-contract.test.mjs +2 -1
- package/.agents/skills/e-code-review/reference/preflight-proposal.md +44 -9
- package/.agents/skills/e-code-review/reference/runner-selection.md +13 -14
- package/.agents/skills/issue-tracker/SKILL.md +5 -4
- package/.agents/skills/pull-request/SKILL.md +11 -13
- package/.agents/skills/pull-request/reference/publication-tasks.md +3 -3
- package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/constants.py +9 -1
- package/.agents/skills/pull-request/scripts/pull_request.py +33 -44
- package/.agents/skills/pull-request/scripts/test_github_pr_command_constants.py +9 -0
- package/.agents/skills/pull-request/scripts/test_pull_request.py +1 -3
- package/.agents/skills/skill-builder/SKILL.md +3 -196
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +6 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +64 -19
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +59 -0
- package/bin/install.agents-home.test.mjs +15 -13
- package/bin/install.mjs +28 -0
- package/bin/install.policy-lint.test.mjs +78 -0
- package/bin/install.prune.test.mjs +59 -20
- package/bin/install.pstack-plugin.test.mjs +114 -0
- package/bin/install.test.mjs +37 -0
- package/bin/refresh-pstack-plugin-skills.test.mjs +211 -0
- package/hooks/advisory/pr_done_reminder.py +342 -0
- package/hooks/advisory/test_pr_done_reminder.py +249 -0
- package/hooks/blocking/bash_post_call_dispatcher.py +53 -6
- package/hooks/blocking/code_rules_paired_test.py +22 -15
- package/hooks/blocking/test_bash_post_call_dispatcher.py +65 -4
- package/hooks/blocking/test_code_rules_enforcer_paired_test.py +36 -29
- package/hooks/blocking/test_code_rules_js_resume_task_enumeration.py +4 -4
- package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +5 -5
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +68 -133
- package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +20 -51
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +18 -18
- package/hooks/git-hooks/git_hooks_constants/staged_policy_lint.py +12 -0
- package/hooks/git-hooks/pre_commit.py +52 -11
- package/hooks/git-hooks/test_installed_policy_lint.py +159 -0
- package/hooks/git-hooks/test_native_policy_commit.py +87 -0
- package/hooks/git-hooks/test_pre_commit.py +246 -0
- package/hooks/hooks.json +1 -1
- package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +14 -1
- package/hooks/hooks_constants/pr_done_reminder_constants.py +98 -0
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -43
- package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +19 -2
- package/hooks/hooks_constants/test_pr_done_reminder_constants.py +40 -0
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +20 -32
- package/package.json +2 -2
- package/rules/durable-post-artifacts.md +2 -1
- package/rules/git-workflow.md +6 -6
- package/scripts/check.ps1 +0 -1
- package/{.agents/skills/run-claude-dev-env/driver.mjs → scripts/ci/installer-lifecycle.mjs} +3 -28
- package/scripts/ci/installer-lifecycle.test.mjs +26 -0
- package/scripts/ci/windows-installer-lifecycle.ps1 +1 -11
- package/scripts/dev_env_scripts_constants/shared_tree_constants.py +4 -5
- package/{.agents/skills/_shared/pr-loop/scripts → scripts}/durable_post_lint.py +5 -41
- package/{.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/durable_post_lint_constants.py → scripts/durable_post_lint_config/config/constants.py} +0 -10
- package/{.agents/skills/_shared/pr-loop/scripts/test_durable_post_lint_constants.py → scripts/durable_post_lint_config/config/test_constants.py} +1 -1
- package/scripts/policy_lint/adapter_detectors.py +13 -6
- package/scripts/policy_lint/adapters.py +7 -4
- package/scripts/policy_lint/config/constants.py +5 -0
- package/scripts/policy_lint/engine.py +48 -4
- package/scripts/policy_lint/model.py +1 -0
- package/scripts/policy_lint/registry.py +1 -1
- package/scripts/policy_lint/selection.py +2 -1
- package/scripts/policy_lint/selection_git.py +2 -0
- package/scripts/refresh_pstack_plugin_skills.mjs +200 -0
- package/scripts/shared_tree_paths.py +32 -3
- package/scripts/test_shared_tree_paths.py +33 -0
- package/scripts/tests/test_policy_lint_rules_archive.py +120 -0
- package/scripts/tests/test_policy_lint_rules_engine.py +25 -4
- package/scripts/tests/test_policy_lint_rules_installed_loader.py +29 -0
- package/scripts/tests/test_policy_lint_rules_registry.py +227 -13
- package/scripts/tests/test_policy_lint_selection.py +25 -2
- package/scripts/tests/test_policy_lint_selection_native_index.py +30 -0
- package/scripts/tests/test_rules.py +3 -7
- package/.agents/skills/_shared/pr-loop/.claude/CLAUDE.md +0 -1
- package/.agents/skills/_shared/pr-loop/AGENTS.md +0 -1
- package/.agents/skills/_shared/pr-loop/audit-contract.md +0 -5
- package/.agents/skills/_shared/pr-loop/audit-reply-template.md +0 -5
- package/.agents/skills/_shared/pr-loop/code-rules-gate.md +0 -5
- package/.agents/skills/_shared/pr-loop/fix-protocol.md +0 -5
- package/.agents/skills/_shared/pr-loop/gh-payloads.md +0 -5
- package/.agents/skills/_shared/pr-loop/portable-driver.md +0 -149
- package/.agents/skills/_shared/pr-loop/post-audit-thread-contract.md +0 -5
- package/.agents/skills/_shared/pr-loop/precatch-rubric.md +0 -5
- package/.agents/skills/_shared/pr-loop/preflight-proposal.contract.test.mjs +0 -119
- package/.agents/skills/_shared/pr-loop/preflight-proposal.md +0 -5
- package/.agents/skills/_shared/pr-loop/prompts/.claude/CLAUDE.md +0 -1
- package/.agents/skills/_shared/pr-loop/prompts/AGENTS.md +0 -1
- package/.agents/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +0 -324
- package/.agents/skills/_shared/pr-loop/scripts/.claude/CLAUDE.md +0 -1
- package/.agents/skills/_shared/pr-loop/scripts/AGENTS.md +0 -1
- package/.agents/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +0 -29
- package/.agents/skills/_shared/pr-loop/scripts/_cli_utils.py +0 -22
- package/.agents/skills/_shared/pr-loop/scripts/_path_resolver.py +0 -186
- package/.agents/skills/_shared/pr-loop/scripts/_xml_utils.py +0 -20
- package/.agents/skills/_shared/pr-loop/scripts/audit_category_schema.py +0 -355
- package/.agents/skills/_shared/pr-loop/scripts/build_audit_prompt.py +0 -289
- package/.agents/skills/_shared/pr-loop/scripts/build_converge_task_list.py +0 -310
- package/.agents/skills/_shared/pr-loop/scripts/build_fix_prompt.py +0 -292
- package/.agents/skills/_shared/pr-loop/scripts/init_loop_state.py +0 -134
- package/.agents/skills/_shared/pr-loop/scripts/portable_converge_driver.py +0 -1646
- package/.agents/skills/_shared/pr-loop/scripts/preflight_worktree.py +0 -392
- package/.agents/skills/_shared/pr-loop/scripts/select_converge_pacer.py +0 -215
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/.claude/CLAUDE.md +0 -1
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +0 -1
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/__init__.py +0 -0
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +0 -37
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/converge_task_list_constants.py +0 -56
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/handoff_constants.py +0 -45
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/pacer_constants.py +0 -47
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +0 -157
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/portable_driver_constants.py +0 -177
- package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +0 -70
- package/.agents/skills/_shared/pr-loop/scripts/teardown_worktrees.py +0 -172
- package/.agents/skills/_shared/pr-loop/scripts/test__path_resolver.py +0 -106
- package/.agents/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +0 -94
- package/.agents/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +0 -499
- package/.agents/skills/_shared/pr-loop/scripts/test_build_converge_task_list.py +0 -140
- package/.agents/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +0 -262
- package/.agents/skills/_shared/pr-loop/scripts/test_init_loop_state.py +0 -71
- package/.agents/skills/_shared/pr-loop/scripts/test_portable_converge_driver.py +0 -1119
- package/.agents/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +0 -287
- package/.agents/skills/_shared/pr-loop/scripts/test_select_converge_pacer.py +0 -207
- package/.agents/skills/_shared/pr-loop/scripts/test_teardown_worktrees.py +0 -59
- package/.agents/skills/_shared/pr-loop/scripts/test_write_handoff.py +0 -201
- package/.agents/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +0 -182
- package/.agents/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +0 -206
- package/.agents/skills/_shared/pr-loop/scripts/write_handoff.py +0 -309
- package/.agents/skills/_shared/pr-loop/state-schema.md +0 -5
- package/.agents/skills/_shared/pr-loop/worker-spawn.md +0 -5
- package/.agents/skills/autoconverge/.claude/CLAUDE.md +0 -1
- package/.agents/skills/autoconverge/AGENTS.md +0 -1
- package/.agents/skills/autoconverge/SKILL.md +0 -431
- package/.agents/skills/autoconverge/reference/.claude/CLAUDE.md +0 -1
- package/.agents/skills/autoconverge/reference/AGENTS.md +0 -1
- package/.agents/skills/autoconverge/reference/closing-report.md +0 -93
- package/.agents/skills/autoconverge/reference/convergence-gates.md +0 -31
- package/.agents/skills/autoconverge/reference/convergence.md +0 -219
- package/.agents/skills/autoconverge/reference/copilot-findings.md +0 -51
- package/.agents/skills/autoconverge/reference/gotchas.md +0 -66
- package/.agents/skills/autoconverge/reference/headless-safety.md +0 -44
- package/.agents/skills/autoconverge/reference/multi-pr.md +0 -128
- package/.agents/skills/autoconverge/reference/self-closing-loop.md +0 -84
- package/.agents/skills/autoconverge/reference/stop-conditions.md +0 -127
- package/.agents/skills/autoconverge/test_portable_pacer_gate.py +0 -54
- package/.agents/skills/autoconverge/workflow/.claude/CLAUDE.md +0 -1
- package/.agents/skills/autoconverge/workflow/AGENTS.md +0 -1
- package/.agents/skills/autoconverge/workflow/aggregate_runs.py +0 -371
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/.claude/CLAUDE.md +0 -1
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +0 -1
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/__init__.py +0 -0
- package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +0 -250
- package/.agents/skills/autoconverge/workflow/converge.clean-audit.test.mjs +0 -665
- package/.agents/skills/autoconverge/workflow/converge.codex-gate.test.mjs +0 -472
- package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +0 -64
- package/.agents/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +0 -1009
- package/.agents/skills/autoconverge/workflow/converge.fix-progress.test.mjs +0 -107
- package/.agents/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +0 -33
- package/.agents/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +0 -15
- package/.agents/skills/autoconverge/workflow/converge.mjs +0 -3132
- package/.agents/skills/autoconverge/workflow/converge.p2-advance.test.mjs +0 -202
- package/.agents/skills/autoconverge/workflow/converge.path-aware.test.mjs +0 -47
- package/.agents/skills/autoconverge/workflow/converge.precatch.test.mjs +0 -152
- package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +0 -116
- package/.agents/skills/autoconverge/workflow/converge_multi.mjs +0 -169
- package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +0 -173
- package/.agents/skills/autoconverge/workflow/convergence_summary.py +0 -110
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +0 -2
- package/.agents/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +0 -266
- package/.agents/skills/autoconverge/workflow/render_report.py +0 -1024
- package/.agents/skills/autoconverge/workflow/test_aggregate_runs.py +0 -134
- package/.agents/skills/autoconverge/workflow/test_convergence_summary.py +0 -149
- package/.agents/skills/autoconverge/workflow/test_render_report.py +0 -812
- package/.agents/skills/hitl/SKILL.md +0 -45
- package/.agents/skills/pr-cleanup/SKILL.md +0 -140
- package/.agents/skills/pr-cleanup/reference/process-inventory.md +0 -17
- package/.agents/skills/pr-cleanup/reference/task-seeds.md +0 -22
- package/.agents/skills/pr-name-by-capability/SKILL.md +0 -73
- package/.agents/skills/pr-name-by-capability/reference/fetch-commands.md +0 -29
- package/.agents/skills/pr-name-by-capability/reference/offense-examples.md +0 -32
- package/.agents/skills/pr-name-by-capability/reference/report-template.md +0 -29
- package/.agents/skills/pr-name-by-capability/reference/rule-checklist.md +0 -38
- package/.agents/skills/pr-name-by-capability/reference/task-seeds.md +0 -10
- package/.agents/skills/pr-plain-language-cleanup/SKILL.md +0 -93
- package/.agents/skills/pr-refinement/SKILL.md +0 -31
- package/.agents/skills/pr-shared-extraction/SKILL.md +0 -177
- package/.agents/skills/pr-shared-extraction/reference/examples.md +0 -46
- package/.agents/skills/pr-shared-extraction/reference/offense-taxonomy.md +0 -100
- package/.agents/skills/pr-small-cl/SKILL.md +0 -52
- package/.agents/skills/pr-title-description/SKILL.md +0 -96
- package/.agents/skills/prototype/SKILL.md +0 -85
- package/.agents/skills/prototype/reference/honest-limitations.md +0 -23
- package/.agents/skills/prototype/reference/promotion-tasks.md +0 -23
- package/.agents/skills/prototype/scripts/build_sandbox_settings.py +0 -249
- package/.agents/skills/prototype/scripts/conftest.py +0 -15
- package/.agents/skills/prototype/scripts/launch_sandbox.py +0 -205
- package/.agents/skills/prototype/scripts/probe_sandbox_safety.py +0 -311
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/__init__.py +0 -1
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +0 -41
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +0 -23
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +0 -45
- package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +0 -10
- package/.agents/skills/prototype/scripts/test_build_sandbox_settings.py +0 -275
- package/.agents/skills/prototype/scripts/test_launch_sandbox.py +0 -303
- package/.agents/skills/prototype/scripts/test_probe_sandbox_safety.py +0 -312
- package/.agents/skills/prototype/workflows/promotion.md +0 -25
- package/.agents/skills/prototype/workflows/sandbox.md +0 -35
- package/.agents/skills/rebase/.claude/CLAUDE.md +0 -1
- package/.agents/skills/rebase/AGENTS.md +0 -1
- package/.agents/skills/rebase/SKILL.md +0 -178
- package/.agents/skills/review-router/SKILL.md +0 -51
- package/.agents/skills/review-router/reference/route-policy.json +0 -7
- package/.agents/skills/review-router/reference/task-seeds.md +0 -1
- package/.agents/skills/review-router/scripts/conftest.py +0 -4
- package/.agents/skills/review-router/scripts/review_router.py +0 -50
- package/.agents/skills/review-router/scripts/review_router_cli.py +0 -211
- package/.agents/skills/review-router/scripts/review_router_constants/__init__.py +0 -1
- package/.agents/skills/review-router/scripts/review_router_constants/config/__init__.py +0 -0
- package/.agents/skills/review-router/scripts/review_router_constants/config/constants.py +0 -13
- package/.agents/skills/review-router/scripts/test_review_router.py +0 -23
- package/.agents/skills/review-router/scripts/test_review_router_cli.py +0 -47
- package/.agents/skills/review-tier/SKILL.md +0 -56
- package/.agents/skills/review-tier/reference/task-seeds.md +0 -1
- package/.agents/skills/review-tier/reference/tier-policy.json +0 -9
- package/.agents/skills/review-tier/scripts/conftest.py +0 -4
- package/.agents/skills/review-tier/scripts/review_tier.py +0 -359
- package/.agents/skills/review-tier/scripts/review_tier_constants/__init__.py +0 -1
- package/.agents/skills/review-tier/scripts/review_tier_constants/config/__init__.py +0 -0
- package/.agents/skills/review-tier/scripts/review_tier_constants/config/constants.py +0 -29
- package/.agents/skills/review-tier/scripts/test_generation.py +0 -159
- package/.agents/skills/review-tier/scripts/test_review_tier.py +0 -33
- package/.agents/skills/run-claude-dev-env/SKILL.md +0 -84
- package/.agents/skills/session-log/.claude/CLAUDE.md +0 -1
- package/.agents/skills/session-log/AGENTS.md +0 -1
- package/.agents/skills/session-log/SKILL.md +0 -170
- package/.agents/skills/session-log/templates/frontmatter.md +0 -40
- package/.agents/skills/session-tidy/.claude/CLAUDE.md +0 -1
- package/.agents/skills/session-tidy/AGENTS.md +0 -1
- package/.agents/skills/session-tidy/SKILL.md +0 -191
- package/.agents/skills/skill-builder/references/.claude/CLAUDE.md +0 -1
- package/.agents/skills/skill-builder/references/AGENTS.md +0 -1
- package/.agents/skills/skill-builder/references/delegation-map.md +0 -130
- package/.agents/skills/skill-builder/references/description-field.md +0 -111
- package/.agents/skills/skill-builder/references/deterministic-elements.md +0 -218
- package/.agents/skills/skill-builder/references/progressive-disclosure.md +0 -122
- package/.agents/skills/skill-builder/references/self-audit-checklist.md +0 -126
- package/.agents/skills/skill-builder/references/skill-modularity.md +0 -111
- package/.agents/skills/skill-builder/references/skill-types.md +0 -232
- package/.agents/skills/skill-builder/references/thariq-x-post-skills.json +0 -33
- package/.agents/skills/skill-builder/templates/.claude/CLAUDE.md +0 -1
- package/.agents/skills/skill-builder/templates/AGENTS.md +0 -1
- package/.agents/skills/skill-builder/templates/gap-analysis.md +0 -87
- package/.agents/skills/skill-builder/workflows/.claude/CLAUDE.md +0 -1
- package/.agents/skills/skill-builder/workflows/AGENTS.md +0 -1
- package/.agents/skills/skill-builder/workflows/improve-skill.md +0 -136
- package/.agents/skills/skill-builder/workflows/new-skill.md +0 -179
- package/.agents/skills/skill-builder/workflows/polish-skill.md +0 -139
- package/.agents/skills/source-command-sr-loop/SKILL.md +0 -82
- package/.agents/skills/update/.claude/CLAUDE.md +0 -1
- package/.agents/skills/update/AGENTS.md +0 -1
- package/.agents/skills/update/SKILL.md +0 -190
- package/_shared/pr-loop/AGENTS.md +0 -1
- package/_shared/pr-loop/CLAUDE.md +0 -1
- package/_shared/pr-loop/audit-contract.md +0 -183
- package/_shared/pr-loop/audit-reply-template.md +0 -151
- package/_shared/pr-loop/code-rules-gate.md +0 -65
- package/_shared/pr-loop/fix-protocol.md +0 -60
- package/_shared/pr-loop/gh-payloads.md +0 -75
- package/_shared/pr-loop/post-audit-thread-contract.md +0 -51
- package/_shared/pr-loop/precatch-rubric.md +0 -66
- package/_shared/pr-loop/preflight-proposal.md +0 -56
- package/_shared/pr-loop/scripts/post_audit_thread.py +0 -1242
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/post_audit_thread_constants.py +0 -199
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +0 -1115
- package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +0 -129
- package/_shared/pr-loop/state-schema.md +0 -45
- package/_shared/pr-loop/worker-spawn.md +0 -189
- /package/{.agents/skills/_shared/pr-loop/scripts → scripts}/test_durable_post_lint.py +0 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { test } from 'node:test';
|
|
2
|
+
import { strict as assert } from 'node:assert';
|
|
3
|
+
import {
|
|
4
|
+
mkdtempSync,
|
|
5
|
+
mkdirSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
symlinkSync,
|
|
9
|
+
writeFileSync,
|
|
10
|
+
} from 'node:fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
12
|
+
import { dirname, join } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { execFileSync } from 'node:child_process';
|
|
15
|
+
import {
|
|
16
|
+
PSTACK_PLUGIN_MANIFEST_RELATIVE_PATH,
|
|
17
|
+
findPstackSkillDirectoryNames,
|
|
18
|
+
refreshPstackPluginManifest,
|
|
19
|
+
} from '../scripts/refresh_pstack_plugin_skills.mjs';
|
|
20
|
+
|
|
21
|
+
function buildPluginRoot(homeDirectory, skillNames, manifest) {
|
|
22
|
+
const pluginRoot = join(homeDirectory, 'pstack');
|
|
23
|
+
for (const eachSkillName of skillNames) {
|
|
24
|
+
const skillDirectory = join(pluginRoot, eachSkillName);
|
|
25
|
+
mkdirSync(skillDirectory, { recursive: true });
|
|
26
|
+
writeFileSync(join(skillDirectory, 'SKILL.md'), '---\n');
|
|
27
|
+
}
|
|
28
|
+
const manifestPath = join(pluginRoot, PSTACK_PLUGIN_MANIFEST_RELATIVE_PATH);
|
|
29
|
+
mkdirSync(join(pluginRoot, '.claude-plugin'), { recursive: true });
|
|
30
|
+
if (manifest !== null) {
|
|
31
|
+
writeFileSync(manifestPath, JSON.stringify(manifest));
|
|
32
|
+
}
|
|
33
|
+
return pluginRoot;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function withTemporaryHome(runAssertions) {
|
|
37
|
+
const homeDirectory = mkdtempSync(join(tmpdir(), 'cdev-pstack-manifest-'));
|
|
38
|
+
try {
|
|
39
|
+
runAssertions(homeDirectory);
|
|
40
|
+
} finally {
|
|
41
|
+
rmSync(homeDirectory, { recursive: true, force: true });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
test('finds only sub-folders that hold a skill entry file', () => {
|
|
46
|
+
withTemporaryHome((homeDirectory) => {
|
|
47
|
+
const pluginRoot = buildPluginRoot(homeDirectory, ['how', 'why'], { skills: [] });
|
|
48
|
+
mkdirSync(join(pluginRoot, 'docs'), { recursive: true });
|
|
49
|
+
writeFileSync(join(pluginRoot, '.claude-plugin', 'SKILL.md'), '---\n');
|
|
50
|
+
|
|
51
|
+
assert.deepEqual(findPstackSkillDirectoryNames(pluginRoot), ['how', 'why']);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('writes a manifest when the plugin folder has none', () => {
|
|
56
|
+
withTemporaryHome((homeDirectory) => {
|
|
57
|
+
const pluginRoot = buildPluginRoot(homeDirectory, ['how'], null);
|
|
58
|
+
|
|
59
|
+
const outcome = refreshPstackPluginManifest(pluginRoot);
|
|
60
|
+
|
|
61
|
+
assert.equal(outcome.didWrite, true);
|
|
62
|
+
const manifestPath = join(pluginRoot, PSTACK_PLUGIN_MANIFEST_RELATIVE_PATH);
|
|
63
|
+
const written = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
64
|
+
assert.equal(written.name, 'pstack');
|
|
65
|
+
assert.deepEqual(written.skills, ['./how']);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('adds a skill folder the manifest does not list', () => {
|
|
70
|
+
withTemporaryHome((homeDirectory) => {
|
|
71
|
+
const pluginRoot = buildPluginRoot(homeDirectory, ['how', 'why'], {
|
|
72
|
+
name: 'pstack',
|
|
73
|
+
skills: ['./how'],
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const outcome = refreshPstackPluginManifest(pluginRoot);
|
|
77
|
+
|
|
78
|
+
assert.equal(outcome.didWrite, true);
|
|
79
|
+
assert.deepEqual(outcome.previousSkillPaths, ['./how']);
|
|
80
|
+
assert.deepEqual(outcome.currentSkillPaths, ['./how', './why']);
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test('leaves the manifest byte for byte when nothing changed', () => {
|
|
85
|
+
withTemporaryHome((homeDirectory) => {
|
|
86
|
+
const pluginRoot = buildPluginRoot(homeDirectory, ['how'], {
|
|
87
|
+
name: 'pstack',
|
|
88
|
+
skills: ['./how'],
|
|
89
|
+
});
|
|
90
|
+
const manifestPath = join(pluginRoot, PSTACK_PLUGIN_MANIFEST_RELATIVE_PATH);
|
|
91
|
+
writeFileSync(manifestPath, JSON.stringify({ name: 'pstack', skills: ['./how'] }));
|
|
92
|
+
const before = readFileSync(manifestPath, 'utf8');
|
|
93
|
+
|
|
94
|
+
const outcome = refreshPstackPluginManifest(pluginRoot);
|
|
95
|
+
|
|
96
|
+
assert.equal(outcome.didWrite, false);
|
|
97
|
+
assert.equal(readFileSync(manifestPath, 'utf8'), before);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test('reads a string skills value as a one-entry list', () => {
|
|
102
|
+
withTemporaryHome((homeDirectory) => {
|
|
103
|
+
const pluginRoot = buildPluginRoot(homeDirectory, ['how'], {
|
|
104
|
+
name: 'pstack',
|
|
105
|
+
skills: './',
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const outcome = refreshPstackPluginManifest(pluginRoot);
|
|
109
|
+
|
|
110
|
+
assert.deepEqual(outcome.previousSkillPaths, ['./']);
|
|
111
|
+
assert.deepEqual(outcome.currentSkillPaths, ['./how']);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test('keeps manifest fields the refresher does not own', () => {
|
|
116
|
+
withTemporaryHome((homeDirectory) => {
|
|
117
|
+
const pluginRoot = buildPluginRoot(homeDirectory, ['how'], {
|
|
118
|
+
name: 'pstack',
|
|
119
|
+
version: '9.9.9',
|
|
120
|
+
description: 'a description the user edited',
|
|
121
|
+
skills: [],
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
refreshPstackPluginManifest(pluginRoot);
|
|
125
|
+
|
|
126
|
+
const manifestPath = join(pluginRoot, PSTACK_PLUGIN_MANIFEST_RELATIVE_PATH);
|
|
127
|
+
const written = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
128
|
+
assert.equal(written.version, '9.9.9');
|
|
129
|
+
assert.equal(written.description, 'a description the user edited');
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test('refuses a folder that is itself one skill', () => {
|
|
134
|
+
withTemporaryHome((homeDirectory) => {
|
|
135
|
+
const pluginRoot = buildPluginRoot(homeDirectory, ['how'], null);
|
|
136
|
+
writeFileSync(join(pluginRoot, 'SKILL.md'), '---\n');
|
|
137
|
+
|
|
138
|
+
const outcome = refreshPstackPluginManifest(pluginRoot);
|
|
139
|
+
|
|
140
|
+
assert.equal(outcome.didWrite, false);
|
|
141
|
+
assert.equal(outcome.reason, 'plugin root is itself a skill');
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test('running the file directly reports what it did', () => {
|
|
146
|
+
withTemporaryHome((homeDirectory) => {
|
|
147
|
+
const pluginRoot = buildPluginRoot(homeDirectory, ['how'], null);
|
|
148
|
+
const modulePath = join(
|
|
149
|
+
dirname(dirname(fileURLToPath(import.meta.url))),
|
|
150
|
+
'scripts',
|
|
151
|
+
'refresh_pstack_plugin_skills.mjs',
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
const firstRun = execFileSync('node', [modulePath, pluginRoot], { encoding: 'utf8' });
|
|
155
|
+
const secondRun = execFileSync('node', [modulePath, pluginRoot], { encoding: 'utf8' });
|
|
156
|
+
|
|
157
|
+
assert.match(firstRun, /Updated\. 1 skills listed\./);
|
|
158
|
+
assert.match(secondRun, /No change\. 1 skills listed\./);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
test('running the file through a directory link still reports what it did', () => {
|
|
163
|
+
withTemporaryHome((homeDirectory) => {
|
|
164
|
+
const pluginRoot = buildPluginRoot(homeDirectory, ['how'], null);
|
|
165
|
+
const scriptsDirectory = join(
|
|
166
|
+
dirname(dirname(fileURLToPath(import.meta.url))),
|
|
167
|
+
'scripts',
|
|
168
|
+
);
|
|
169
|
+
const linkedScriptsDirectory = join(homeDirectory, 'linked-scripts');
|
|
170
|
+
symlinkSync(scriptsDirectory, linkedScriptsDirectory, 'junction');
|
|
171
|
+
const linkedModulePath = join(linkedScriptsDirectory, 'refresh_pstack_plugin_skills.mjs');
|
|
172
|
+
|
|
173
|
+
const output = execFileSync('node', [linkedModulePath, pluginRoot], { encoding: 'utf8' });
|
|
174
|
+
|
|
175
|
+
assert.match(output, /Updated\. 1 skills listed\./);
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
test('with no folder argument it follows CLAUDE_CONFIG_DIR', () => {
|
|
180
|
+
withTemporaryHome((homeDirectory) => {
|
|
181
|
+
const claudeConfigDirectory = join(homeDirectory, 'profile', '.claude');
|
|
182
|
+
const pluginRoot = join(claudeConfigDirectory, 'skills', 'pstack');
|
|
183
|
+
mkdirSync(join(pluginRoot, 'how'), { recursive: true });
|
|
184
|
+
writeFileSync(join(pluginRoot, 'how', 'SKILL.md'), '---\n');
|
|
185
|
+
const modulePath = join(
|
|
186
|
+
dirname(dirname(fileURLToPath(import.meta.url))),
|
|
187
|
+
'scripts',
|
|
188
|
+
'refresh_pstack_plugin_skills.mjs',
|
|
189
|
+
);
|
|
190
|
+
|
|
191
|
+
const output = execFileSync('node', [modulePath], {
|
|
192
|
+
encoding: 'utf8',
|
|
193
|
+
env: { ...process.env, CLAUDE_CONFIG_DIR: claudeConfigDirectory },
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
assert.match(output, /Updated\. 1 skills listed\./);
|
|
197
|
+
const manifestPath = join(pluginRoot, PSTACK_PLUGIN_MANIFEST_RELATIVE_PATH);
|
|
198
|
+
assert.deepEqual(JSON.parse(readFileSync(manifestPath, 'utf8')).skills, ['./how']);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test('refuses a folder that holds no skills', () => {
|
|
203
|
+
withTemporaryHome((homeDirectory) => {
|
|
204
|
+
const pluginRoot = buildPluginRoot(homeDirectory, [], null);
|
|
205
|
+
|
|
206
|
+
const outcome = refreshPstackPluginManifest(pluginRoot);
|
|
207
|
+
|
|
208
|
+
assert.equal(outcome.didWrite, false);
|
|
209
|
+
assert.equal(outcome.reason, 'plugin root holds no skills');
|
|
210
|
+
});
|
|
211
|
+
});
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""PostToolUse context reminder: the PR done checklist after a push.
|
|
3
|
+
|
|
4
|
+
This hook never blocks. It watches every Bash call finish and, after a
|
|
5
|
+
successful ``git push`` or ``gh pr create``, probes the branch's pull request
|
|
6
|
+
through ``gh pr view`` and adds one loud checklist to the agent's context::
|
|
7
|
+
|
|
8
|
+
=== PR DONE CHECKLIST (context reminder, never a block) ===
|
|
9
|
+
PR #42 https://github.com/acme/widgets/pull/42
|
|
10
|
+
Mergeable: CONFLICTING (CONFLICTS with the base branch. Merge origin/main ...)
|
|
11
|
+
CI checks: 1 failing, 2 pending, 4 total
|
|
12
|
+
Draft: yes
|
|
13
|
+
Label done: not set
|
|
14
|
+
Verdict: NOT DONE. Do not add the done label yet.
|
|
15
|
+
Re-check: gh pr view 42 --json mergeable,mergeStateStatus,statusCheckRollup,labels
|
|
16
|
+
A PR is done only when every line above is clean.
|
|
17
|
+
|
|
18
|
+
Why a reminder and not a gate: agents keep working. The rule "a PR is done
|
|
19
|
+
only when GitHub reports it mergeable and CI is clean" arrives as live state
|
|
20
|
+
at the exact moment the agent just pushed, so no instruction file has to be
|
|
21
|
+
remembered. A block would stall the run; this only informs it.
|
|
22
|
+
|
|
23
|
+
Quiet branches: a non-Bash tool, a command that is not a push or a PR
|
|
24
|
+
creation, a push whose harness-reported exit status is non-zero, or a ``gh``
|
|
25
|
+
failure other than "no pull requests found" each emit nothing.
|
|
26
|
+
|
|
27
|
+
Hosted by ``blocking/bash_post_call_dispatcher.py``, which forwards the
|
|
28
|
+
``hookSpecificOutput.additionalContext`` this hook prints.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import json
|
|
34
|
+
import shlex
|
|
35
|
+
import subprocess
|
|
36
|
+
import sys
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
_hooks_root_directory = str(Path(__file__).resolve().parent.parent)
|
|
41
|
+
if _hooks_root_directory not in sys.path:
|
|
42
|
+
sys.path.insert(0, _hooks_root_directory)
|
|
43
|
+
from hooks_constants.bash_pre_tool_use_dispatcher_constants import (
|
|
44
|
+
ALL_BASH_AND_POWERSHELL_TOOL_NAMES,
|
|
45
|
+
)
|
|
46
|
+
from hooks_constants.pr_done_reminder_constants import (
|
|
47
|
+
ADD_LABEL_COMMAND_TEMPLATE,
|
|
48
|
+
ALL_FAILING_CHECK_CONCLUSIONS,
|
|
49
|
+
ALL_PASSING_CHECK_CONCLUSIONS,
|
|
50
|
+
CHECK_COMPLETED_STATUS,
|
|
51
|
+
DONE_LABEL_NAME,
|
|
52
|
+
EXIT_CODE_ERROR_PREFIX,
|
|
53
|
+
GH_PR_CREATE_ACTION,
|
|
54
|
+
GH_PR_SUBCOMMAND,
|
|
55
|
+
ALL_GH_PR_VIEW_ARGUMENTS,
|
|
56
|
+
ALL_GIT_OPTIONS_WITH_VALUE,
|
|
57
|
+
ALL_POWERSHELL_COMMAND_FLAGS,
|
|
58
|
+
ALL_POWERSHELL_PROGRAM_NAMES,
|
|
59
|
+
GH_PR_VIEW_TIMEOUT_SECONDS,
|
|
60
|
+
GH_PROGRAM_NAME,
|
|
61
|
+
GIT_PROGRAM_NAME,
|
|
62
|
+
GIT_PUSH_SUBCOMMAND,
|
|
63
|
+
MERGEABLE_CLEAN_VALUE,
|
|
64
|
+
MERGEABLE_UNKNOWN_VALUE,
|
|
65
|
+
NO_PULL_REQUEST_MARKER,
|
|
66
|
+
NO_PULL_REQUEST_REMINDER,
|
|
67
|
+
RECHECK_COMMAND_TEMPLATE,
|
|
68
|
+
REMINDER_FOOTER,
|
|
69
|
+
REMINDER_HEADER,
|
|
70
|
+
REMINDER_LINE_SEPARATOR,
|
|
71
|
+
ALL_REMINDER_HINTS_BY_MERGEABLE,
|
|
72
|
+
STATUS_CONTEXT_STATE_KEY,
|
|
73
|
+
VERDICT_DONE,
|
|
74
|
+
VERDICT_NOT_DONE,
|
|
75
|
+
)
|
|
76
|
+
from hooks_constants.pre_tool_use_stdin import read_hook_input_dictionary_from_stdin
|
|
77
|
+
from hooks_constants.shell_command_segments import (
|
|
78
|
+
effective_leading_program,
|
|
79
|
+
split_into_segments,
|
|
80
|
+
token_basename,
|
|
81
|
+
)
|
|
82
|
+
except ImportError as import_error:
|
|
83
|
+
raise ImportError(
|
|
84
|
+
"pr_done_reminder: cannot import its dependencies; "
|
|
85
|
+
"ensure the hooks directory is importable."
|
|
86
|
+
) from import_error
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _command_tokens(command: str) -> list[str]:
|
|
90
|
+
try:
|
|
91
|
+
return shlex.split(command, posix=True)
|
|
92
|
+
except ValueError:
|
|
93
|
+
return command.split()
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _segment_program_and_arguments(all_segment_tokens: list[str]) -> tuple[str, list[str]]:
|
|
97
|
+
program_token = effective_leading_program(all_segment_tokens)
|
|
98
|
+
if program_token is None:
|
|
99
|
+
return "", []
|
|
100
|
+
program_index = all_segment_tokens.index(program_token)
|
|
101
|
+
return token_basename(program_token), all_segment_tokens[program_index + 1 :]
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _git_subcommand(all_arguments: list[str]) -> str | None:
|
|
105
|
+
"""Return git's subcommand after its global options.
|
|
106
|
+
|
|
107
|
+
``git -C <path> push`` and ``git -c <key=value> push`` carry an option
|
|
108
|
+
value before the subcommand; ``--git-dir=<path>`` carries none. The first
|
|
109
|
+
token left after those is the subcommand, so ``git stash push`` yields
|
|
110
|
+
``stash`` and ``git log --grep push`` yields ``log``.
|
|
111
|
+
"""
|
|
112
|
+
should_skip_next_token = False
|
|
113
|
+
for each_argument in all_arguments:
|
|
114
|
+
if should_skip_next_token:
|
|
115
|
+
should_skip_next_token = False
|
|
116
|
+
continue
|
|
117
|
+
if each_argument in ALL_GIT_OPTIONS_WITH_VALUE:
|
|
118
|
+
should_skip_next_token = True
|
|
119
|
+
continue
|
|
120
|
+
if each_argument.startswith("-"):
|
|
121
|
+
continue
|
|
122
|
+
return each_argument
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _is_git_push(program_name: str, all_arguments: list[str]) -> bool:
|
|
127
|
+
return (
|
|
128
|
+
program_name == GIT_PROGRAM_NAME and _git_subcommand(all_arguments) == GIT_PUSH_SUBCOMMAND
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _is_gh_pr_create(program_name: str, all_arguments: list[str]) -> bool:
|
|
133
|
+
return program_name == GH_PROGRAM_NAME and all_arguments[:2] == [
|
|
134
|
+
GH_PR_SUBCOMMAND,
|
|
135
|
+
GH_PR_CREATE_ACTION,
|
|
136
|
+
]
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _powershell_inner_commands(all_command_tokens: list[str]) -> list[str]:
|
|
140
|
+
"""Return the script texts every ``pwsh -Command "..."`` wrapper runs.
|
|
141
|
+
|
|
142
|
+
Read from the raw shlex tokens, before segment splitting: the quoted
|
|
143
|
+
script is one token here, and splitting it on ``;`` first would cut a
|
|
144
|
+
``git add -A; git push`` script in half.
|
|
145
|
+
"""
|
|
146
|
+
all_inner_commands: list[str] = []
|
|
147
|
+
has_seen_powershell_program = False
|
|
148
|
+
for each_token_index, each_token in enumerate(all_command_tokens[:-1]):
|
|
149
|
+
if token_basename(each_token) in ALL_POWERSHELL_PROGRAM_NAMES:
|
|
150
|
+
has_seen_powershell_program = True
|
|
151
|
+
continue
|
|
152
|
+
if has_seen_powershell_program and each_token.lower() in ALL_POWERSHELL_COMMAND_FLAGS:
|
|
153
|
+
all_inner_commands.append(all_command_tokens[each_token_index + 1])
|
|
154
|
+
has_seen_powershell_program = False
|
|
155
|
+
return all_inner_commands
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def command_triggers_reminder(command: str) -> bool:
|
|
159
|
+
"""Return True when any segment of the command is a git push or a gh pr create.
|
|
160
|
+
|
|
161
|
+
A ``pwsh -Command "..."`` wrapper is unwrapped and its inner text scanned
|
|
162
|
+
the same way, since the shell rule routes Bash-tool commands through pwsh.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
command: The Bash command text the agent ran.
|
|
166
|
+
"""
|
|
167
|
+
all_command_tokens = _command_tokens(command)
|
|
168
|
+
for each_inner_command in _powershell_inner_commands(all_command_tokens):
|
|
169
|
+
if command_triggers_reminder(each_inner_command):
|
|
170
|
+
return True
|
|
171
|
+
for each_segment in split_into_segments(all_command_tokens):
|
|
172
|
+
program_name, all_arguments = _segment_program_and_arguments(each_segment)
|
|
173
|
+
if _is_git_push(program_name, all_arguments) or _is_gh_pr_create(
|
|
174
|
+
program_name, all_arguments
|
|
175
|
+
):
|
|
176
|
+
return True
|
|
177
|
+
return False
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _harness_reported_failure(tool_response: object) -> bool:
|
|
181
|
+
return isinstance(tool_response, str) and tool_response.startswith(EXIT_CODE_ERROR_PREFIX)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _check_outcome(all_check_fields: dict[str, object]) -> str:
|
|
185
|
+
"""Return failing, pending, or passing for one statusCheckRollup entry.
|
|
186
|
+
|
|
187
|
+
A CheckRun entry carries ``status`` and ``conclusion``; a StatusContext
|
|
188
|
+
entry carries ``state``. Both shapes appear in the same rollup list.
|
|
189
|
+
"""
|
|
190
|
+
state_text = str(all_check_fields.get(STATUS_CONTEXT_STATE_KEY) or "").upper()
|
|
191
|
+
if state_text:
|
|
192
|
+
if state_text in ALL_FAILING_CHECK_CONCLUSIONS:
|
|
193
|
+
return "failing"
|
|
194
|
+
return "passing" if state_text in ALL_PASSING_CHECK_CONCLUSIONS else "pending"
|
|
195
|
+
if str(all_check_fields.get("status") or "").upper() != CHECK_COMPLETED_STATUS:
|
|
196
|
+
return "pending"
|
|
197
|
+
conclusion_text = str(all_check_fields.get("conclusion") or "").upper()
|
|
198
|
+
return "failing" if conclusion_text in ALL_FAILING_CHECK_CONCLUSIONS else "passing"
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _check_counts(all_pr_fields: dict[str, object]) -> dict[str, int]:
|
|
202
|
+
all_checks = all_pr_fields.get("statusCheckRollup")
|
|
203
|
+
all_check_counts = {"failing": 0, "pending": 0, "passing": 0, "total": 0}
|
|
204
|
+
if not isinstance(all_checks, list):
|
|
205
|
+
return all_check_counts
|
|
206
|
+
for each_check_fields in all_checks:
|
|
207
|
+
if isinstance(each_check_fields, dict):
|
|
208
|
+
all_check_counts[_check_outcome(each_check_fields)] += 1
|
|
209
|
+
all_check_counts["total"] += 1
|
|
210
|
+
return all_check_counts
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def _checks_line(all_check_counts: dict[str, int]) -> str:
|
|
214
|
+
if all_check_counts["total"] == 0:
|
|
215
|
+
return "none reported"
|
|
216
|
+
return (
|
|
217
|
+
f"{all_check_counts['failing']} failing, "
|
|
218
|
+
f"{all_check_counts['pending']} pending, "
|
|
219
|
+
f"{all_check_counts['total']} total"
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _has_done_label(all_pr_fields: dict[str, object]) -> bool:
|
|
224
|
+
all_labels = all_pr_fields.get("labels")
|
|
225
|
+
if not isinstance(all_labels, list):
|
|
226
|
+
return False
|
|
227
|
+
return any(
|
|
228
|
+
isinstance(each_label, dict) and each_label.get("name") == DONE_LABEL_NAME
|
|
229
|
+
for each_label in all_labels
|
|
230
|
+
)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _is_done(mergeable_value: str, all_check_counts: dict[str, int]) -> bool:
|
|
234
|
+
return (
|
|
235
|
+
mergeable_value == MERGEABLE_CLEAN_VALUE
|
|
236
|
+
and all_check_counts["failing"] == 0
|
|
237
|
+
and all_check_counts["pending"] == 0
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _verdict_lines(number: object, is_done: bool) -> list[str]:
|
|
242
|
+
"""Return the verdict line, the add-label command on DONE, and the re-check line."""
|
|
243
|
+
all_lines = [f"Verdict: {VERDICT_DONE if is_done else VERDICT_NOT_DONE}"]
|
|
244
|
+
if is_done:
|
|
245
|
+
add_label_command = ADD_LABEL_COMMAND_TEMPLATE.format(number=number, label=DONE_LABEL_NAME)
|
|
246
|
+
all_lines.append(f"Add label: {add_label_command}")
|
|
247
|
+
all_lines.append(f"Re-check: {RECHECK_COMMAND_TEMPLATE.format(number=number)}")
|
|
248
|
+
return all_lines
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def build_reminder_context(all_pr_fields: dict[str, object]) -> str:
|
|
252
|
+
"""Build the checklist text from a ``gh pr view --json`` object.
|
|
253
|
+
|
|
254
|
+
Args:
|
|
255
|
+
all_pr_fields: The parsed pull request JSON with number, url, isDraft,
|
|
256
|
+
mergeable, mergeStateStatus, statusCheckRollup, and labels.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
The multi-line checklist ending in a DONE or NOT DONE verdict.
|
|
260
|
+
"""
|
|
261
|
+
number = all_pr_fields.get("number", "?")
|
|
262
|
+
mergeable_value = str(all_pr_fields.get("mergeable") or MERGEABLE_UNKNOWN_VALUE).upper()
|
|
263
|
+
mergeable_hint = ALL_REMINDER_HINTS_BY_MERGEABLE.get(
|
|
264
|
+
mergeable_value, ALL_REMINDER_HINTS_BY_MERGEABLE[MERGEABLE_UNKNOWN_VALUE]
|
|
265
|
+
)
|
|
266
|
+
all_check_counts = _check_counts(all_pr_fields)
|
|
267
|
+
all_lines = [
|
|
268
|
+
REMINDER_HEADER,
|
|
269
|
+
f"PR #{number} {all_pr_fields.get('url', '')}",
|
|
270
|
+
f"Mergeable: {mergeable_value} ({mergeable_hint})",
|
|
271
|
+
f"CI checks: {_checks_line(all_check_counts)}",
|
|
272
|
+
f"Draft: {'yes' if all_pr_fields.get('isDraft') else 'no'}",
|
|
273
|
+
f"Label done: {'set' if _has_done_label(all_pr_fields) else 'not set'}",
|
|
274
|
+
*_verdict_lines(number, _is_done(mergeable_value, all_check_counts)),
|
|
275
|
+
REMINDER_FOOTER,
|
|
276
|
+
]
|
|
277
|
+
return REMINDER_LINE_SEPARATOR.join(all_lines)
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def _probe_pull_request(cwd: str) -> str | None:
|
|
281
|
+
"""Return the reminder text from a live ``gh pr view``, or None to stay quiet."""
|
|
282
|
+
try:
|
|
283
|
+
completed_process = subprocess.run(
|
|
284
|
+
list(ALL_GH_PR_VIEW_ARGUMENTS),
|
|
285
|
+
cwd=cwd or None,
|
|
286
|
+
capture_output=True,
|
|
287
|
+
text=True,
|
|
288
|
+
encoding="utf-8",
|
|
289
|
+
errors="replace",
|
|
290
|
+
timeout=GH_PR_VIEW_TIMEOUT_SECONDS,
|
|
291
|
+
check=False,
|
|
292
|
+
)
|
|
293
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
294
|
+
return None
|
|
295
|
+
if completed_process.returncode != 0:
|
|
296
|
+
combined_output = f"{completed_process.stdout}\n{completed_process.stderr}".lower()
|
|
297
|
+
return NO_PULL_REQUEST_REMINDER if NO_PULL_REQUEST_MARKER in combined_output else None
|
|
298
|
+
try:
|
|
299
|
+
all_pr_fields = json.loads(completed_process.stdout)
|
|
300
|
+
except json.JSONDecodeError:
|
|
301
|
+
return None
|
|
302
|
+
if not isinstance(all_pr_fields, dict):
|
|
303
|
+
return None
|
|
304
|
+
return build_reminder_context(all_pr_fields)
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _emit_context(context_text: str) -> None:
|
|
308
|
+
payload = {
|
|
309
|
+
"hookSpecificOutput": {
|
|
310
|
+
"hookEventName": "PostToolUse",
|
|
311
|
+
"additionalContext": context_text,
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
sys.stdout.write(json.dumps(payload))
|
|
315
|
+
sys.stdout.flush()
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def main() -> None:
|
|
319
|
+
"""Add the PR done checklist to context after a successful push or PR creation.
|
|
320
|
+
|
|
321
|
+
Reads the PostToolUse payload from stdin. Every quiet branch returns with
|
|
322
|
+
no output, so this hook can never alter or block a tool call.
|
|
323
|
+
"""
|
|
324
|
+
hook_payload = read_hook_input_dictionary_from_stdin()
|
|
325
|
+
if (
|
|
326
|
+
hook_payload is None
|
|
327
|
+
or hook_payload.get("tool_name") not in ALL_BASH_AND_POWERSHELL_TOOL_NAMES
|
|
328
|
+
):
|
|
329
|
+
return
|
|
330
|
+
tool_input = hook_payload.get("tool_input")
|
|
331
|
+
command = tool_input.get("command", "") if isinstance(tool_input, dict) else ""
|
|
332
|
+
if not isinstance(command, str) or not command_triggers_reminder(command):
|
|
333
|
+
return
|
|
334
|
+
if _harness_reported_failure(hook_payload.get("tool_response")):
|
|
335
|
+
return
|
|
336
|
+
context_text = _probe_pull_request(str(hook_payload.get("cwd") or ""))
|
|
337
|
+
if context_text is not None:
|
|
338
|
+
_emit_context(context_text)
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
if __name__ == "__main__":
|
|
342
|
+
main()
|