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,234 +0,0 @@
1
- """Unit tests for the pr-description-writer PreToolUse gate on gh pr create."""
2
-
3
- from __future__ import annotations
4
-
5
- import importlib.util
6
- import io
7
- import json
8
- import sys
9
- from pathlib import Path
10
- from unittest import mock
11
-
12
- import pytest
13
-
14
- _BLOCKING_DIRECTORY = Path(__file__).resolve().parent
15
- _HOOKS_DIRECTORY = _BLOCKING_DIRECTORY.parent
16
- for each_directory in (_BLOCKING_DIRECTORY, _HOOKS_DIRECTORY):
17
- if str(each_directory) not in sys.path:
18
- sys.path.insert(0, str(each_directory))
19
-
20
- _GATE_SPEC = importlib.util.spec_from_file_location(
21
- "pr_description_writer_gate",
22
- _BLOCKING_DIRECTORY / "pr_description_writer_gate.py",
23
- )
24
- assert _GATE_SPEC is not None
25
- assert _GATE_SPEC.loader is not None
26
- gate_module = importlib.util.module_from_spec(_GATE_SPEC)
27
- _GATE_SPEC.loader.exec_module(gate_module)
28
-
29
- _GATE_CONSTANTS = importlib.import_module(
30
- "hooks_constants.pr_description_writer_gate_constants"
31
- )
32
- _PR_DESCRIPTION_WRITER_SUBAGENT_TYPE = _GATE_CONSTANTS.PR_DESCRIPTION_WRITER_SUBAGENT_TYPE
33
-
34
- _SESSION_ID = "session-abc123"
35
- _PLAIN_CREATE_COMMAND = 'gh pr create --draft --title "feat: add it" --body-file body.md'
36
-
37
-
38
- def _payload(command: str, tool_name: str = "Bash") -> dict[str, object]:
39
- """Build a PreToolUse payload carrying one shell command.
40
-
41
- Args:
42
- command: The shell command the gate inspects.
43
- tool_name: The tool the command runs through.
44
-
45
- Returns:
46
- The payload dictionary the gate reads from stdin.
47
- """
48
- return {
49
- "tool_name": tool_name,
50
- "session_id": _SESSION_ID,
51
- "tool_input": {"command": command},
52
- }
53
-
54
-
55
- def _printed_output_for(command: str, temp_directory: Path, tool_name: str = "Bash") -> str:
56
- """Run the gate against one command and return everything it printed.
57
-
58
- Args:
59
- command: The shell command the gate inspects.
60
- temp_directory: The directory standing in for the system temp root.
61
- tool_name: The tool the command runs through.
62
-
63
- Returns:
64
- The gate's stripped stdout — empty on an allow.
65
- """
66
- captured_stdout = io.StringIO()
67
- payload_text = json.dumps(_payload(command, tool_name))
68
- with (
69
- mock.patch("sys.stdin", io.StringIO(payload_text)),
70
- mock.patch("sys.stdout", captured_stdout),
71
- mock.patch.object(_GATE_CONSTANTS.tempfile, "gettempdir", return_value=str(temp_directory)),
72
- pytest.raises(SystemExit),
73
- ):
74
- gate_module.main()
75
- return captured_stdout.getvalue().strip()
76
-
77
-
78
- def _decision_for(command: str, temp_directory: Path, tool_name: str = "Bash") -> str:
79
- """Return the gate's permission decision for one command.
80
-
81
- Args:
82
- command: The shell command the gate inspects.
83
- temp_directory: The directory standing in for the system temp root.
84
- tool_name: The tool the command runs through.
85
-
86
- Returns:
87
- The ``permissionDecision`` string, or an empty string on an allow.
88
- """
89
- printed_text = _printed_output_for(command, temp_directory, tool_name)
90
- if not printed_text:
91
- return ""
92
- hook_output = json.loads(printed_text).get("hookSpecificOutput", {})
93
- return str(hook_output.get("permissionDecision", ""))
94
-
95
-
96
- def _record_spawn(temp_directory: Path, session_id: str = _SESSION_ID) -> None:
97
- """Write the marker file that stands for a recorded agent spawn.
98
-
99
- Args:
100
- temp_directory: The directory standing in for the system temp root.
101
- """
102
- marker_name = (
103
- f"{_GATE_CONSTANTS.SPAWN_MARKER_FILE_PREFIX}{session_id}"
104
- f"{_GATE_CONSTANTS.SPAWN_MARKER_FILE_SUFFIX}"
105
- )
106
- (temp_directory / marker_name).touch()
107
-
108
-
109
- def should_deny_pr_create_with_no_recorded_spawn(tmp_path: Path) -> None:
110
- """A gh pr create with no recorded spawn is denied."""
111
- assert _decision_for(_PLAIN_CREATE_COMMAND, tmp_path) == "deny"
112
-
113
-
114
- def should_allow_pr_create_after_a_recorded_spawn(tmp_path: Path) -> None:
115
- """A gh pr create is allowed once this session recorded a spawn."""
116
- _record_spawn(tmp_path)
117
- assert _decision_for(_PLAIN_CREATE_COMMAND, tmp_path) == ""
118
-
119
-
120
- def should_allow_pr_create_carrying_the_bypass_marker(tmp_path: Path) -> None:
121
- """A trailing bypass comment allows the create with no recorded spawn."""
122
- command = f"{_PLAIN_CREATE_COMMAND} {gate_module.SPAWN_BYPASS_MARKER}"
123
- assert _decision_for(command, tmp_path) == ""
124
-
125
-
126
- def should_allow_multiline_pr_create_carrying_the_bypass_marker(tmp_path: Path) -> None:
127
- """A final bypass comment allows a create after a continued shell line."""
128
- command = f"cd . && \\\n+{_PLAIN_CREATE_COMMAND} {gate_module.SPAWN_BYPASS_MARKER}"
129
- assert _decision_for(command, tmp_path) == ""
130
-
131
-
132
- def should_deny_when_the_bypass_marker_follows_another_shell_segment(tmp_path: Path) -> None:
133
- """A final marker for echo cannot opt a preceding create out."""
134
- command = f"{_PLAIN_CREATE_COMMAND} && echo {gate_module.SPAWN_BYPASS_MARKER}"
135
- assert _decision_for(command, tmp_path) == "deny"
136
-
137
-
138
- def should_deny_when_only_another_session_has_a_spawn_marker(tmp_path: Path) -> None:
139
- """A marker from another session cannot unlock this session's create."""
140
- _record_spawn(tmp_path, session_id="earlier-session")
141
- assert _decision_for(_PLAIN_CREATE_COMMAND, tmp_path) == "deny"
142
-
143
-
144
- def should_deny_when_the_marker_text_sits_inside_a_quoted_value(tmp_path: Path) -> None:
145
- """The bypass text inside a quoted title leaves the gate running."""
146
- command = 'gh pr create --title "fix # pr-description-skip" --body-file body.md'
147
- assert _decision_for(command, tmp_path) == "deny"
148
-
149
-
150
- def should_deny_a_pr_create_that_follows_another_command(tmp_path: Path) -> None:
151
- """A create in a later command segment is still gated."""
152
- command = f"cd /repo && {_PLAIN_CREATE_COMMAND}"
153
- assert _decision_for(command, tmp_path) == "deny"
154
-
155
-
156
- @pytest.mark.parametrize("shell_tool_name", ["Bash", "PowerShell"])
157
- def should_gate_each_shell_tool(shell_tool_name: str, tmp_path: Path) -> None:
158
- """Both shell tools reach the gate."""
159
- decision = _decision_for(_PLAIN_CREATE_COMMAND, tmp_path, tool_name=shell_tool_name)
160
- assert decision == "deny"
161
-
162
-
163
- @pytest.mark.parametrize(
164
- "unrelated_command",
165
- [
166
- "gh pr edit 12 --body-file body.md",
167
- "gh pr comment 12 --body-file body.md",
168
- "gh pr ready 12",
169
- "gh issue create --title x --body-file body.md",
170
- "git commit -m x",
171
- "echo gh pr create",
172
- ],
173
- )
174
- def should_stay_quiet_for_a_command_that_creates_no_pull_request(
175
- unrelated_command: str, tmp_path: Path
176
- ) -> None:
177
- """A command other than gh pr create passes untouched."""
178
- assert _decision_for(unrelated_command, tmp_path) == ""
179
-
180
-
181
- def should_stay_quiet_for_a_tool_that_runs_no_shell(tmp_path: Path) -> None:
182
- """A non-shell tool carrying the same text passes untouched."""
183
- assert _decision_for(_PLAIN_CREATE_COMMAND, tmp_path, tool_name="Write") == ""
184
-
185
-
186
- def should_deny_when_the_payload_names_no_session(tmp_path: Path) -> None:
187
- """A payload with no session id reads as no record and denies.
188
-
189
- A shared fallback marker would let one session's spawn silently clear a
190
- later session's create, so an unusable session id denies instead.
191
- """
192
- _record_spawn(tmp_path)
193
- captured_stdout = io.StringIO()
194
- payload = _payload(_PLAIN_CREATE_COMMAND)
195
- payload["session_id"] = ""
196
- with (
197
- mock.patch("sys.stdin", io.StringIO(json.dumps(payload))),
198
- mock.patch("sys.stdout", captured_stdout),
199
- mock.patch.object(_GATE_CONSTANTS.tempfile, "gettempdir", return_value=str(tmp_path)),
200
- pytest.raises(SystemExit),
201
- ):
202
- gate_module.main()
203
- hook_output = json.loads(captured_stdout.getvalue().strip())["hookSpecificOutput"]
204
- assert hook_output["permissionDecision"] == "deny"
205
-
206
-
207
- def should_allow_an_untokenizable_command(tmp_path: Path) -> None:
208
- """A command that will not tokenize fails open."""
209
- assert _decision_for('gh pr create --title "unclosed', tmp_path) == ""
210
-
211
-
212
- def should_name_both_ways_forward_in_the_denial(tmp_path: Path) -> None:
213
- """The denial text names the agent spawn and the bypass marker."""
214
- printed_text = _printed_output_for(_PLAIN_CREATE_COMMAND, tmp_path)
215
- denial_reason = json.loads(printed_text)["hookSpecificOutput"]["permissionDecisionReason"]
216
- assert _PR_DESCRIPTION_WRITER_SUBAGENT_TYPE in denial_reason
217
- assert gate_module.SPAWN_BYPASS_MARKER in denial_reason
218
-
219
-
220
- def should_read_the_marker_path_the_shared_builder_produces(tmp_path: Path) -> None:
221
- """The gate reads exactly the file the shared path builder names.
222
-
223
- The tracker writes through `spawn_marker_path` and the gate reads through it,
224
- so neither side owns a private copy of the marker file name.
225
- """
226
- marker_file = _GATE_CONSTANTS.spawn_marker_path(_SESSION_ID, temp_directory=tmp_path)
227
- assert marker_file is not None
228
- marker_file.touch()
229
- assert _decision_for(_PLAIN_CREATE_COMMAND, tmp_path) == ""
230
-
231
-
232
- def should_deny_when_the_session_id_names_no_marker(tmp_path: Path) -> None:
233
- """A session id that sanitizes to nothing yields no path, so the gate denies."""
234
- assert _GATE_CONSTANTS.spawn_marker_path("", temp_directory=tmp_path) is None
@@ -1,343 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- PreToolUse:Bash hook that validates prerequisites before running test commands.
4
-
5
- Intercepts playwright/pytest commands and checks:
6
- 1. Target server is reachable and healthy
7
- 2. Database file exists (for Django projects)
8
- 3. (Playwright only) Django server has --test-db flag
9
- 4. (Playwright only) Frontend builds successfully before e2e tests
10
-
11
- Blocks doomed test runs early instead of letting them hang for minutes.
12
- """
13
- import json
14
- import os
15
- import re
16
- import subprocess
17
- import sys
18
- from urllib.parse import urlparse
19
-
20
- try:
21
- import psutil
22
- except Exception:
23
- psutil = None
24
-
25
- TEST_COMMAND_PATTERNS = [
26
- re.compile(r'\bplaywright\s+test\b'),
27
- re.compile(r'\bnpx\s+playwright\b'),
28
- re.compile(r'\bpytest\b'),
29
- re.compile(r'\bpython\s+-m\s+pytest\b'),
30
- ]
31
-
32
- SERVER_URL_PATTERN = re.compile(r'https?://[^\s"\']+')
33
-
34
- DEFAULT_PLAYWRIGHT_URL = "http://localhost:3000"
35
- DEFAULT_DJANGO_URL = "http://localhost:8000"
36
-
37
- CURL_TIMEOUT_SECONDS = 2
38
- DJANGO_DB_FILENAME = "db.sqlite3"
39
-
40
- BLOCKED_STATUS_CODES = {500, 502, 503, 504}
41
- HEALTH_CHECK_ERROR_TEMPLATE = "BLOCKED: Server at {} is not healthy ({}). Fix the server before running tests."
42
- UNREACHABLE_ERROR_TEMPLATE = "BLOCKED: Server at {} is unreachable. Start the server before running tests."
43
- MISSING_DB_ERROR_TEMPLATE = "BLOCKED: No database file ({}) found in {}. Run migrations before running tests."
44
- FRONTEND_BUILD_FAILED_MESSAGE = "BLOCKED: Frontend build failed. Fix build errors before running e2e tests."
45
- MISSING_TEST_DB_FLAG_TEMPLATE = "BLOCKED: Django server on port {} is not running with --test-db. Restart with: python manage.py runserver --test-db 0.0.0.0:{}"
46
- PORT_CONFLICT_ERROR_TEMPLATE = "BLOCKED: Multiple Django runserver processes are bound to port {} across worktrees: {}. Stop stale servers first."
47
- FRONTEND_DIRECTORY_NAME = "frontend"
48
- NPM_BUILD_COMMAND = "npm run build"
49
- COLLECTSTATIC_COMMAND = "python manage.py collectstatic --noinput"
50
- BUILD_TIMEOUT_SECONDS = 120
51
- PLAYWRIGHT_COMMAND_PATTERNS = [
52
- re.compile(r'\bplaywright\s+test\b'),
53
- re.compile(r'\bnpx\s+playwright\b'),
54
- ]
55
-
56
-
57
- def is_test_command(command: str) -> bool:
58
- for each_pattern in TEST_COMMAND_PATTERNS:
59
- if each_pattern.search(command):
60
- return True
61
- return False
62
-
63
-
64
- def is_playwright_command(command: str) -> bool:
65
- for each_pattern in PLAYWRIGHT_COMMAND_PATTERNS:
66
- if each_pattern.search(command):
67
- return True
68
- return False
69
-
70
-
71
- def extract_target_url(command: str) -> str:
72
- url_match = SERVER_URL_PATTERN.search(command)
73
- if url_match:
74
- return url_match.group(0)
75
-
76
- is_playwright = "playwright" in command
77
- if is_playwright:
78
- return DEFAULT_PLAYWRIGHT_URL
79
-
80
- is_pytest = "pytest" in command
81
- if is_pytest:
82
- return DEFAULT_DJANGO_URL
83
-
84
- return DEFAULT_PLAYWRIGHT_URL
85
-
86
-
87
- def check_server_health(target_url: str) -> str | None:
88
- try:
89
- curl_result = subprocess.run(
90
- ["curl", "-s", "-o", "/dev/null", "-w", "%{http_code}", "--max-time", str(CURL_TIMEOUT_SECONDS), target_url],
91
- capture_output=True,
92
- text=True,
93
- timeout=CURL_TIMEOUT_SECONDS + 1,
94
- )
95
- http_status_code = int(curl_result.stdout.strip())
96
- except (subprocess.TimeoutExpired, ValueError, OSError):
97
- return UNREACHABLE_ERROR_TEMPLATE.format(target_url)
98
-
99
- if http_status_code == 0:
100
- return UNREACHABLE_ERROR_TEMPLATE.format(target_url)
101
-
102
- if http_status_code in BLOCKED_STATUS_CODES:
103
- return HEALTH_CHECK_ERROR_TEMPLATE.format(target_url, f"HTTP {http_status_code}")
104
-
105
- return None
106
-
107
-
108
- def find_project_root(command: str) -> str | None:
109
- working_directory = os.environ.get("PWD", os.getcwd())
110
-
111
- directory_match = re.search(r'--project[= ](\S+)', command)
112
- if directory_match:
113
- return os.path.abspath(directory_match.group(1))
114
-
115
- cd_match = re.search(r'cd\s+"([^"]+)"', command) or re.search(r"cd\s+'([^']+)'", command) or re.search(r'cd\s+(\S+)', command)
116
- if cd_match:
117
- cd_target = cd_match.group(1)
118
- if os.path.isabs(cd_target):
119
- return cd_target
120
- return os.path.join(working_directory, cd_target)
121
-
122
- return working_directory
123
-
124
-
125
- def check_django_database(command: str) -> str | None:
126
- is_django_test = "pytest" in command or "manage.py" in command
127
- if not is_django_test:
128
- return None
129
-
130
- project_root = find_project_root(command)
131
- if not project_root:
132
- return None
133
-
134
- manage_py_path = os.path.join(project_root, "manage.py")
135
- if not os.path.exists(manage_py_path):
136
- return None
137
-
138
- database_path = os.path.join(project_root, DJANGO_DB_FILENAME)
139
- if os.path.exists(database_path):
140
- return None
141
-
142
- return MISSING_DB_ERROR_TEMPLATE.format(DJANGO_DB_FILENAME, project_root)
143
-
144
-
145
- def find_frontend_directory(command: str) -> str | None:
146
- project_root = find_project_root(command)
147
- if not project_root:
148
- return None
149
-
150
- frontend_path = os.path.join(project_root, FRONTEND_DIRECTORY_NAME)
151
- if os.path.isdir(frontend_path):
152
- return frontend_path
153
-
154
- return None
155
-
156
-
157
- def build_frontend(command: str) -> str | None:
158
- frontend_path = find_frontend_directory(command)
159
- if not frontend_path:
160
- return None
161
-
162
- project_root = find_project_root(command)
163
-
164
- npm_build_result = subprocess.run(
165
- NPM_BUILD_COMMAND.split(),
166
- cwd=frontend_path,
167
- capture_output=True,
168
- text=True,
169
- timeout=BUILD_TIMEOUT_SECONDS,
170
- )
171
- if npm_build_result.returncode != 0:
172
- return FRONTEND_BUILD_FAILED_MESSAGE
173
-
174
- collectstatic_result = subprocess.run(
175
- COLLECTSTATIC_COMMAND.split(),
176
- cwd=project_root,
177
- capture_output=True,
178
- text=True,
179
- timeout=BUILD_TIMEOUT_SECONDS,
180
- )
181
- if collectstatic_result.returncode != 0:
182
- return FRONTEND_BUILD_FAILED_MESSAGE
183
-
184
- return None
185
-
186
-
187
- def extract_port_from_url(target_url: str) -> str:
188
- parsed_url = urlparse(target_url)
189
- if parsed_url.port:
190
- return str(parsed_url.port)
191
- return "8000"
192
-
193
-
194
- def check_test_db_flag(target_url: str) -> str | None:
195
- port = extract_port_from_url(target_url)
196
-
197
- try:
198
- ps_result = subprocess.run(
199
- ["ps", "aux"],
200
- capture_output=True,
201
- text=True,
202
- timeout=CURL_TIMEOUT_SECONDS,
203
- )
204
- except (subprocess.TimeoutExpired, OSError):
205
- return None
206
-
207
- is_runserver_found = False
208
- for each_line in ps_result.stdout.splitlines():
209
- if "runserver" not in each_line:
210
- continue
211
- if "grep" in each_line:
212
- continue
213
- is_runserver_found = True
214
- if "--test-db" in each_line:
215
- return None
216
-
217
- if not is_runserver_found:
218
- return None
219
-
220
- return MISSING_TEST_DB_FLAG_TEMPLATE.format(port, port)
221
-
222
-
223
- def _get_runserver_processes_on_port(target_port: str) -> list[tuple[int, str]]:
224
- if psutil is None:
225
- return []
226
-
227
- runserver_processes: list[tuple[int, str]] = []
228
- port_token = f":{target_port}"
229
-
230
- for each_process in psutil.process_iter(["pid", "cmdline", "cwd"]):
231
- try:
232
- commandline_parts = each_process.info.get("cmdline") or []
233
- if len(commandline_parts) < 3:
234
- continue
235
- if commandline_parts[1] != "manage.py" or commandline_parts[2] != "runserver":
236
- continue
237
-
238
- full_commandline = " ".join(commandline_parts)
239
- if port_token not in full_commandline:
240
- continue
241
-
242
- process_working_directory = each_process.info.get("cwd") or ""
243
- runserver_processes.append((each_process.info["pid"], process_working_directory))
244
- except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess, KeyError):
245
- continue
246
-
247
- return runserver_processes
248
-
249
-
250
- def check_runserver_port_conflicts(target_url: str, project_root: str | None) -> str | None:
251
- parsed_target_url = urlparse(target_url)
252
- target_host = parsed_target_url.hostname or ""
253
- if target_host not in {"localhost", "127.0.0.1", "0.0.0.0"}:
254
- return None
255
-
256
- target_port = str(parsed_target_url.port or 8000)
257
- runserver_processes = _get_runserver_processes_on_port(target_port)
258
- if len(runserver_processes) <= 1:
259
- return None
260
-
261
- project_root_realpath = os.path.realpath(project_root) if project_root else None
262
- unique_directories: set[str] = set()
263
- for _, directory_path in runserver_processes:
264
- if not directory_path:
265
- continue
266
- unique_directories.add(os.path.realpath(directory_path))
267
-
268
- if len(unique_directories) <= 1:
269
- return None
270
-
271
- if project_root_realpath and project_root_realpath in unique_directories:
272
- other_worktrees = sorted(path for path in unique_directories if path != project_root_realpath)
273
- if not other_worktrees:
274
- return None
275
- return PORT_CONFLICT_ERROR_TEMPLATE.format(target_port, ", ".join(other_worktrees))
276
-
277
- return PORT_CONFLICT_ERROR_TEMPLATE.format(target_port, ", ".join(sorted(unique_directories)))
278
-
279
-
280
- def build_deny_response(reason: str) -> dict:
281
- return {
282
- "hookSpecificOutput": {
283
- "hookEventName": "PreToolUse",
284
- "permissionDecision": "deny",
285
- "permissionDecisionReason": reason,
286
- }
287
- }
288
-
289
-
290
- def main() -> None:
291
- try:
292
- hook_input = json.load(sys.stdin)
293
- except json.JSONDecodeError:
294
- sys.exit(0)
295
-
296
- tool_name = hook_input.get("tool_name", "")
297
- if tool_name != "Bash":
298
- sys.exit(0)
299
-
300
- command = hook_input.get("tool_input", {}).get("command", "")
301
- if not is_test_command(command):
302
- sys.exit(0)
303
-
304
- project_root = find_project_root(command)
305
- is_django_project = project_root and os.path.exists(os.path.join(project_root, "manage.py"))
306
-
307
- if not is_django_project and not is_playwright_command(command):
308
- sys.exit(0)
309
-
310
- database_error = check_django_database(command)
311
- if database_error:
312
- print(json.dumps(build_deny_response(database_error)))
313
- sys.exit(0)
314
-
315
- target_url = extract_target_url(command)
316
-
317
- is_e2e_test = is_playwright_command(command)
318
- if is_e2e_test:
319
- conflict_error = check_runserver_port_conflicts(target_url, project_root)
320
- if conflict_error:
321
- print(json.dumps(build_deny_response(conflict_error)))
322
- sys.exit(0)
323
-
324
- test_db_error = check_test_db_flag(target_url)
325
- if test_db_error:
326
- print(json.dumps(build_deny_response(test_db_error)))
327
- sys.exit(0)
328
-
329
- frontend_build_error = build_frontend(command)
330
- if frontend_build_error:
331
- print(json.dumps(build_deny_response(frontend_build_error)))
332
- sys.exit(0)
333
-
334
- server_error = check_server_health(target_url)
335
- if server_error:
336
- print(json.dumps(build_deny_response(server_error)))
337
- sys.exit(0)
338
-
339
- sys.exit(0)
340
-
341
-
342
- if __name__ == "__main__":
343
- main()
@@ -1,80 +0,0 @@
1
- """Production-path tests for pytest-Django and Playwright preflight selection."""
2
-
3
- from __future__ import annotations
4
-
5
- import subprocess
6
- import sys
7
- from pathlib import Path
8
-
9
- _BLOCKING_DIRECTORY = Path(__file__).resolve().parent
10
- _DISPATCHER_PATH = _BLOCKING_DIRECTORY / "bash_pre_tool_use_dispatcher.py"
11
- if str(_BLOCKING_DIRECTORY) not in sys.path:
12
- sys.path.insert(0, str(_BLOCKING_DIRECTORY))
13
-
14
- from test_hook_subprocess_support import ( # noqa: E402
15
- build_bash_payload,
16
- read_hook_permission_decision,
17
- run_hook_as_subprocess,
18
- )
19
-
20
-
21
- def _run_bash_dispatch(
22
- command: str, project_root: Path
23
- ) -> subprocess.CompletedProcess[str]:
24
- return run_hook_as_subprocess(
25
- hook_script_path=_DISPATCHER_PATH,
26
- payload_text=build_bash_payload(command),
27
- working_directory=project_root,
28
- home_directory=project_root,
29
- environment_updates_by_name={"PWD": str(project_root)},
30
- )
31
-
32
-
33
- def _create_django_project_fixture(tmp_path: Path) -> Path:
34
- project_root = tmp_path / "django-project"
35
- project_root.mkdir()
36
- (project_root / "manage.py").write_text(
37
- "import sys\n\nif __name__ == '__main__':\n sys.exit(0)\n",
38
- encoding="utf-8",
39
- )
40
- return project_root
41
-
42
-
43
- def test_dispatcher_selects_pytest_django_changed_test(tmp_path: Path) -> None:
44
- project_root = _create_django_project_fixture(tmp_path)
45
-
46
- command = "python -m pytest tests/test_models.py"
47
- completed_process = _run_bash_dispatch(command, project_root)
48
-
49
- assert completed_process.returncode == 0, completed_process.stderr
50
- decision, reason = read_hook_permission_decision(completed_process.stdout)
51
- assert decision == "deny"
52
- assert "No database file (db.sqlite3)" in reason
53
-
54
-
55
- def test_dispatcher_ignores_unrecognized_django_test_runner(tmp_path: Path) -> None:
56
- project_root = _create_django_project_fixture(tmp_path)
57
-
58
- command = "python -m unittest tests/test_models.py"
59
- completed_process = _run_bash_dispatch(command, project_root)
60
-
61
- assert completed_process.returncode == 0, completed_process.stderr
62
- assert completed_process.stdout == ""
63
-
64
-
65
- def test_dispatcher_selects_playwright_changed_spec(tmp_path: Path) -> None:
66
- command = "npx playwright test tests/account.spec.ts --base-url http://127.0.0.1:1"
67
- completed_process = _run_bash_dispatch(command, tmp_path)
68
-
69
- assert completed_process.returncode == 0, completed_process.stderr
70
- decision, reason = read_hook_permission_decision(completed_process.stdout)
71
- assert decision == "deny"
72
- assert "Server at http://127.0.0.1:1 is unreachable" in reason
73
-
74
-
75
- def test_dispatcher_ignores_non_playwright_test_runner(tmp_path: Path) -> None:
76
- command = "node tests/account.spec.ts"
77
- completed_process = _run_bash_dispatch(command, tmp_path)
78
-
79
- assert completed_process.returncode == 0, completed_process.stderr
80
- assert completed_process.stdout == ""