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.
Files changed (350) hide show
  1. package/.agents/agents/pr-description-writer.md +2 -2
  2. package/.agents/agents/test_agent_frontmatter.py +2 -2
  3. package/.agents/agents/test_pr_description_writer_style.py +1 -3
  4. package/.agents/skills/e-code-review/SKILL.md +12 -12
  5. package/.agents/skills/e-code-review/preflight-proposal.contract.test.mjs +20 -0
  6. package/.agents/skills/e-code-review/reference/fix.md +15 -17
  7. package/.agents/skills/e-code-review/reference/loop.md +35 -39
  8. package/.agents/skills/e-code-review/reference/mode-contract.test.mjs +2 -1
  9. package/.agents/skills/e-code-review/reference/preflight-proposal.md +44 -9
  10. package/.agents/skills/e-code-review/reference/runner-selection.md +13 -14
  11. package/.agents/skills/issue-tracker/SKILL.md +7 -0
  12. package/.agents/skills/pull-request/SKILL.md +216 -0
  13. package/.agents/skills/pull-request/reference/publication-tasks.md +18 -0
  14. package/.agents/skills/pull-request/scripts/github_pr_command_constants/__init__.py +1 -0
  15. package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/__init__.py +1 -0
  16. package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/constants.py +69 -0
  17. package/.agents/skills/pull-request/scripts/pull_request.py +265 -0
  18. package/.agents/skills/pull-request/scripts/recover_legacy_author.py +251 -0
  19. package/.agents/skills/pull-request/scripts/test_github_pr_command_constants.py +58 -0
  20. package/.agents/skills/pull-request/scripts/test_pull_request.py +396 -0
  21. package/.agents/skills/pull-request/scripts/test_recover_legacy_author.py +221 -0
  22. package/.agents/skills/skill-builder/SKILL.md +3 -196
  23. package/.agents/skills/test-runner/SKILL.md +69 -0
  24. package/.agents/skills/test-runner/scripts/preflight_checks.py +365 -0
  25. package/.agents/skills/test-runner/scripts/run_tests.py +316 -0
  26. package/.agents/skills/test-runner/scripts/test_preflight_checks.py +185 -0
  27. package/.agents/skills/test-runner/scripts/test_run_tests.py +281 -0
  28. package/.agents/skills/test-runner/scripts/test_runner_constants/__init__.py +1 -0
  29. package/.agents/skills/test-runner/scripts/test_runner_constants/config/__init__.py +1 -0
  30. package/.agents/skills/test-runner/scripts/test_runner_constants/config/constants.py +96 -0
  31. package/.agents/skills-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
  32. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +6 -0
  33. package/_shared/pr-loop/scripts/terminology_sweep.py +64 -19
  34. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +59 -0
  35. package/bin/ever-shipped-skills.mjs +2 -0
  36. package/bin/ever-shipped-skills.test.mjs +8 -0
  37. package/bin/install.agents-home.test.mjs +15 -13
  38. package/bin/install.mjs +37 -1
  39. package/bin/install.policy-lint.test.mjs +78 -0
  40. package/bin/install.prune.test.mjs +149 -20
  41. package/bin/install.pstack-plugin.test.mjs +114 -0
  42. package/bin/install.test.mjs +61 -7
  43. package/bin/refresh-pstack-plugin-skills.test.mjs +211 -0
  44. package/hooks/advisory/pr_done_reminder.py +342 -0
  45. package/hooks/advisory/test_pr_done_reminder.py +249 -0
  46. package/hooks/blocking/bash_post_call_dispatcher.py +53 -6
  47. package/hooks/blocking/code_rules_paired_test.py +22 -15
  48. package/hooks/blocking/pii_prevention_blocker.py +32 -51
  49. package/hooks/blocking/test_bash_post_call_dispatcher.py +55 -17
  50. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +1 -4
  51. package/hooks/blocking/test_code_rules_command_dispatch.py +2 -2
  52. package/hooks/blocking/test_code_rules_enforcer_paired_test.py +36 -29
  53. package/hooks/blocking/test_code_rules_js_resume_task_enumeration.py +4 -4
  54. package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +5 -5
  55. package/hooks/blocking/test_pii_prevention_blocker.py +37 -89
  56. package/hooks/blocking/test_pre_tool_use_dispatcher.py +68 -133
  57. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +20 -51
  58. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +18 -18
  59. package/hooks/gh_post_body_texts.py +195 -0
  60. package/hooks/git-hooks/git_hooks_constants/staged_policy_lint.py +12 -0
  61. package/hooks/git-hooks/pre_commit.py +52 -11
  62. package/hooks/git-hooks/test_installed_policy_lint.py +159 -0
  63. package/hooks/git-hooks/test_native_policy_commit.py +87 -0
  64. package/hooks/git-hooks/test_pre_commit.py +246 -0
  65. package/hooks/hooks.json +1 -16
  66. package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +14 -2
  67. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -8
  68. package/hooks/hooks_constants/gh_post_body_texts_constants.py +12 -0
  69. package/hooks/hooks_constants/pr_done_reminder_constants.py +98 -0
  70. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -43
  71. package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +17 -7
  72. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +0 -7
  73. package/hooks/hooks_constants/test_failure_recorder_constants.py +3 -3
  74. package/hooks/hooks_constants/test_gh_post_body_texts_constants.py +19 -0
  75. package/hooks/hooks_constants/test_pr_done_reminder_constants.py +40 -0
  76. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +20 -32
  77. package/hooks/observability/test_failure_recorder.py +1 -1
  78. package/hooks/pyproject.toml +1 -1
  79. package/hooks/test_gh_post_body_texts.py +79 -0
  80. package/package.json +2 -2
  81. package/rules/durable-post-artifacts.md +32 -20
  82. package/rules/gh-cli-conventions.md +23 -13
  83. package/rules/git-workflow.md +21 -8
  84. package/scripts/check.ps1 +0 -1
  85. package/{.agents/skills/run-claude-dev-env/driver.mjs → scripts/ci/installer-lifecycle.mjs} +3 -28
  86. package/scripts/ci/installer-lifecycle.test.mjs +26 -0
  87. package/scripts/ci/windows-installer-lifecycle.ps1 +1 -11
  88. package/scripts/dev_env_scripts_constants/shared_tree_constants.py +4 -5
  89. package/scripts/durable_post_lint.py +206 -0
  90. package/scripts/durable_post_lint_config/config/constants.py +96 -0
  91. package/scripts/durable_post_lint_config/config/test_constants.py +25 -0
  92. package/scripts/policy_lint/adapter_configuration.py +53 -12
  93. package/scripts/policy_lint/adapter_detectors.py +13 -6
  94. package/scripts/policy_lint/adapters.py +7 -4
  95. package/scripts/policy_lint/config/constants.py +5 -0
  96. package/scripts/policy_lint/engine.py +48 -4
  97. package/scripts/policy_lint/model.py +1 -0
  98. package/scripts/policy_lint/registry.py +1 -1
  99. package/scripts/policy_lint/selection.py +2 -1
  100. package/scripts/policy_lint/selection_git.py +2 -0
  101. package/scripts/refresh_pstack_plugin_skills.mjs +200 -0
  102. package/scripts/repository_checks/config/constants.py +0 -5
  103. package/scripts/shared_tree_paths.py +32 -3
  104. package/scripts/test_durable_post_lint.py +332 -0
  105. package/scripts/test_shared_tree_paths.py +33 -0
  106. package/scripts/tests/test_adapter_configuration.py +54 -0
  107. package/scripts/tests/test_policy_lint_rules_archive.py +120 -0
  108. package/scripts/tests/test_policy_lint_rules_engine.py +25 -4
  109. package/scripts/tests/test_policy_lint_rules_installed_loader.py +29 -0
  110. package/scripts/tests/test_policy_lint_rules_registry.py +227 -13
  111. package/scripts/tests/test_policy_lint_selection.py +25 -2
  112. package/scripts/tests/test_policy_lint_selection_native_index.py +30 -0
  113. package/scripts/tests/test_rules.py +3 -7
  114. package/.agents/skills/_shared/pr-loop/.claude/CLAUDE.md +0 -1
  115. package/.agents/skills/_shared/pr-loop/AGENTS.md +0 -1
  116. package/.agents/skills/_shared/pr-loop/audit-contract.md +0 -5
  117. package/.agents/skills/_shared/pr-loop/audit-reply-template.md +0 -5
  118. package/.agents/skills/_shared/pr-loop/code-rules-gate.md +0 -5
  119. package/.agents/skills/_shared/pr-loop/fix-protocol.md +0 -5
  120. package/.agents/skills/_shared/pr-loop/gh-payloads.md +0 -5
  121. package/.agents/skills/_shared/pr-loop/portable-driver.md +0 -149
  122. package/.agents/skills/_shared/pr-loop/post-audit-thread-contract.md +0 -5
  123. package/.agents/skills/_shared/pr-loop/precatch-rubric.md +0 -5
  124. package/.agents/skills/_shared/pr-loop/preflight-proposal.contract.test.mjs +0 -119
  125. package/.agents/skills/_shared/pr-loop/preflight-proposal.md +0 -5
  126. package/.agents/skills/_shared/pr-loop/prompts/.claude/CLAUDE.md +0 -1
  127. package/.agents/skills/_shared/pr-loop/prompts/AGENTS.md +0 -1
  128. package/.agents/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +0 -324
  129. package/.agents/skills/_shared/pr-loop/scripts/.claude/CLAUDE.md +0 -1
  130. package/.agents/skills/_shared/pr-loop/scripts/AGENTS.md +0 -1
  131. package/.agents/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +0 -29
  132. package/.agents/skills/_shared/pr-loop/scripts/_cli_utils.py +0 -22
  133. package/.agents/skills/_shared/pr-loop/scripts/_path_resolver.py +0 -186
  134. package/.agents/skills/_shared/pr-loop/scripts/_xml_utils.py +0 -20
  135. package/.agents/skills/_shared/pr-loop/scripts/audit_category_schema.py +0 -355
  136. package/.agents/skills/_shared/pr-loop/scripts/build_audit_prompt.py +0 -289
  137. package/.agents/skills/_shared/pr-loop/scripts/build_converge_task_list.py +0 -310
  138. package/.agents/skills/_shared/pr-loop/scripts/build_fix_prompt.py +0 -292
  139. package/.agents/skills/_shared/pr-loop/scripts/init_loop_state.py +0 -134
  140. package/.agents/skills/_shared/pr-loop/scripts/portable_converge_driver.py +0 -1646
  141. package/.agents/skills/_shared/pr-loop/scripts/preflight_worktree.py +0 -392
  142. package/.agents/skills/_shared/pr-loop/scripts/select_converge_pacer.py +0 -215
  143. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/.claude/CLAUDE.md +0 -1
  144. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +0 -1
  145. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/__init__.py +0 -0
  146. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +0 -37
  147. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/converge_task_list_constants.py +0 -56
  148. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/handoff_constants.py +0 -45
  149. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/pacer_constants.py +0 -47
  150. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +0 -157
  151. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/portable_driver_constants.py +0 -177
  152. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +0 -70
  153. package/.agents/skills/_shared/pr-loop/scripts/teardown_worktrees.py +0 -172
  154. package/.agents/skills/_shared/pr-loop/scripts/test__path_resolver.py +0 -106
  155. package/.agents/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +0 -94
  156. package/.agents/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +0 -499
  157. package/.agents/skills/_shared/pr-loop/scripts/test_build_converge_task_list.py +0 -140
  158. package/.agents/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +0 -262
  159. package/.agents/skills/_shared/pr-loop/scripts/test_init_loop_state.py +0 -71
  160. package/.agents/skills/_shared/pr-loop/scripts/test_portable_converge_driver.py +0 -1119
  161. package/.agents/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +0 -287
  162. package/.agents/skills/_shared/pr-loop/scripts/test_select_converge_pacer.py +0 -207
  163. package/.agents/skills/_shared/pr-loop/scripts/test_teardown_worktrees.py +0 -59
  164. package/.agents/skills/_shared/pr-loop/scripts/test_write_handoff.py +0 -201
  165. package/.agents/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +0 -182
  166. package/.agents/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +0 -206
  167. package/.agents/skills/_shared/pr-loop/scripts/write_handoff.py +0 -309
  168. package/.agents/skills/_shared/pr-loop/state-schema.md +0 -5
  169. package/.agents/skills/_shared/pr-loop/worker-spawn.md +0 -5
  170. package/.agents/skills/autoconverge/.claude/CLAUDE.md +0 -1
  171. package/.agents/skills/autoconverge/AGENTS.md +0 -1
  172. package/.agents/skills/autoconverge/SKILL.md +0 -431
  173. package/.agents/skills/autoconverge/reference/.claude/CLAUDE.md +0 -1
  174. package/.agents/skills/autoconverge/reference/AGENTS.md +0 -1
  175. package/.agents/skills/autoconverge/reference/closing-report.md +0 -93
  176. package/.agents/skills/autoconverge/reference/convergence-gates.md +0 -31
  177. package/.agents/skills/autoconverge/reference/convergence.md +0 -219
  178. package/.agents/skills/autoconverge/reference/copilot-findings.md +0 -51
  179. package/.agents/skills/autoconverge/reference/gotchas.md +0 -66
  180. package/.agents/skills/autoconverge/reference/headless-safety.md +0 -44
  181. package/.agents/skills/autoconverge/reference/multi-pr.md +0 -128
  182. package/.agents/skills/autoconverge/reference/self-closing-loop.md +0 -84
  183. package/.agents/skills/autoconverge/reference/stop-conditions.md +0 -127
  184. package/.agents/skills/autoconverge/test_portable_pacer_gate.py +0 -54
  185. package/.agents/skills/autoconverge/workflow/.claude/CLAUDE.md +0 -1
  186. package/.agents/skills/autoconverge/workflow/AGENTS.md +0 -1
  187. package/.agents/skills/autoconverge/workflow/aggregate_runs.py +0 -371
  188. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/.claude/CLAUDE.md +0 -1
  189. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +0 -1
  190. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/__init__.py +0 -0
  191. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +0 -250
  192. package/.agents/skills/autoconverge/workflow/converge.clean-audit.test.mjs +0 -665
  193. package/.agents/skills/autoconverge/workflow/converge.codex-gate.test.mjs +0 -472
  194. package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +0 -64
  195. package/.agents/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +0 -1009
  196. package/.agents/skills/autoconverge/workflow/converge.fix-progress.test.mjs +0 -107
  197. package/.agents/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +0 -33
  198. package/.agents/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +0 -15
  199. package/.agents/skills/autoconverge/workflow/converge.mjs +0 -3132
  200. package/.agents/skills/autoconverge/workflow/converge.p2-advance.test.mjs +0 -202
  201. package/.agents/skills/autoconverge/workflow/converge.path-aware.test.mjs +0 -47
  202. package/.agents/skills/autoconverge/workflow/converge.precatch.test.mjs +0 -152
  203. package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +0 -116
  204. package/.agents/skills/autoconverge/workflow/converge_multi.mjs +0 -169
  205. package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +0 -173
  206. package/.agents/skills/autoconverge/workflow/convergence_summary.py +0 -110
  207. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +0 -2
  208. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +0 -2
  209. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +0 -2
  210. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +0 -2
  211. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +0 -2
  212. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +0 -2
  213. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +0 -2
  214. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +0 -2
  215. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +0 -2
  216. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +0 -266
  217. package/.agents/skills/autoconverge/workflow/render_report.py +0 -1024
  218. package/.agents/skills/autoconverge/workflow/test_aggregate_runs.py +0 -134
  219. package/.agents/skills/autoconverge/workflow/test_convergence_summary.py +0 -149
  220. package/.agents/skills/autoconverge/workflow/test_render_report.py +0 -812
  221. package/.agents/skills/hitl/SKILL.md +0 -45
  222. package/.agents/skills/pr-cleanup/SKILL.md +0 -140
  223. package/.agents/skills/pr-cleanup/reference/process-inventory.md +0 -17
  224. package/.agents/skills/pr-cleanup/reference/task-seeds.md +0 -22
  225. package/.agents/skills/pr-name-by-capability/SKILL.md +0 -73
  226. package/.agents/skills/pr-name-by-capability/reference/fetch-commands.md +0 -29
  227. package/.agents/skills/pr-name-by-capability/reference/offense-examples.md +0 -32
  228. package/.agents/skills/pr-name-by-capability/reference/report-template.md +0 -29
  229. package/.agents/skills/pr-name-by-capability/reference/rule-checklist.md +0 -38
  230. package/.agents/skills/pr-name-by-capability/reference/task-seeds.md +0 -10
  231. package/.agents/skills/pr-plain-language-cleanup/SKILL.md +0 -93
  232. package/.agents/skills/pr-refinement/SKILL.md +0 -31
  233. package/.agents/skills/pr-shared-extraction/SKILL.md +0 -177
  234. package/.agents/skills/pr-shared-extraction/reference/examples.md +0 -46
  235. package/.agents/skills/pr-shared-extraction/reference/offense-taxonomy.md +0 -100
  236. package/.agents/skills/pr-small-cl/SKILL.md +0 -52
  237. package/.agents/skills/pr-title-description/SKILL.md +0 -96
  238. package/.agents/skills/prototype/SKILL.md +0 -85
  239. package/.agents/skills/prototype/reference/honest-limitations.md +0 -23
  240. package/.agents/skills/prototype/reference/promotion-tasks.md +0 -23
  241. package/.agents/skills/prototype/scripts/build_sandbox_settings.py +0 -249
  242. package/.agents/skills/prototype/scripts/conftest.py +0 -15
  243. package/.agents/skills/prototype/scripts/launch_sandbox.py +0 -205
  244. package/.agents/skills/prototype/scripts/probe_sandbox_safety.py +0 -311
  245. package/.agents/skills/prototype/scripts/prototype_scripts_constants/__init__.py +0 -1
  246. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
  247. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +0 -41
  248. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +0 -23
  249. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +0 -45
  250. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +0 -10
  251. package/.agents/skills/prototype/scripts/test_build_sandbox_settings.py +0 -275
  252. package/.agents/skills/prototype/scripts/test_launch_sandbox.py +0 -303
  253. package/.agents/skills/prototype/scripts/test_probe_sandbox_safety.py +0 -312
  254. package/.agents/skills/prototype/workflows/promotion.md +0 -25
  255. package/.agents/skills/prototype/workflows/sandbox.md +0 -35
  256. package/.agents/skills/rebase/.claude/CLAUDE.md +0 -1
  257. package/.agents/skills/rebase/AGENTS.md +0 -1
  258. package/.agents/skills/rebase/SKILL.md +0 -178
  259. package/.agents/skills/review-router/SKILL.md +0 -51
  260. package/.agents/skills/review-router/reference/route-policy.json +0 -7
  261. package/.agents/skills/review-router/reference/task-seeds.md +0 -1
  262. package/.agents/skills/review-router/scripts/conftest.py +0 -4
  263. package/.agents/skills/review-router/scripts/review_router.py +0 -50
  264. package/.agents/skills/review-router/scripts/review_router_cli.py +0 -211
  265. package/.agents/skills/review-router/scripts/review_router_constants/__init__.py +0 -1
  266. package/.agents/skills/review-router/scripts/review_router_constants/config/__init__.py +0 -0
  267. package/.agents/skills/review-router/scripts/review_router_constants/config/constants.py +0 -13
  268. package/.agents/skills/review-router/scripts/test_review_router.py +0 -23
  269. package/.agents/skills/review-router/scripts/test_review_router_cli.py +0 -47
  270. package/.agents/skills/review-tier/SKILL.md +0 -56
  271. package/.agents/skills/review-tier/reference/task-seeds.md +0 -1
  272. package/.agents/skills/review-tier/reference/tier-policy.json +0 -9
  273. package/.agents/skills/review-tier/scripts/conftest.py +0 -4
  274. package/.agents/skills/review-tier/scripts/review_tier.py +0 -359
  275. package/.agents/skills/review-tier/scripts/review_tier_constants/__init__.py +0 -1
  276. package/.agents/skills/review-tier/scripts/review_tier_constants/config/__init__.py +0 -0
  277. package/.agents/skills/review-tier/scripts/review_tier_constants/config/constants.py +0 -29
  278. package/.agents/skills/review-tier/scripts/test_generation.py +0 -159
  279. package/.agents/skills/review-tier/scripts/test_review_tier.py +0 -33
  280. package/.agents/skills/run-claude-dev-env/SKILL.md +0 -84
  281. package/.agents/skills/session-log/.claude/CLAUDE.md +0 -1
  282. package/.agents/skills/session-log/AGENTS.md +0 -1
  283. package/.agents/skills/session-log/SKILL.md +0 -170
  284. package/.agents/skills/session-log/templates/frontmatter.md +0 -40
  285. package/.agents/skills/session-tidy/.claude/CLAUDE.md +0 -1
  286. package/.agents/skills/session-tidy/AGENTS.md +0 -1
  287. package/.agents/skills/session-tidy/SKILL.md +0 -191
  288. package/.agents/skills/skill-builder/references/.claude/CLAUDE.md +0 -1
  289. package/.agents/skills/skill-builder/references/AGENTS.md +0 -1
  290. package/.agents/skills/skill-builder/references/delegation-map.md +0 -130
  291. package/.agents/skills/skill-builder/references/description-field.md +0 -111
  292. package/.agents/skills/skill-builder/references/deterministic-elements.md +0 -218
  293. package/.agents/skills/skill-builder/references/progressive-disclosure.md +0 -122
  294. package/.agents/skills/skill-builder/references/self-audit-checklist.md +0 -126
  295. package/.agents/skills/skill-builder/references/skill-modularity.md +0 -111
  296. package/.agents/skills/skill-builder/references/skill-types.md +0 -232
  297. package/.agents/skills/skill-builder/references/thariq-x-post-skills.json +0 -33
  298. package/.agents/skills/skill-builder/templates/.claude/CLAUDE.md +0 -1
  299. package/.agents/skills/skill-builder/templates/AGENTS.md +0 -1
  300. package/.agents/skills/skill-builder/templates/gap-analysis.md +0 -87
  301. package/.agents/skills/skill-builder/workflows/.claude/CLAUDE.md +0 -1
  302. package/.agents/skills/skill-builder/workflows/AGENTS.md +0 -1
  303. package/.agents/skills/skill-builder/workflows/improve-skill.md +0 -136
  304. package/.agents/skills/skill-builder/workflows/new-skill.md +0 -179
  305. package/.agents/skills/skill-builder/workflows/polish-skill.md +0 -139
  306. package/.agents/skills/source-command-sr-loop/SKILL.md +0 -82
  307. package/.agents/skills/update/.claude/CLAUDE.md +0 -1
  308. package/.agents/skills/update/AGENTS.md +0 -1
  309. package/.agents/skills/update/SKILL.md +0 -190
  310. package/_shared/pr-loop/AGENTS.md +0 -1
  311. package/_shared/pr-loop/CLAUDE.md +0 -1
  312. package/_shared/pr-loop/audit-contract.md +0 -183
  313. package/_shared/pr-loop/audit-reply-template.md +0 -151
  314. package/_shared/pr-loop/code-rules-gate.md +0 -65
  315. package/_shared/pr-loop/fix-protocol.md +0 -60
  316. package/_shared/pr-loop/gh-payloads.md +0 -75
  317. package/_shared/pr-loop/post-audit-thread-contract.md +0 -51
  318. package/_shared/pr-loop/precatch-rubric.md +0 -66
  319. package/_shared/pr-loop/preflight-proposal.md +0 -56
  320. package/_shared/pr-loop/scripts/post_audit_thread.py +0 -1242
  321. package/_shared/pr-loop/scripts/pr_loop_shared_constants/post_audit_thread_constants.py +0 -199
  322. package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +0 -1115
  323. package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +0 -129
  324. package/_shared/pr-loop/state-schema.md +0 -45
  325. package/_shared/pr-loop/worker-spawn.md +0 -189
  326. package/hooks/_gh_pr_author_swap_utils.py +0 -1225
  327. package/hooks/blocking/conventional_pr_title_gate.py +0 -443
  328. package/hooks/blocking/gh_body_arg_blocker.py +0 -176
  329. package/hooks/blocking/gh_pr_author_enforcer.py +0 -487
  330. package/hooks/blocking/gh_pr_author_restore.py +0 -105
  331. package/hooks/blocking/pr_description_writer_gate.py +0 -256
  332. package/hooks/blocking/test_conventional_pr_title_gate.py +0 -640
  333. package/hooks/blocking/test_gh_body_arg_blocker.py +0 -441
  334. package/hooks/blocking/test_gh_pr_author_enforcer.py +0 -1166
  335. package/hooks/blocking/test_gh_pr_author_restore.py +0 -513
  336. package/hooks/blocking/test_gh_pr_author_swap_utils.py +0 -910
  337. package/hooks/blocking/test_pr_description_writer_gate.py +0 -234
  338. package/hooks/blocking/test_preflight_check.py +0 -343
  339. package/hooks/blocking/test_test_preflight_check.py +0 -80
  340. package/hooks/blocking/test_volatile_path_in_post_blocker.py +0 -322
  341. package/hooks/blocking/volatile_path_in_post_blocker.py +0 -461
  342. package/hooks/hooks_constants/conventional_pr_title_gate_constants.py +0 -58
  343. package/hooks/hooks_constants/gh_pr_author_swap_constants.py +0 -76
  344. package/hooks/hooks_constants/pr_description_writer_gate_constants.py +0 -73
  345. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +0 -55
  346. package/hooks/observability/pr_description_writer_spawn_tracker.py +0 -113
  347. package/hooks/observability/test_pr_description_writer_spawn_tracker.py +0 -150
  348. package/hooks/session/gh_pr_author_session_cleanup.py +0 -175
  349. package/hooks/session/test_gh_pr_author_session_cleanup.py +0 -576
  350. package/hooks/test__gh_pr_author_swap_utils.py +0 -333
@@ -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()
@@ -0,0 +1,249 @@
1
+ """Behavior tests for the PR done-checklist context reminder.
2
+
3
+ The hook is a PostToolUse observer on Bash. It never blocks. These tests pin
4
+ three things: which commands wake it, what the checklist says for each live
5
+ PR state, and that a probe failure stays quiet. The ``gh`` probe is faked at
6
+ the subprocess boundary so no test touches the network.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import io
12
+ import json
13
+ import subprocess
14
+ import sys
15
+ from pathlib import Path
16
+
17
+ import pytest
18
+
19
+ try:
20
+ advisory_directory = str(Path(__file__).resolve().parent)
21
+ if advisory_directory not in sys.path:
22
+ sys.path.insert(0, advisory_directory)
23
+ import pr_done_reminder
24
+ except ImportError as import_error:
25
+ raise ImportError(
26
+ "test_pr_done_reminder: cannot import its sibling modules; "
27
+ "ensure the advisory directory is importable."
28
+ ) from import_error
29
+
30
+
31
+ _CLEAN_PR_OBJECT: dict[str, object] = {
32
+ "number": 42,
33
+ "url": "https://github.com/acme/widgets/pull/42",
34
+ "isDraft": True,
35
+ "mergeable": "MERGEABLE",
36
+ "mergeStateStatus": "CLEAN",
37
+ "statusCheckRollup": [
38
+ {"name": "pytest", "status": "COMPLETED", "conclusion": "SUCCESS"},
39
+ {"context": "lint", "state": "SUCCESS"},
40
+ ],
41
+ "labels": [],
42
+ }
43
+
44
+
45
+ def _payload(command: str, tool_response: object = None) -> str:
46
+ response: object = {"stdout": "", "stderr": ""} if tool_response is None else tool_response
47
+ return json.dumps(
48
+ {
49
+ "session_id": "reminder-session",
50
+ "cwd": "C:/repo",
51
+ "tool_name": "Bash",
52
+ "tool_input": {"command": command},
53
+ "tool_response": response,
54
+ }
55
+ )
56
+
57
+
58
+ def _fake_gh(pr_object: dict[str, object] | None, stderr: str = "") -> object:
59
+ def _run(*args: object, **kwargs: object) -> subprocess.CompletedProcess[str]:
60
+ del args, kwargs
61
+ if pr_object is None:
62
+ return subprocess.CompletedProcess([], 1, stdout="", stderr=stderr)
63
+ return subprocess.CompletedProcess([], 0, stdout=json.dumps(pr_object), stderr="")
64
+
65
+ return _run
66
+
67
+
68
+ def _run_main(
69
+ monkeypatch: pytest.MonkeyPatch,
70
+ capsys: pytest.CaptureFixture[str],
71
+ payload_text: str,
72
+ fake_gh: object,
73
+ ) -> str:
74
+ monkeypatch.setattr(pr_done_reminder.subprocess, "run", fake_gh)
75
+ monkeypatch.setattr(sys, "stdin", io.StringIO(payload_text))
76
+ pr_done_reminder.main()
77
+ return capsys.readouterr().out
78
+
79
+
80
+ def _additional_context(stdout_text: str) -> str:
81
+ return str(json.loads(stdout_text)["hookSpecificOutput"]["additionalContext"])
82
+
83
+
84
+ @pytest.mark.parametrize(
85
+ "command",
86
+ [
87
+ "git push",
88
+ "git push -u origin feat/x",
89
+ "git -C C:/repo push origin HEAD",
90
+ "gh pr create --draft --title t --body-file b.md",
91
+ "git add -A && git commit -F m.txt && git push",
92
+ "git -c push.default=current push",
93
+ "git --git-dir=C:/repo/.git push",
94
+ 'pwsh -NoProfile -Command "git push -u origin feat/x"',
95
+ "pwsh -NoProfile -Command 'gh pr create --draft --fill'",
96
+ 'powershell -Command "git add -A; git push"',
97
+ ],
98
+ )
99
+ def test_should_wake_on_git_push_and_gh_pr_create(command: str) -> None:
100
+ assert pr_done_reminder.command_triggers_reminder(command) is True
101
+
102
+
103
+ @pytest.mark.parametrize(
104
+ "command",
105
+ [
106
+ "git status",
107
+ "git pull",
108
+ "git stash push -u -q -- packages",
109
+ "git log --grep push",
110
+ "git commit -m push",
111
+ "gh pr view 42",
112
+ "gh pr ready --undo",
113
+ "echo git push",
114
+ 'pwsh -NoProfile -Command "git status"',
115
+ "pytest tests/",
116
+ ],
117
+ )
118
+ def test_should_stay_quiet_for_other_commands(command: str) -> None:
119
+ assert pr_done_reminder.command_triggers_reminder(command) is False
120
+
121
+
122
+ def test_should_say_done_when_mergeable_and_checks_pass() -> None:
123
+ context = pr_done_reminder.build_reminder_context(_CLEAN_PR_OBJECT)
124
+
125
+ assert context.startswith("=== PR DONE CHECKLIST")
126
+ assert "never a block" in context
127
+ assert "DONE. Add the label now:" in context
128
+ assert "gh pr edit 42 --add-label done" in context
129
+ assert "NOT DONE" not in context
130
+
131
+
132
+ def test_should_keep_the_checklist_order_verdict_then_recheck_then_footer() -> None:
133
+ all_lines = pr_done_reminder.build_reminder_context(_CLEAN_PR_OBJECT).split("\n")
134
+
135
+ all_labels = [each_line.split(":")[0] for each_line in all_lines[1:-1]]
136
+ assert all_labels == [
137
+ "PR #42 https",
138
+ "Mergeable",
139
+ "CI checks",
140
+ "Draft",
141
+ "Label done",
142
+ "Verdict",
143
+ "Add label",
144
+ "Re-check",
145
+ ]
146
+ assert all_lines[-1] == "A PR is done only when every line above is clean."
147
+
148
+
149
+ def test_should_say_not_done_and_name_the_conflict_fix() -> None:
150
+ conflicted = {**_CLEAN_PR_OBJECT, "mergeable": "CONFLICTING", "mergeStateStatus": "DIRTY"}
151
+
152
+ context = pr_done_reminder.build_reminder_context(conflicted)
153
+
154
+ assert "NOT DONE" in context
155
+ assert "CONFLICTS with the base branch" in context
156
+ assert "gh pr view 42 --json mergeable" in context
157
+ assert "gh pr edit 42 --add-label" not in context
158
+
159
+
160
+ def test_should_count_failing_and_pending_checks() -> None:
161
+ mixed_checks = {
162
+ **_CLEAN_PR_OBJECT,
163
+ "statusCheckRollup": [
164
+ {"name": "pytest", "status": "COMPLETED", "conclusion": "FAILURE"},
165
+ {"name": "mypy", "status": "IN_PROGRESS", "conclusion": ""},
166
+ {"context": "lint", "state": "PENDING"},
167
+ {"context": "docs", "state": "SUCCESS"},
168
+ ],
169
+ }
170
+
171
+ context = pr_done_reminder.build_reminder_context(mixed_checks)
172
+
173
+ assert "1 failing" in context
174
+ assert "2 pending" in context
175
+ assert "4 total" in context
176
+ assert "NOT DONE" in context
177
+
178
+
179
+ def test_should_tell_the_agent_to_wait_when_github_is_still_computing() -> None:
180
+ unknown = {**_CLEAN_PR_OBJECT, "mergeable": "UNKNOWN", "mergeStateStatus": "UNKNOWN"}
181
+
182
+ context = pr_done_reminder.build_reminder_context(unknown)
183
+
184
+ assert "still computing" in context
185
+ assert "NOT DONE" in context
186
+
187
+
188
+ def test_should_report_the_done_label_when_already_set() -> None:
189
+ labeled = {**_CLEAN_PR_OBJECT, "labels": [{"name": "done"}]}
190
+
191
+ context = pr_done_reminder.build_reminder_context(labeled)
192
+
193
+ assert "Label done: set" in context
194
+
195
+
196
+ def test_main_should_emit_additional_context_after_a_successful_push(
197
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
198
+ ) -> None:
199
+ stdout_text = _run_main(monkeypatch, capsys, _payload("git push"), _fake_gh(_CLEAN_PR_OBJECT))
200
+
201
+ payload = json.loads(stdout_text)
202
+ assert payload["hookSpecificOutput"]["hookEventName"] == "PostToolUse"
203
+ assert "permissionDecision" not in payload["hookSpecificOutput"]
204
+ assert "decision" not in payload
205
+ assert "PR #42" in _additional_context(stdout_text)
206
+
207
+
208
+ def test_main_should_also_serve_the_powershell_tool(
209
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
210
+ ) -> None:
211
+ payload = json.loads(_payload("git push"))
212
+ payload["tool_name"] = "PowerShell"
213
+
214
+ stdout_text = _run_main(monkeypatch, capsys, json.dumps(payload), _fake_gh(_CLEAN_PR_OBJECT))
215
+
216
+ assert "PR #42" in _additional_context(stdout_text)
217
+
218
+
219
+ def test_main_should_stay_quiet_when_the_push_failed(
220
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
221
+ ) -> None:
222
+ failed_push = _payload("git push", "Error: Exit code 1\nrejected")
223
+
224
+ assert _run_main(monkeypatch, capsys, failed_push, _fake_gh(_CLEAN_PR_OBJECT)) == ""
225
+
226
+
227
+ def test_main_should_stay_quiet_for_a_non_trigger_command(
228
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
229
+ ) -> None:
230
+ assert _run_main(monkeypatch, capsys, _payload("git status"), _fake_gh(_CLEAN_PR_OBJECT)) == ""
231
+
232
+
233
+ def test_main_should_remind_to_open_a_pr_when_the_branch_has_none(
234
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
235
+ ) -> None:
236
+ no_pr = _fake_gh(None, stderr='no pull requests found for branch "feat/x"')
237
+
238
+ context = _additional_context(_run_main(monkeypatch, capsys, _payload("git push"), no_pr))
239
+
240
+ assert "No open pull request found" in context
241
+ assert "gh pr create --draft" in context
242
+
243
+
244
+ def test_main_should_stay_quiet_on_any_other_gh_failure(
245
+ monkeypatch: pytest.MonkeyPatch, capsys: pytest.CaptureFixture[str]
246
+ ) -> None:
247
+ gh_down = _fake_gh(None, stderr="error connecting to api.github.com")
248
+
249
+ assert _run_main(monkeypatch, capsys, _payload("git push"), gh_down) == ""
@@ -14,9 +14,11 @@ collision, and needs no change to that shared, out-of-scope test file.
14
14
  Reads the tool payload from stdin once, selects the hosted hooks applicable to
15
15
  the payload's tool name, and runs each one in-process via the shared
16
16
  hosted-hook runner. A hosted PostToolUse hook here carries no permission
17
- decision -- it is a side-effecting observer, not a gate -- so this dispatcher
18
- runs the whole roster unconditionally and emits nothing itself, matching every
19
- hosted hook's own empty-stdout contract.
17
+ decision -- it is an observer, not a gate -- so this dispatcher runs the whole
18
+ roster unconditionally and never blocks. The one thing it forwards is context:
19
+ a hosted hook may print a ``hookSpecificOutput.additionalContext`` string (the
20
+ PR done reminder does), and the dispatcher joins every such string into one
21
+ PostToolUse payload. A hook that prints nothing adds nothing.
20
22
 
21
23
  A single hosted hook crash fails open: ``run_hook_capturing_output`` isolates
22
24
  it, so it contributes nothing and does not stop the remaining hosted hooks.
@@ -27,11 +29,17 @@ imports load once into this process rather than once per hook.
27
29
 
28
30
  from __future__ import annotations
29
31
 
32
+ import json
33
+ import sys
30
34
 
31
35
  import _path_setup # noqa: F401
32
36
 
33
37
  from hooks_constants.bash_post_call_dispatcher_constants import (
38
+ ADDITIONAL_CONTEXT_JOIN_SEPARATOR,
39
+ ADDITIONAL_CONTEXT_KEY,
34
40
  ALL_BASH_POST_TOOL_USE_HOSTED_HOOK_ENTRIES,
41
+ HOOK_SPECIFIC_OUTPUT_KEY,
42
+ POST_TOOL_USE_HOOK_EVENT_NAME,
35
43
  )
36
44
  from hooks_constants.bash_pre_tool_use_dispatcher_constants import BashHostedHookEntry
37
45
  from hooks_constants.hosted_hook_runner import (
@@ -50,15 +58,54 @@ def select_applicable_entries(tool_name: str) -> list[BashHostedHookEntry]:
50
58
  ]
51
59
 
52
60
 
61
+ def additional_context_from_hook_output(captured_stdout: str) -> str | None:
62
+ """Return the additionalContext string a hosted hook printed, or None.
63
+
64
+ Empty output, non-JSON output, and JSON without a non-empty
65
+ ``hookSpecificOutput.additionalContext`` string all return None.
66
+ """
67
+ if not captured_stdout.strip():
68
+ return None
69
+ try:
70
+ hook_output = json.loads(captured_stdout)
71
+ except json.JSONDecodeError:
72
+ return None
73
+ if not isinstance(hook_output, dict):
74
+ return None
75
+ hook_specific_output = hook_output.get(HOOK_SPECIFIC_OUTPUT_KEY)
76
+ if not isinstance(hook_specific_output, dict):
77
+ return None
78
+ additional_context = hook_specific_output.get(ADDITIONAL_CONTEXT_KEY)
79
+ if not isinstance(additional_context, str) or not additional_context.strip():
80
+ return None
81
+ return additional_context
82
+
83
+
53
84
  def dispatch(payload_text: str, tool_name: str) -> None:
54
- """Run every hosted hook applicable to tool_name, in registration order.
85
+ """Run every hosted hook applicable to tool_name, then forward their context.
55
86
 
56
87
  Each hosted hook's crash is isolated by ``run_hook_capturing_output``, so
57
- one hook failing never stops a later one in the same roster.
88
+ one hook failing never stops a later one in the same roster. Every
89
+ additionalContext string the hooks printed is joined into one PostToolUse
90
+ payload on stdout; when no hook printed one, nothing is written.
58
91
  """
92
+ all_additional_context: list[str] = []
59
93
  for each_entry in select_applicable_entries(tool_name):
60
94
  script_path = resolved_hook_script_path(each_entry.script_relative_path)
61
- run_hook_capturing_output(script_path, payload_text)
95
+ hook_run = run_hook_capturing_output(script_path, payload_text)
96
+ additional_context = additional_context_from_hook_output(hook_run.captured_stdout)
97
+ if additional_context is not None:
98
+ all_additional_context.append(additional_context)
99
+ if not all_additional_context:
100
+ return
101
+ payload = {
102
+ HOOK_SPECIFIC_OUTPUT_KEY: {
103
+ "hookEventName": POST_TOOL_USE_HOOK_EVENT_NAME,
104
+ ADDITIONAL_CONTEXT_KEY: ADDITIONAL_CONTEXT_JOIN_SEPARATOR.join(all_additional_context),
105
+ }
106
+ }
107
+ sys.stdout.write(json.dumps(payload))
108
+ sys.stdout.flush()
62
109
 
63
110
 
64
111
  def main() -> None: