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
@@ -1,487 +0,0 @@
1
- #!/usr/bin/env python3
2
- """PreToolUse hook: auto-switch the active gh CLI account to GITHUB_DEFAULT_ACCOUNT for `gh pr create`.
3
-
4
- Pinning every PR to a single canonical author makes the /bugteam and /qbug
5
- follow-up swap deterministic. Those skills refuse to post REQUEST_CHANGES
6
- reviews when the active gh CLI account matches the PR author (the GitHub
7
- API returns HTTP 422 — "cannot review own pull request"). When every PR
8
- has the same author, the swap step before bugteam is the same single
9
- command every time.
10
-
11
- Behavior:
12
- - No-op when the bash command does not invoke `gh pr create`.
13
- - No-op when `--web` / `-w` is present, since the browser flow does not
14
- create the PR via the gh CLI token.
15
- - No-op when GITHUB_DEFAULT_ACCOUNT is unset (other users without this
16
- workflow are unaffected).
17
- - No-op when the active gh account cannot be determined (gh missing,
18
- network failure) — defers to gh's own error path rather than blocking
19
- a command that may already be broken for other reasons.
20
- - No-op when the active gh account already equals GITHUB_DEFAULT_ACCOUNT.
21
- - Otherwise runs `gh auth switch --user <required>` silently and writes
22
- a per-session state file recording the original account. The PostToolUse
23
- companion (gh_pr_author_restore.py) reads that state file and swaps
24
- back after `gh pr create` finishes. On switch failure the hook falls
25
- back to the deny payload with the manual command.
26
- """
27
-
28
- from __future__ import annotations
29
-
30
- import json
31
- import os
32
- import subprocess
33
- import sys
34
- from pathlib import Path
35
-
36
- hooks_parent_directory = str(Path(__file__).resolve().parent.parent)
37
- if hooks_parent_directory not in sys.path:
38
- sys.path.insert(0, hooks_parent_directory)
39
-
40
- from _gh_pr_author_swap_utils import ( # noqa: E402
41
- _all_gh_pr_create_segments,
42
- _command_invokes_gh_pr_create_in_stripped,
43
- _delete_state_file,
44
- _preprocess_command_for_matching,
45
- _state_file_path,
46
- _strip_substitution_bodies,
47
- _switch_gh_account,
48
- _write_line,
49
- )
50
- from hooks_constants.gh_pr_author_swap_constants import (
51
- ALL_GH_API_USER_COMMAND,
52
- BASH_TOOL_NAME,
53
- GH_API_USER_TIMEOUT_SECONDS,
54
- OS_O_NOFOLLOW_ATTRIBUTE_NAME,
55
- REQUIRED_ACCOUNT_ENV_VAR,
56
- STATE_FILE_ORIGINAL_ACCOUNT_KEY,
57
- STATE_FILE_PAYLOAD_TEXT_ENCODING_NAME,
58
- STATE_FILE_PERMISSION_MODE,
59
- STATE_FILE_PRIMARY_ACCOUNT_KEY,
60
- WEB_FLAG_PATTERN,
61
- )
62
- from hooks_constants.hook_block_logger import log_hook_block # noqa: E402
63
-
64
-
65
- def _active_gh_account() -> str | None:
66
- """Return the login of the active gh CLI account, or None when undetermined.
67
-
68
- Returns:
69
- The login string from ``gh api user --jq .login`` on success.
70
- None when gh is missing, the gh binary lacks executable permission,
71
- the command fails, times out, or returns an empty value.
72
- ``OSError`` covers every spawn-time failure
73
- (``FileNotFoundError`` when gh is absent, ``PermissionError``
74
- when gh exists but is not executable, and any other
75
- platform-specific spawn errors) so the hook follows its
76
- documented "skip the check" failure path rather than crashing.
77
- The caller treats None as "skip the check."
78
- """
79
- try:
80
- completed_process = subprocess.run(
81
- list(ALL_GH_API_USER_COMMAND),
82
- capture_output=True,
83
- text=True,
84
- timeout=GH_API_USER_TIMEOUT_SECONDS,
85
- check=False,
86
- )
87
- except (OSError, subprocess.SubprocessError):
88
- return None
89
- if completed_process.returncode != 0:
90
- return None
91
- stripped_login = completed_process.stdout.strip()
92
- return stripped_login or None
93
-
94
-
95
- def _write_swap_state(
96
- state_file: Path,
97
- original_account: str,
98
- primary_account: str,
99
- ) -> bool:
100
- """Persist the swap-back state for the PostToolUse restore hook.
101
-
102
- The state file is created atomically with ``os.open`` using
103
- ``O_WRONLY | O_CREAT | O_EXCL`` (plus ``O_NOFOLLOW`` on platforms
104
- that expose it) so an attacker on a shared POSIX workstation cannot
105
- pre-create the predictable path as a symlink pointing at an
106
- arbitrary writable file. The mode bits are set at create time so
107
- the file is never momentarily world-readable between ``open`` and
108
- ``chmod``. A defense-in-depth ``chmod`` call follows the write in
109
- case the platform's umask honored the ``mode`` argument differently
110
- than expected.
111
-
112
- A stale file left by a crashed prior session can collide with the
113
- ``O_EXCL`` guard. The function unlinks such a file and retries the
114
- create exactly once; a second collision is treated as a write
115
- failure so the caller does not silently overwrite something it did
116
- not create.
117
-
118
- A failure after a successful write unlinks the partially-written
119
- file via ``_delete_state_file`` before returning False so the caller
120
- does not leave a world-readable state file behind for the
121
- SessionStart cleanup hook to later pick up and trigger an unexpected
122
- ``gh auth switch``.
123
-
124
- Every ``os.close`` call is guarded by ``try``/``except OSError``
125
- because delayed-writeback filesystems (NFS, FUSE) can surface a
126
- write error at close time rather than at write time. On the
127
- post-successful-write branch, an ``OSError`` from ``os.close`` is
128
- treated as a write failure: the file is unlinked and False is
129
- returned so the caller rolls back the gh auth switch. On the
130
- partial-write failure branch the file is already being unlinked,
131
- so an ``OSError`` from ``os.close`` is swallowed — re-raising
132
- would crash the hook mid-rollback.
133
-
134
- Args:
135
- state_file: Destination path returned by ``_state_file_path``.
136
- original_account: Login that was active before the swap.
137
- primary_account: Login swapped to (always ``GITHUB_DEFAULT_ACCOUNT``).
138
-
139
- Returns:
140
- True when the atomic create, write, and chmod all succeed.
141
- False on any filesystem failure. A failure at any stage unlinks
142
- any partially-written file so the caller does not leave a
143
- world-readable state file behind.
144
- """
145
- swap_state = {
146
- STATE_FILE_ORIGINAL_ACCOUNT_KEY: original_account,
147
- STATE_FILE_PRIMARY_ACCOUNT_KEY: primary_account,
148
- }
149
- serialized_payload = json.dumps(swap_state).encode(STATE_FILE_PAYLOAD_TEXT_ENCODING_NAME)
150
- open_flags = os.O_WRONLY | os.O_CREAT | os.O_EXCL
151
- if hasattr(os, OS_O_NOFOLLOW_ATTRIBUTE_NAME):
152
- open_flags |= getattr(os, OS_O_NOFOLLOW_ATTRIBUTE_NAME)
153
- file_descriptor = _open_state_file_with_retry(state_file, open_flags)
154
- if file_descriptor is None:
155
- return False
156
- if not _write_payload_completely(file_descriptor, serialized_payload):
157
- try:
158
- os.close(file_descriptor)
159
- except OSError:
160
- pass
161
- _delete_state_file(state_file)
162
- return False
163
- try:
164
- os.close(file_descriptor)
165
- except OSError:
166
- _delete_state_file(state_file)
167
- return False
168
- try:
169
- os.chmod(state_file, STATE_FILE_PERMISSION_MODE)
170
- except OSError:
171
- _delete_state_file(state_file)
172
- return False
173
- return True
174
-
175
-
176
- def _write_payload_completely(file_descriptor: int, serialized_payload: bytes) -> bool:
177
- """Write every byte of ``serialized_payload`` to ``file_descriptor``.
178
-
179
- ``os.write`` is documented to potentially write fewer bytes than
180
- requested, especially on pipes and non-blocking descriptors. The
181
- state file is opened in blocking mode, but the contract holds — a
182
- signal arriving mid-write can cut a write short, and partial writes
183
- have been observed across NFS mounts and FUSE filesystems. Treating
184
- the first ``os.write`` return value as authoritative would leave a
185
- truncated JSON state file on disk; the restore hook would then
186
- parse the truncated file, log "malformed state file", delete it,
187
- and leave the active gh CLI account stranded on the canonical
188
- author instead of restoring the original account.
189
-
190
- The loop reissues ``os.write`` against a slice of the payload that
191
- starts at the byte count already written, until every byte has been
192
- emitted. A return value of zero from ``os.write`` indicates the
193
- underlying file descriptor cannot accept any more bytes and is
194
- treated as a write failure so the caller can roll back rather than
195
- spin forever.
196
-
197
- Args:
198
- file_descriptor: Open file descriptor returned by ``os.open``.
199
- The caller retains ownership and is responsible for closing
200
- the descriptor whether this function returns True or False.
201
- serialized_payload: Encoded JSON payload to write. The caller
202
- encodes via ``STATE_FILE_PAYLOAD_TEXT_ENCODING_NAME`` before
203
- invoking this helper.
204
-
205
- Returns:
206
- True when every byte of ``serialized_payload`` was written.
207
- False on any ``OSError`` raised by ``os.write`` or when
208
- ``os.write`` returns zero before the payload is complete.
209
- """
210
- payload_length = len(serialized_payload)
211
- bytes_already_written = 0
212
- while bytes_already_written < payload_length:
213
- try:
214
- bytes_just_written = os.write(
215
- file_descriptor,
216
- serialized_payload[bytes_already_written:],
217
- )
218
- except OSError:
219
- return False
220
- if bytes_just_written == 0:
221
- return False
222
- bytes_already_written += bytes_just_written
223
- return True
224
-
225
-
226
- def _open_state_file_with_retry(state_file: Path, open_flags: int) -> int | None:
227
- """Open the state file atomically, unlinking a stale collision once.
228
-
229
- The enforcer can race against a state file left behind by a prior
230
- crashed session at the same predictable path. The first ``O_EXCL``
231
- open raises ``FileExistsError`` in that case; the function unlinks
232
- the stale file and retries exactly once. A second ``FileExistsError``
233
- is treated as a genuine race against a concurrent process and
234
- surfaces as ``None`` so the caller can fall back to its deny path.
235
-
236
- Args:
237
- state_file: Destination path returned by ``_state_file_path``.
238
- open_flags: Bitmask passed to ``os.open`` — must include
239
- ``O_EXCL`` so this retry logic can distinguish "stale file
240
- collision" from "wrote a fresh file".
241
-
242
- Returns:
243
- A file descriptor on success. ``None`` when both the initial
244
- open and the post-unlink retry fail.
245
- """
246
- try:
247
- return os.open(state_file, open_flags, STATE_FILE_PERMISSION_MODE)
248
- except FileExistsError:
249
- try:
250
- state_file.unlink()
251
- except OSError:
252
- return None
253
- except OSError:
254
- return None
255
- try:
256
- return os.open(state_file, open_flags, STATE_FILE_PERMISSION_MODE)
257
- except OSError:
258
- return None
259
-
260
-
261
- def _build_switch_failure_message(required_account: str, current_account: str) -> str:
262
- """Build the deny reason emitted when the silent auto-switch fails.
263
-
264
- Args:
265
- required_account: Value of GITHUB_DEFAULT_ACCOUNT.
266
- current_account: Login returned by gh before the failed switch.
267
-
268
- Returns:
269
- A multi-line corrective message naming both accounts and giving
270
- the exact ``gh auth switch`` command the user should run.
271
- """
272
- return (
273
- f"BLOCKED [gh-pr-author]: tried to auto-switch the active gh CLI "
274
- f"account from `{current_account}` to `{required_account}` so "
275
- f"`gh pr create` would author from the canonical account, but "
276
- f"`gh auth switch` failed.\n\n"
277
- f" Current: {current_account}\n"
278
- f" Required: {required_account} (from ${REQUIRED_ACCOUNT_ENV_VAR})\n\n"
279
- f"Run first:\n"
280
- f" gh auth switch --user {required_account}\n\n"
281
- f"If you genuinely want to author this PR from a different account "
282
- f"in this one case, switch to that account and retry. To create the "
283
- f"PR through the browser instead (uses your browser's GitHub session, "
284
- f"not the gh CLI token), add `--web`."
285
- )
286
-
287
-
288
- def _build_state_write_failure_message(
289
- required_account: str,
290
- current_account: str,
291
- state_file: Path,
292
- has_rollback_succeeded: bool,
293
- ) -> str:
294
- """Build the deny reason emitted when state-file persistence fails after a successful swap.
295
-
296
- Args:
297
- required_account: Value of GITHUB_DEFAULT_ACCOUNT (the swap target).
298
- current_account: Login that was active before the swap (the
299
- restore target the failed state file should have recorded).
300
- state_file: Path the enforcer tried and failed to write.
301
- has_rollback_succeeded: True when the reverse ``gh auth switch``
302
- back to ``current_account`` returned zero, so the user is
303
- on the original account again. False when the reverse switch
304
- also failed and the user is still on ``required_account``.
305
-
306
- Returns:
307
- A multi-line corrective message explaining the swap and
308
- rollback outcome. The lead-in sentence describes the actual
309
- state — "the swap was reversed" when the rollback succeeded,
310
- "the reverse-switch also failed and you are still on
311
- ``required_account``" when it did not. The trailing
312
- ``gh auth status`` / ``gh auth switch`` recovery commands are
313
- emitted in both branches so the user can verify and recover
314
- regardless of where the gh CLI ended up.
315
- """
316
- if has_rollback_succeeded:
317
- rollback_outcome_sentence = (
318
- f"The swap was reversed to put `{current_account}` back in place, "
319
- f"and `gh pr create` is being denied to prevent leaving the "
320
- f"workflow in an inconsistent state."
321
- )
322
- else:
323
- rollback_outcome_sentence = (
324
- f"The reverse `gh auth switch` to put `{current_account}` back "
325
- f"in place ALSO failed, so the active gh CLI account is still "
326
- f"`{required_account}`. `gh pr create` is being denied so the "
327
- f"user can recover the original account before re-running."
328
- )
329
- return (
330
- f"BLOCKED [gh-pr-author]: swapped the active gh CLI account "
331
- f"from `{current_account}` to `{required_account}` so "
332
- f"`gh pr create` would author from the canonical account, but "
333
- f"writing the per-session state file used to restore the prior "
334
- f"account afterward failed. {rollback_outcome_sentence}\n\n"
335
- f" Original: {current_account}\n"
336
- f" Required: {required_account} (from ${REQUIRED_ACCOUNT_ENV_VAR})\n"
337
- f" State file (failed): {state_file}\n\n"
338
- f"Verify the active account and recover manually:\n"
339
- f" gh auth status\n"
340
- f" gh auth switch --user {current_account}\n\n"
341
- f"Then re-run `gh pr create` so the enforcer can retry the swap."
342
- )
343
-
344
-
345
- def _command_uses_web_flag_in_stripped(preprocessed_command: str) -> bool:
346
- """Return True when EVERY ``gh pr create`` segment uses ``--web`` / ``-w``.
347
-
348
- The flag is only relevant when it modifies the ``gh pr create``
349
- invocation itself. A ``-w`` token belonging to an unrelated command
350
- (for example ``curl -w '%{http_code}'``) before ``gh pr create``, or
351
- a flag attached to a chained command after a separator like ``&&`` /
352
- ``||`` / ``;`` / ``|`` / newline, must not flip the enforcer into
353
- the browser-flow no-op path. A ``-w`` sitting inside a quoted
354
- argument (for example ``--body "see -w docs"``) likewise must not
355
- match — the caller blanks those out via
356
- ``_preprocess_command_for_matching`` before passing the command in
357
- here.
358
-
359
- A ``--web`` token sitting inside a substitution body (for example
360
- ``gh pr create --title "$(echo --web)"``) is an argument to the
361
- subshell command, not a flag on the outer ``gh pr create`` —
362
- ``_strip_substitution_bodies`` blanks the substitution before the
363
- segment search so the false-positive does not skip the swap. The
364
- safety bias is intentional: a substitution that genuinely expands
365
- to ``--web`` is now treated as a non-web invocation and still
366
- triggers the account swap, which is harmless because ``gh pr create``
367
- with both ``--web`` and the canonical author swapped in just runs
368
- the browser flow.
369
-
370
- When the command chains multiple ``gh pr create`` invocations
371
- (``gh pr create --web && gh pr create --title T``), the enforcer
372
- must trigger as long as ANY of them omits the web flag — otherwise
373
- the second invocation would slip through under the active account.
374
- A short-circuiting ``all()`` over every segment gives that
375
- "browser-flow only when EVERY segment opts in" semantics.
376
-
377
- Args:
378
- preprocessed_command: Output of ``_preprocess_command_for_matching`` —
379
- the caller is responsible for blanking inert quoted regions
380
- and bash comments before passing in. ``main()`` computes
381
- this once and passes it to both this helper and
382
- ``_command_invokes_gh_pr_create_in_stripped`` so the
383
- character-walk preprocessing runs exactly once per command.
384
-
385
- Returns:
386
- True when every ``gh pr create`` segment in the preprocessed
387
- command carries ``--web`` or ``-w`` as a whole token. False
388
- when ``gh pr create`` is absent, or when any segment lacks the
389
- flag (including segments whose only ``--web`` token sat inside
390
- a substitution body that has now been blanked).
391
- """
392
- all_gh_pr_create_segments = _all_gh_pr_create_segments(preprocessed_command)
393
- if not all_gh_pr_create_segments:
394
- return False
395
- return all(
396
- bool(WEB_FLAG_PATTERN.search(_strip_substitution_bodies(each_segment)))
397
- for each_segment in all_gh_pr_create_segments
398
- )
399
-
400
-
401
- def _emit_deny_payload(reason_text: str) -> None:
402
- """Write the JSON deny payload to stdout for Claude Code to consume.
403
-
404
- Args:
405
- reason_text: User-facing explanation displayed by Claude Code.
406
- """
407
- deny_payload = {
408
- "hookSpecificOutput": {
409
- "hookEventName": "PreToolUse",
410
- "permissionDecision": "deny",
411
- "permissionDecisionReason": reason_text,
412
- }
413
- }
414
- log_hook_block(
415
- calling_hook_name="gh_pr_author_enforcer.py",
416
- hook_event="PreToolUse",
417
- block_reason=reason_text,
418
- tool_name=BASH_TOOL_NAME,
419
- )
420
- _write_line(json.dumps(deny_payload), sys.stdout)
421
-
422
-
423
- def main() -> None:
424
- """Read PreToolUse hook input on stdin and auto-switch the gh account when warranted.
425
-
426
- Exits 0 in all paths. On the silent-switch success path no output is
427
- produced. On switch-failure the JSON deny payload is written to
428
- stdout. On every no-op condition nothing is written.
429
- """
430
- try:
431
- hook_input = json.load(sys.stdin)
432
- except json.JSONDecodeError:
433
- sys.exit(0)
434
-
435
- if hook_input.get("tool_name") != BASH_TOOL_NAME:
436
- sys.exit(0)
437
-
438
- command = hook_input.get("tool_input", {}).get("command", "")
439
- if not command:
440
- sys.exit(0)
441
-
442
- preprocessed_command = _preprocess_command_for_matching(command)
443
- if not _command_invokes_gh_pr_create_in_stripped(preprocessed_command):
444
- sys.exit(0)
445
-
446
- if _command_uses_web_flag_in_stripped(preprocessed_command):
447
- sys.exit(0)
448
-
449
- required_account = os.environ.get(REQUIRED_ACCOUNT_ENV_VAR, "").strip()
450
- if not required_account:
451
- sys.exit(0)
452
-
453
- current_account = _active_gh_account()
454
- if current_account is None:
455
- sys.exit(0)
456
- if current_account.casefold() == required_account.casefold():
457
- sys.exit(0)
458
-
459
- has_switched_account = _switch_gh_account(required_account)
460
- if not has_switched_account:
461
- _emit_deny_payload(
462
- _build_switch_failure_message(required_account, current_account)
463
- )
464
- sys.exit(0)
465
-
466
- session_id = str(hook_input.get("session_id") or "")
467
- state_file = _state_file_path(session_id)
468
- has_written_state = _write_swap_state(
469
- state_file,
470
- original_account=current_account,
471
- primary_account=required_account,
472
- )
473
- if not has_written_state:
474
- has_rollback_succeeded = _switch_gh_account(current_account)
475
- _emit_deny_payload(
476
- _build_state_write_failure_message(
477
- required_account,
478
- current_account,
479
- state_file,
480
- has_rollback_succeeded,
481
- )
482
- )
483
- sys.exit(0)
484
-
485
-
486
- if __name__ == "__main__":
487
- main()
@@ -1,105 +0,0 @@
1
- #!/usr/bin/env python3
2
- """PostToolUse hook: restore the prior gh CLI account after `gh pr create` runs.
3
-
4
- Companion to ``gh_pr_author_enforcer.py``. When the PreToolUse enforcer
5
- silently swaps the active gh account to ``GITHUB_DEFAULT_ACCOUNT`` and
6
- records the original account in a per-session state file, this hook
7
- reads that state file after the matching Bash invocation finishes and
8
- runs ``gh auth switch --user <original>`` to put the prior account back
9
- in place.
10
-
11
- The state file is deleted only when the restore switch succeeds. If
12
- ``gh auth switch`` fails the state file is left in place so the
13
- SessionStart cleanup hook (``gh_pr_author_session_cleanup.py``) can
14
- retry on the next session start instead of stranding the user on the
15
- canonical author account.
16
-
17
- Behavior:
18
- - No-op when tool_name is not Bash.
19
- - No-op when the command did not invoke ``gh pr create`` (uses the same
20
- regex as the enforcer so the pair stays in sync).
21
- - No-op when no per-session state file exists — means the enforcer
22
- never swapped on this command.
23
- - Otherwise reads the state file, runs ``gh auth switch --user <original>``,
24
- and deletes the state file only when the switch succeeded. Failures
25
- are logged to stderr; this hook never blocks the workflow.
26
- """
27
-
28
- from __future__ import annotations
29
-
30
- import json
31
- import sys
32
- from pathlib import Path
33
-
34
- hooks_parent_directory = str(Path(__file__).resolve().parent.parent)
35
- if hooks_parent_directory not in sys.path:
36
- sys.path.insert(0, hooks_parent_directory)
37
-
38
- from _gh_pr_author_swap_utils import ( # noqa: E402
39
- _build_restore_failure_message,
40
- _command_invokes_gh_pr_create_in_stripped,
41
- _delete_state_file,
42
- _preprocess_command_for_matching,
43
- _read_original_account,
44
- _state_file_is_attacker_planted,
45
- _state_file_path,
46
- _switch_gh_account,
47
- _write_line,
48
- )
49
- from hooks_constants.gh_pr_author_swap_constants import BASH_TOOL_NAME # noqa: E402
50
-
51
-
52
- def main() -> None:
53
- """Read PostToolUse hook input on stdin and restore the prior gh account.
54
-
55
- Exits 0 in every path. Errors are logged to stderr only — this hook
56
- must never block subsequent commands.
57
- """
58
- try:
59
- hook_input = json.load(sys.stdin)
60
- except json.JSONDecodeError:
61
- sys.exit(0)
62
-
63
- if hook_input.get("tool_name") != BASH_TOOL_NAME:
64
- sys.exit(0)
65
-
66
- command = hook_input.get("tool_input", {}).get("command", "")
67
- if not command:
68
- sys.exit(0)
69
- preprocessed_command = _preprocess_command_for_matching(command)
70
- if not _command_invokes_gh_pr_create_in_stripped(preprocessed_command):
71
- sys.exit(0)
72
-
73
- session_id = str(hook_input.get("session_id") or "")
74
- state_file = _state_file_path(session_id)
75
- if _state_file_is_attacker_planted(state_file):
76
- _write_line(
77
- f"[gh-pr-author-restore] state file at {state_file} has unexpected mode/owner; "
78
- f"skipping restore and preserving file for inspection",
79
- sys.stderr,
80
- )
81
- sys.exit(0)
82
- original_account = _read_original_account(state_file)
83
- if original_account is None:
84
- if state_file.exists():
85
- _delete_state_file(state_file)
86
- sys.exit(0)
87
-
88
- has_restored_account = _switch_gh_account(original_account)
89
- if has_restored_account:
90
- _delete_state_file(state_file)
91
- else:
92
- _write_line(
93
- _build_restore_failure_message(
94
- "gh-pr-author-restore",
95
- original_account,
96
- state_file,
97
- "state file",
98
- ),
99
- sys.stderr,
100
- )
101
- sys.exit(0)
102
-
103
-
104
- if __name__ == "__main__":
105
- main()