claude-dev-env 4.3.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) 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 +5 -4
  12. package/.agents/skills/pull-request/SKILL.md +11 -13
  13. package/.agents/skills/pull-request/reference/publication-tasks.md +3 -3
  14. package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/constants.py +9 -1
  15. package/.agents/skills/pull-request/scripts/pull_request.py +33 -44
  16. package/.agents/skills/pull-request/scripts/test_github_pr_command_constants.py +9 -0
  17. package/.agents/skills/pull-request/scripts/test_pull_request.py +1 -3
  18. package/.agents/skills/skill-builder/SKILL.md +3 -196
  19. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +6 -0
  20. package/_shared/pr-loop/scripts/terminology_sweep.py +64 -19
  21. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +59 -0
  22. package/bin/install.agents-home.test.mjs +15 -13
  23. package/bin/install.mjs +28 -0
  24. package/bin/install.policy-lint.test.mjs +78 -0
  25. package/bin/install.prune.test.mjs +59 -20
  26. package/bin/install.pstack-plugin.test.mjs +114 -0
  27. package/bin/install.test.mjs +37 -0
  28. package/bin/refresh-pstack-plugin-skills.test.mjs +211 -0
  29. package/hooks/advisory/pr_done_reminder.py +342 -0
  30. package/hooks/advisory/test_pr_done_reminder.py +249 -0
  31. package/hooks/blocking/bash_post_call_dispatcher.py +53 -6
  32. package/hooks/blocking/code_rules_paired_test.py +22 -15
  33. package/hooks/blocking/test_bash_post_call_dispatcher.py +65 -4
  34. package/hooks/blocking/test_code_rules_enforcer_paired_test.py +36 -29
  35. package/hooks/blocking/test_code_rules_js_resume_task_enumeration.py +4 -4
  36. package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +5 -5
  37. package/hooks/blocking/test_pre_tool_use_dispatcher.py +68 -133
  38. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +20 -51
  39. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +18 -18
  40. package/hooks/git-hooks/git_hooks_constants/staged_policy_lint.py +12 -0
  41. package/hooks/git-hooks/pre_commit.py +52 -11
  42. package/hooks/git-hooks/test_installed_policy_lint.py +159 -0
  43. package/hooks/git-hooks/test_native_policy_commit.py +87 -0
  44. package/hooks/git-hooks/test_pre_commit.py +246 -0
  45. package/hooks/hooks.json +1 -1
  46. package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +14 -1
  47. package/hooks/hooks_constants/pr_done_reminder_constants.py +98 -0
  48. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -43
  49. package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +19 -2
  50. package/hooks/hooks_constants/test_pr_done_reminder_constants.py +40 -0
  51. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +20 -32
  52. package/package.json +2 -2
  53. package/rules/durable-post-artifacts.md +2 -1
  54. package/rules/git-workflow.md +6 -6
  55. package/scripts/check.ps1 +0 -1
  56. package/{.agents/skills/run-claude-dev-env/driver.mjs → scripts/ci/installer-lifecycle.mjs} +3 -28
  57. package/scripts/ci/installer-lifecycle.test.mjs +26 -0
  58. package/scripts/ci/windows-installer-lifecycle.ps1 +1 -11
  59. package/scripts/dev_env_scripts_constants/shared_tree_constants.py +4 -5
  60. package/{.agents/skills/_shared/pr-loop/scripts → scripts}/durable_post_lint.py +5 -41
  61. package/{.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/durable_post_lint_constants.py → scripts/durable_post_lint_config/config/constants.py} +0 -10
  62. package/{.agents/skills/_shared/pr-loop/scripts/test_durable_post_lint_constants.py → scripts/durable_post_lint_config/config/test_constants.py} +1 -1
  63. package/scripts/policy_lint/adapter_detectors.py +13 -6
  64. package/scripts/policy_lint/adapters.py +7 -4
  65. package/scripts/policy_lint/config/constants.py +5 -0
  66. package/scripts/policy_lint/engine.py +48 -4
  67. package/scripts/policy_lint/model.py +1 -0
  68. package/scripts/policy_lint/registry.py +1 -1
  69. package/scripts/policy_lint/selection.py +2 -1
  70. package/scripts/policy_lint/selection_git.py +2 -0
  71. package/scripts/refresh_pstack_plugin_skills.mjs +200 -0
  72. package/scripts/shared_tree_paths.py +32 -3
  73. package/scripts/test_shared_tree_paths.py +33 -0
  74. package/scripts/tests/test_policy_lint_rules_archive.py +120 -0
  75. package/scripts/tests/test_policy_lint_rules_engine.py +25 -4
  76. package/scripts/tests/test_policy_lint_rules_installed_loader.py +29 -0
  77. package/scripts/tests/test_policy_lint_rules_registry.py +227 -13
  78. package/scripts/tests/test_policy_lint_selection.py +25 -2
  79. package/scripts/tests/test_policy_lint_selection_native_index.py +30 -0
  80. package/scripts/tests/test_rules.py +3 -7
  81. package/.agents/skills/_shared/pr-loop/.claude/CLAUDE.md +0 -1
  82. package/.agents/skills/_shared/pr-loop/AGENTS.md +0 -1
  83. package/.agents/skills/_shared/pr-loop/audit-contract.md +0 -5
  84. package/.agents/skills/_shared/pr-loop/audit-reply-template.md +0 -5
  85. package/.agents/skills/_shared/pr-loop/code-rules-gate.md +0 -5
  86. package/.agents/skills/_shared/pr-loop/fix-protocol.md +0 -5
  87. package/.agents/skills/_shared/pr-loop/gh-payloads.md +0 -5
  88. package/.agents/skills/_shared/pr-loop/portable-driver.md +0 -149
  89. package/.agents/skills/_shared/pr-loop/post-audit-thread-contract.md +0 -5
  90. package/.agents/skills/_shared/pr-loop/precatch-rubric.md +0 -5
  91. package/.agents/skills/_shared/pr-loop/preflight-proposal.contract.test.mjs +0 -119
  92. package/.agents/skills/_shared/pr-loop/preflight-proposal.md +0 -5
  93. package/.agents/skills/_shared/pr-loop/prompts/.claude/CLAUDE.md +0 -1
  94. package/.agents/skills/_shared/pr-loop/prompts/AGENTS.md +0 -1
  95. package/.agents/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +0 -324
  96. package/.agents/skills/_shared/pr-loop/scripts/.claude/CLAUDE.md +0 -1
  97. package/.agents/skills/_shared/pr-loop/scripts/AGENTS.md +0 -1
  98. package/.agents/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +0 -29
  99. package/.agents/skills/_shared/pr-loop/scripts/_cli_utils.py +0 -22
  100. package/.agents/skills/_shared/pr-loop/scripts/_path_resolver.py +0 -186
  101. package/.agents/skills/_shared/pr-loop/scripts/_xml_utils.py +0 -20
  102. package/.agents/skills/_shared/pr-loop/scripts/audit_category_schema.py +0 -355
  103. package/.agents/skills/_shared/pr-loop/scripts/build_audit_prompt.py +0 -289
  104. package/.agents/skills/_shared/pr-loop/scripts/build_converge_task_list.py +0 -310
  105. package/.agents/skills/_shared/pr-loop/scripts/build_fix_prompt.py +0 -292
  106. package/.agents/skills/_shared/pr-loop/scripts/init_loop_state.py +0 -134
  107. package/.agents/skills/_shared/pr-loop/scripts/portable_converge_driver.py +0 -1646
  108. package/.agents/skills/_shared/pr-loop/scripts/preflight_worktree.py +0 -392
  109. package/.agents/skills/_shared/pr-loop/scripts/select_converge_pacer.py +0 -215
  110. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/.claude/CLAUDE.md +0 -1
  111. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +0 -1
  112. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/__init__.py +0 -0
  113. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +0 -37
  114. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/converge_task_list_constants.py +0 -56
  115. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/handoff_constants.py +0 -45
  116. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/pacer_constants.py +0 -47
  117. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +0 -157
  118. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/portable_driver_constants.py +0 -177
  119. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +0 -70
  120. package/.agents/skills/_shared/pr-loop/scripts/teardown_worktrees.py +0 -172
  121. package/.agents/skills/_shared/pr-loop/scripts/test__path_resolver.py +0 -106
  122. package/.agents/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +0 -94
  123. package/.agents/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +0 -499
  124. package/.agents/skills/_shared/pr-loop/scripts/test_build_converge_task_list.py +0 -140
  125. package/.agents/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +0 -262
  126. package/.agents/skills/_shared/pr-loop/scripts/test_init_loop_state.py +0 -71
  127. package/.agents/skills/_shared/pr-loop/scripts/test_portable_converge_driver.py +0 -1119
  128. package/.agents/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +0 -287
  129. package/.agents/skills/_shared/pr-loop/scripts/test_select_converge_pacer.py +0 -207
  130. package/.agents/skills/_shared/pr-loop/scripts/test_teardown_worktrees.py +0 -59
  131. package/.agents/skills/_shared/pr-loop/scripts/test_write_handoff.py +0 -201
  132. package/.agents/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +0 -182
  133. package/.agents/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +0 -206
  134. package/.agents/skills/_shared/pr-loop/scripts/write_handoff.py +0 -309
  135. package/.agents/skills/_shared/pr-loop/state-schema.md +0 -5
  136. package/.agents/skills/_shared/pr-loop/worker-spawn.md +0 -5
  137. package/.agents/skills/autoconverge/.claude/CLAUDE.md +0 -1
  138. package/.agents/skills/autoconverge/AGENTS.md +0 -1
  139. package/.agents/skills/autoconverge/SKILL.md +0 -431
  140. package/.agents/skills/autoconverge/reference/.claude/CLAUDE.md +0 -1
  141. package/.agents/skills/autoconverge/reference/AGENTS.md +0 -1
  142. package/.agents/skills/autoconverge/reference/closing-report.md +0 -93
  143. package/.agents/skills/autoconverge/reference/convergence-gates.md +0 -31
  144. package/.agents/skills/autoconverge/reference/convergence.md +0 -219
  145. package/.agents/skills/autoconverge/reference/copilot-findings.md +0 -51
  146. package/.agents/skills/autoconverge/reference/gotchas.md +0 -66
  147. package/.agents/skills/autoconverge/reference/headless-safety.md +0 -44
  148. package/.agents/skills/autoconverge/reference/multi-pr.md +0 -128
  149. package/.agents/skills/autoconverge/reference/self-closing-loop.md +0 -84
  150. package/.agents/skills/autoconverge/reference/stop-conditions.md +0 -127
  151. package/.agents/skills/autoconverge/test_portable_pacer_gate.py +0 -54
  152. package/.agents/skills/autoconverge/workflow/.claude/CLAUDE.md +0 -1
  153. package/.agents/skills/autoconverge/workflow/AGENTS.md +0 -1
  154. package/.agents/skills/autoconverge/workflow/aggregate_runs.py +0 -371
  155. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/.claude/CLAUDE.md +0 -1
  156. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +0 -1
  157. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/__init__.py +0 -0
  158. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +0 -250
  159. package/.agents/skills/autoconverge/workflow/converge.clean-audit.test.mjs +0 -665
  160. package/.agents/skills/autoconverge/workflow/converge.codex-gate.test.mjs +0 -472
  161. package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +0 -64
  162. package/.agents/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +0 -1009
  163. package/.agents/skills/autoconverge/workflow/converge.fix-progress.test.mjs +0 -107
  164. package/.agents/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +0 -33
  165. package/.agents/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +0 -15
  166. package/.agents/skills/autoconverge/workflow/converge.mjs +0 -3132
  167. package/.agents/skills/autoconverge/workflow/converge.p2-advance.test.mjs +0 -202
  168. package/.agents/skills/autoconverge/workflow/converge.path-aware.test.mjs +0 -47
  169. package/.agents/skills/autoconverge/workflow/converge.precatch.test.mjs +0 -152
  170. package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +0 -116
  171. package/.agents/skills/autoconverge/workflow/converge_multi.mjs +0 -169
  172. package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +0 -173
  173. package/.agents/skills/autoconverge/workflow/convergence_summary.py +0 -110
  174. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +0 -2
  175. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +0 -2
  176. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +0 -2
  177. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +0 -2
  178. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +0 -2
  179. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +0 -2
  180. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +0 -2
  181. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +0 -2
  182. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +0 -2
  183. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +0 -266
  184. package/.agents/skills/autoconverge/workflow/render_report.py +0 -1024
  185. package/.agents/skills/autoconverge/workflow/test_aggregate_runs.py +0 -134
  186. package/.agents/skills/autoconverge/workflow/test_convergence_summary.py +0 -149
  187. package/.agents/skills/autoconverge/workflow/test_render_report.py +0 -812
  188. package/.agents/skills/hitl/SKILL.md +0 -45
  189. package/.agents/skills/pr-cleanup/SKILL.md +0 -140
  190. package/.agents/skills/pr-cleanup/reference/process-inventory.md +0 -17
  191. package/.agents/skills/pr-cleanup/reference/task-seeds.md +0 -22
  192. package/.agents/skills/pr-name-by-capability/SKILL.md +0 -73
  193. package/.agents/skills/pr-name-by-capability/reference/fetch-commands.md +0 -29
  194. package/.agents/skills/pr-name-by-capability/reference/offense-examples.md +0 -32
  195. package/.agents/skills/pr-name-by-capability/reference/report-template.md +0 -29
  196. package/.agents/skills/pr-name-by-capability/reference/rule-checklist.md +0 -38
  197. package/.agents/skills/pr-name-by-capability/reference/task-seeds.md +0 -10
  198. package/.agents/skills/pr-plain-language-cleanup/SKILL.md +0 -93
  199. package/.agents/skills/pr-refinement/SKILL.md +0 -31
  200. package/.agents/skills/pr-shared-extraction/SKILL.md +0 -177
  201. package/.agents/skills/pr-shared-extraction/reference/examples.md +0 -46
  202. package/.agents/skills/pr-shared-extraction/reference/offense-taxonomy.md +0 -100
  203. package/.agents/skills/pr-small-cl/SKILL.md +0 -52
  204. package/.agents/skills/pr-title-description/SKILL.md +0 -96
  205. package/.agents/skills/prototype/SKILL.md +0 -85
  206. package/.agents/skills/prototype/reference/honest-limitations.md +0 -23
  207. package/.agents/skills/prototype/reference/promotion-tasks.md +0 -23
  208. package/.agents/skills/prototype/scripts/build_sandbox_settings.py +0 -249
  209. package/.agents/skills/prototype/scripts/conftest.py +0 -15
  210. package/.agents/skills/prototype/scripts/launch_sandbox.py +0 -205
  211. package/.agents/skills/prototype/scripts/probe_sandbox_safety.py +0 -311
  212. package/.agents/skills/prototype/scripts/prototype_scripts_constants/__init__.py +0 -1
  213. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
  214. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +0 -41
  215. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +0 -23
  216. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +0 -45
  217. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +0 -10
  218. package/.agents/skills/prototype/scripts/test_build_sandbox_settings.py +0 -275
  219. package/.agents/skills/prototype/scripts/test_launch_sandbox.py +0 -303
  220. package/.agents/skills/prototype/scripts/test_probe_sandbox_safety.py +0 -312
  221. package/.agents/skills/prototype/workflows/promotion.md +0 -25
  222. package/.agents/skills/prototype/workflows/sandbox.md +0 -35
  223. package/.agents/skills/rebase/.claude/CLAUDE.md +0 -1
  224. package/.agents/skills/rebase/AGENTS.md +0 -1
  225. package/.agents/skills/rebase/SKILL.md +0 -178
  226. package/.agents/skills/review-router/SKILL.md +0 -51
  227. package/.agents/skills/review-router/reference/route-policy.json +0 -7
  228. package/.agents/skills/review-router/reference/task-seeds.md +0 -1
  229. package/.agents/skills/review-router/scripts/conftest.py +0 -4
  230. package/.agents/skills/review-router/scripts/review_router.py +0 -50
  231. package/.agents/skills/review-router/scripts/review_router_cli.py +0 -211
  232. package/.agents/skills/review-router/scripts/review_router_constants/__init__.py +0 -1
  233. package/.agents/skills/review-router/scripts/review_router_constants/config/__init__.py +0 -0
  234. package/.agents/skills/review-router/scripts/review_router_constants/config/constants.py +0 -13
  235. package/.agents/skills/review-router/scripts/test_review_router.py +0 -23
  236. package/.agents/skills/review-router/scripts/test_review_router_cli.py +0 -47
  237. package/.agents/skills/review-tier/SKILL.md +0 -56
  238. package/.agents/skills/review-tier/reference/task-seeds.md +0 -1
  239. package/.agents/skills/review-tier/reference/tier-policy.json +0 -9
  240. package/.agents/skills/review-tier/scripts/conftest.py +0 -4
  241. package/.agents/skills/review-tier/scripts/review_tier.py +0 -359
  242. package/.agents/skills/review-tier/scripts/review_tier_constants/__init__.py +0 -1
  243. package/.agents/skills/review-tier/scripts/review_tier_constants/config/__init__.py +0 -0
  244. package/.agents/skills/review-tier/scripts/review_tier_constants/config/constants.py +0 -29
  245. package/.agents/skills/review-tier/scripts/test_generation.py +0 -159
  246. package/.agents/skills/review-tier/scripts/test_review_tier.py +0 -33
  247. package/.agents/skills/run-claude-dev-env/SKILL.md +0 -84
  248. package/.agents/skills/session-log/.claude/CLAUDE.md +0 -1
  249. package/.agents/skills/session-log/AGENTS.md +0 -1
  250. package/.agents/skills/session-log/SKILL.md +0 -170
  251. package/.agents/skills/session-log/templates/frontmatter.md +0 -40
  252. package/.agents/skills/session-tidy/.claude/CLAUDE.md +0 -1
  253. package/.agents/skills/session-tidy/AGENTS.md +0 -1
  254. package/.agents/skills/session-tidy/SKILL.md +0 -191
  255. package/.agents/skills/skill-builder/references/.claude/CLAUDE.md +0 -1
  256. package/.agents/skills/skill-builder/references/AGENTS.md +0 -1
  257. package/.agents/skills/skill-builder/references/delegation-map.md +0 -130
  258. package/.agents/skills/skill-builder/references/description-field.md +0 -111
  259. package/.agents/skills/skill-builder/references/deterministic-elements.md +0 -218
  260. package/.agents/skills/skill-builder/references/progressive-disclosure.md +0 -122
  261. package/.agents/skills/skill-builder/references/self-audit-checklist.md +0 -126
  262. package/.agents/skills/skill-builder/references/skill-modularity.md +0 -111
  263. package/.agents/skills/skill-builder/references/skill-types.md +0 -232
  264. package/.agents/skills/skill-builder/references/thariq-x-post-skills.json +0 -33
  265. package/.agents/skills/skill-builder/templates/.claude/CLAUDE.md +0 -1
  266. package/.agents/skills/skill-builder/templates/AGENTS.md +0 -1
  267. package/.agents/skills/skill-builder/templates/gap-analysis.md +0 -87
  268. package/.agents/skills/skill-builder/workflows/.claude/CLAUDE.md +0 -1
  269. package/.agents/skills/skill-builder/workflows/AGENTS.md +0 -1
  270. package/.agents/skills/skill-builder/workflows/improve-skill.md +0 -136
  271. package/.agents/skills/skill-builder/workflows/new-skill.md +0 -179
  272. package/.agents/skills/skill-builder/workflows/polish-skill.md +0 -139
  273. package/.agents/skills/source-command-sr-loop/SKILL.md +0 -82
  274. package/.agents/skills/update/.claude/CLAUDE.md +0 -1
  275. package/.agents/skills/update/AGENTS.md +0 -1
  276. package/.agents/skills/update/SKILL.md +0 -190
  277. package/_shared/pr-loop/AGENTS.md +0 -1
  278. package/_shared/pr-loop/CLAUDE.md +0 -1
  279. package/_shared/pr-loop/audit-contract.md +0 -183
  280. package/_shared/pr-loop/audit-reply-template.md +0 -151
  281. package/_shared/pr-loop/code-rules-gate.md +0 -65
  282. package/_shared/pr-loop/fix-protocol.md +0 -60
  283. package/_shared/pr-loop/gh-payloads.md +0 -75
  284. package/_shared/pr-loop/post-audit-thread-contract.md +0 -51
  285. package/_shared/pr-loop/precatch-rubric.md +0 -66
  286. package/_shared/pr-loop/preflight-proposal.md +0 -56
  287. package/_shared/pr-loop/scripts/post_audit_thread.py +0 -1242
  288. package/_shared/pr-loop/scripts/pr_loop_shared_constants/post_audit_thread_constants.py +0 -199
  289. package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +0 -1115
  290. package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +0 -129
  291. package/_shared/pr-loop/state-schema.md +0 -45
  292. package/_shared/pr-loop/worker-spawn.md +0 -189
  293. /package/{.agents/skills/_shared/pr-loop/scripts → scripts}/test_durable_post_lint.py +0 -0
@@ -1,1115 +0,0 @@
1
- """Live smoke tests for post_audit_thread.py.
2
-
3
- Runs against the real GitHub repo ``JonEcho/tests``. The class opens a
4
- single throwaway draft PR in ``setUpClass`` and reuses it across every
5
- test in the class; ``tearDownClass`` closes the PR with
6
- ``--delete-branch``. The CLEAN and DIRTY tests post real reviews against
7
- the shared PR. The retry tests stub the GitHub endpoint with a localhost
8
- HTTP server so the four-attempt retry loop runs deterministically without
9
- contacting api.github.com, but still reference the shared PR's number and
10
- HEAD SHA so the request URL is exercised end-to-end. Authentication uses
11
- ``gh auth token`` — empty token fails loudly per spec.
12
-
13
- Test files are exempt from the no-comment, magic-value, banned-identifier,
14
- and constants-location enforcer rules.
15
- """
16
-
17
- from __future__ import annotations
18
-
19
- import http.server
20
- import json
21
- import os
22
- import shutil
23
- import stat
24
- import subprocess
25
- import sys
26
- import tempfile
27
- import textwrap
28
- import threading
29
- import time
30
- import unittest
31
- import urllib.parse
32
- import uuid
33
- from pathlib import Path
34
- from typing import Any
35
-
36
- THIS_FILE_DIRECTORY = Path(__file__).resolve().parent
37
- SCRIPT_DIRECTORY = THIS_FILE_DIRECTORY.parent
38
-
39
- if str(SCRIPT_DIRECTORY) not in sys.path:
40
- sys.path.insert(0, str(SCRIPT_DIRECTORY))
41
-
42
- from pr_loop_shared_constants.post_audit_thread_constants import ( # noqa: E402
43
- ALL_GH_AUTH_TOKEN_COMMAND_PARTS,
44
- ALL_RETRY_BACKOFF_SECONDS,
45
- BUGTEAM_REVIEWER_ACCOUNT_ENV_VAR_NAME,
46
- GH_TOKEN_ENV_VAR_NAME,
47
- GITHUB_TOKEN_ENV_VAR_NAME,
48
- CLI_FLAG_COMMIT,
49
- CLI_FLAG_FINDINGS_JSON,
50
- CLI_FLAG_OWNER,
51
- CLI_FLAG_PR_NUMBER,
52
- CLI_FLAG_REPO,
53
- CLI_FLAG_SKILL,
54
- CLI_FLAG_STATE,
55
- EXIT_CODE_RETRY_EXHAUSTED,
56
- INLINE_COMMENT_SIDE_RIGHT,
57
- JSON_FIELD_DESCRIPTION,
58
- JSON_FIELD_FIX_SUMMARY,
59
- JSON_FIELD_LINE,
60
- JSON_FIELD_PATH,
61
- JSON_FIELD_SEVERITY,
62
- JSON_FIELD_SIDE,
63
- MAX_RETRY_ATTEMPTS,
64
- SEVERITY_TAG_P0,
65
- SEVERITY_TAG_P1,
66
- SEVERITY_TAG_P2,
67
- SINGLE_REVIEW_API_PATH_TEMPLATE,
68
- SINGLE_REVIEW_COMMENTS_API_PATH_TEMPLATE,
69
- SKILL_BUGTEAM,
70
- STATE_CLEAN,
71
- STATE_DIRTY,
72
- )
73
- from post_audit_thread import ( # noqa: E402
74
- UserInputError,
75
- build_reviews_endpoint_url,
76
- fetch_gh_token_for_account,
77
- list_authenticated_gh_account_logins,
78
- query_active_gh_user_login,
79
- query_pull_request_author_login,
80
- resolve_reviewer_token,
81
- )
82
-
83
- LIVE_TEST_OWNER = "JonEcho"
84
- LIVE_TEST_REPO = "tests"
85
- LIVE_TEST_BRANCH_PREFIX = "pr-loop-test"
86
- LIVE_TEST_PR_TITLE = "TEST: post_audit_thread smoke test (auto-closed)"
87
- LIVE_TEST_PR_BODY = (
88
- "Throwaway PR for post_audit_thread.py live smoke tests. "
89
- "Auto-created by `test_post_audit_thread.py`; closed in `tearDownClass`."
90
- )
91
- LIVE_TEST_BASE_BRANCH = "main"
92
- LIVE_TEST_FIXTURE_FILENAME = "post-audit-thread-fixture.md"
93
- LIVE_TEST_FIXTURE_CONTENT = (
94
- "# Throwaway test fixture\n\n"
95
- "Created by `test_post_audit_thread.py` to satisfy GitHub's "
96
- "non-empty PR-diff requirement. Deleted when the PR closes.\n"
97
- )
98
- LIVE_TEST_FIXTURE_LINE_FOR_FINDING_ONE = 1
99
- LIVE_TEST_FIXTURE_LINE_FOR_FINDING_TWO = 2
100
- LIVE_TEST_FIXTURE_LINE_FOR_FINDING_THREE = 3
101
-
102
- SCRIPT_PATH = SCRIPT_DIRECTORY / "post_audit_thread.py"
103
- REPO_FULL_NAME = f"{LIVE_TEST_OWNER}/{LIVE_TEST_REPO}"
104
-
105
- LIVE_TEST_AUDIT_ACCOUNT_NAME = "jl-cmd"
106
-
107
- GH_EVENT_APPROVED = "APPROVED"
108
- GH_EVENT_CHANGES_REQUESTED = "CHANGES_REQUESTED"
109
-
110
- UUID_SUFFIX_LENGTH = 8
111
-
112
- REVIEW_URL_ID_DELIMITER = "#pullrequestreview-"
113
-
114
-
115
- def _strip_read_only_and_retry(
116
- removal_function: Any, target_path: str, *_exc_info: Any
117
- ) -> None:
118
- try:
119
- os.chmod(target_path, stat.S_IWRITE)
120
- removal_function(target_path)
121
- except OSError:
122
- pass
123
-
124
-
125
- def force_remove_directory(target_path: Path) -> None:
126
- if not target_path.exists():
127
- return
128
- handler_kwargs: dict[str, Any]
129
- if sys.version_info >= (3, 12):
130
- handler_kwargs = {"onexc": _strip_read_only_and_retry}
131
- else:
132
- handler_kwargs = {"onerror": _strip_read_only_and_retry}
133
- try:
134
- shutil.rmtree(str(target_path), **handler_kwargs)
135
- except OSError as removal_error:
136
- sys.stderr.write(
137
- f"force_remove_directory: could not remove {target_path}: {removal_error}\n"
138
- )
139
-
140
-
141
- def resolve_gh_auth_token() -> str:
142
- completion = subprocess.run(
143
- list(ALL_GH_AUTH_TOKEN_COMMAND_PARTS),
144
- capture_output=True,
145
- text=True,
146
- encoding="utf-8",
147
- check=False,
148
- )
149
- if completion.returncode != 0:
150
- raise AssertionError(
151
- f"`gh auth token` failed: rc={completion.returncode} "
152
- f"stderr={completion.stderr.strip()} — live tests require gh to "
153
- f"be authenticated against github.com"
154
- )
155
- token_text = completion.stdout.strip()
156
- if not token_text:
157
- raise AssertionError(
158
- "`gh auth token` returned empty output — not authenticated"
159
- )
160
- return token_text
161
-
162
-
163
- def resolve_audit_account_token(account_name: str) -> str:
164
- completion = subprocess.run(
165
- list(ALL_GH_AUTH_TOKEN_COMMAND_PARTS) + ["--user", account_name],
166
- capture_output=True,
167
- text=True,
168
- encoding="utf-8",
169
- check=False,
170
- )
171
- if completion.returncode != 0:
172
- raise AssertionError(
173
- f"`gh auth token --user {account_name}` failed — the audit-side "
174
- f"account must be authenticated separately from the PR author so "
175
- f"GitHub allows APPROVE / REQUEST_CHANGES on the throwaway PR. "
176
- f"rc={completion.returncode} stderr={completion.stderr.strip()}"
177
- )
178
- token_text = completion.stdout.strip()
179
- if not token_text:
180
- raise AssertionError(
181
- f"`gh auth token --user {account_name}` returned empty output"
182
- )
183
- return token_text
184
-
185
-
186
- def gh_api_object_json(api_path: str) -> dict[str, Any]:
187
- completion = subprocess.run(
188
- ["gh", "api", api_path],
189
- capture_output=True,
190
- text=True,
191
- encoding="utf-8",
192
- check=True,
193
- )
194
- parsed_object: Any = json.loads(completion.stdout)
195
- if not isinstance(parsed_object, dict):
196
- raise AssertionError(
197
- f"unexpected gh api object shape: {type(parsed_object).__name__}"
198
- )
199
- return parsed_object
200
-
201
-
202
- def review_id_from_html_url(html_url: str) -> int:
203
- suffix_parts = html_url.rsplit(REVIEW_URL_ID_DELIMITER, 1)
204
- if len(suffix_parts) != 2:
205
- raise AssertionError(
206
- f"html_url {html_url!r} missing {REVIEW_URL_ID_DELIMITER!r} suffix"
207
- )
208
- return int(suffix_parts[1])
209
-
210
-
211
- def gh_api_paginated_json(api_path: str) -> list[dict[str, Any]]:
212
- completion = subprocess.run(
213
- ["gh", "api", api_path, "--paginate", "--slurp"],
214
- capture_output=True,
215
- text=True,
216
- encoding="utf-8",
217
- check=True,
218
- )
219
- parsed: Any = json.loads(completion.stdout)
220
- if not isinstance(parsed, list):
221
- raise AssertionError(
222
- f"unexpected gh api response shape: {type(parsed).__name__}"
223
- )
224
- flattened: list[dict[str, Any]] = []
225
- for each_page in parsed:
226
- if isinstance(each_page, list):
227
- for each_item in each_page:
228
- if isinstance(each_item, dict):
229
- flattened.append(each_item)
230
- elif isinstance(each_page, dict):
231
- flattened.append(each_page)
232
- return flattened
233
-
234
-
235
- def write_pr_body_temporary_file(body_text: str) -> Path:
236
- handle, body_path_str = tempfile.mkstemp(suffix=".md", prefix="post-audit-pr-body-")
237
- os.close(handle)
238
- body_path = Path(body_path_str)
239
- body_path.write_text(body_text, encoding="utf-8")
240
- return body_path
241
-
242
-
243
- def create_throwaway_pr(
244
- clone_directory: Path,
245
- branch_name: str,
246
- ) -> tuple[int, str]:
247
- subprocess.run(
248
- [
249
- "gh",
250
- "repo",
251
- "clone",
252
- REPO_FULL_NAME,
253
- str(clone_directory),
254
- "--",
255
- "--branch",
256
- LIVE_TEST_BASE_BRANCH,
257
- "--single-branch",
258
- "--depth",
259
- "1",
260
- ],
261
- check=True,
262
- capture_output=True,
263
- text=True,
264
- )
265
- subprocess.run(
266
- [
267
- "git",
268
- "-C",
269
- str(clone_directory),
270
- "config",
271
- "--local",
272
- "core.hooksPath",
273
- str(clone_directory / ".git" / "hooks"),
274
- ],
275
- check=True,
276
- capture_output=True,
277
- text=True,
278
- )
279
- subprocess.run(
280
- ["git", "-C", str(clone_directory), "checkout", "-b", branch_name],
281
- check=True,
282
- capture_output=True,
283
- text=True,
284
- )
285
- fixture_path = clone_directory / LIVE_TEST_FIXTURE_FILENAME
286
- fixture_path.write_text(LIVE_TEST_FIXTURE_CONTENT, encoding="utf-8")
287
- subprocess.run(
288
- ["git", "-C", str(clone_directory), "add", LIVE_TEST_FIXTURE_FILENAME],
289
- check=True,
290
- capture_output=True,
291
- text=True,
292
- )
293
- subprocess.run(
294
- [
295
- "git",
296
- "-C",
297
- str(clone_directory),
298
- "commit",
299
- "-m",
300
- "test: post_audit_thread.py live smoke fixture",
301
- ],
302
- check=True,
303
- capture_output=True,
304
- text=True,
305
- )
306
- head_sha_completion = subprocess.run(
307
- ["git", "-C", str(clone_directory), "rev-parse", "HEAD"],
308
- capture_output=True,
309
- text=True,
310
- encoding="utf-8",
311
- check=True,
312
- )
313
- head_sha = head_sha_completion.stdout.strip()
314
- subprocess.run(
315
- ["git", "-C", str(clone_directory), "push", "-u", "origin", branch_name],
316
- check=True,
317
- capture_output=True,
318
- text=True,
319
- )
320
- body_path = write_pr_body_temporary_file(LIVE_TEST_PR_BODY)
321
- try:
322
- create_completion = subprocess.run(
323
- [
324
- "gh",
325
- "pr",
326
- "create",
327
- "--draft",
328
- "--head",
329
- branch_name,
330
- "--base",
331
- LIVE_TEST_BASE_BRANCH,
332
- "--title",
333
- LIVE_TEST_PR_TITLE,
334
- "--body-file",
335
- str(body_path),
336
- "--repo",
337
- REPO_FULL_NAME,
338
- ],
339
- capture_output=True,
340
- text=True,
341
- encoding="utf-8",
342
- check=True,
343
- cwd=str(clone_directory),
344
- )
345
- finally:
346
- try:
347
- body_path.unlink()
348
- except OSError:
349
- pass
350
- pr_url = create_completion.stdout.strip().splitlines()[-1]
351
- parsed_pr_url = urllib.parse.urlparse(pr_url)
352
- pr_number = int(parsed_pr_url.path.rsplit("/", 1)[-1])
353
- return pr_number, head_sha
354
-
355
-
356
- def close_throwaway_pr(pr_number: int) -> None:
357
- subprocess.run(
358
- [
359
- "gh",
360
- "pr",
361
- "close",
362
- str(pr_number),
363
- "--delete-branch",
364
- "--repo",
365
- REPO_FULL_NAME,
366
- ],
367
- capture_output=True,
368
- text=True,
369
- check=False,
370
- )
371
-
372
-
373
- def remove_local_clone(clone_directory: Path) -> None:
374
- force_remove_directory(clone_directory)
375
-
376
-
377
- def best_effort_delete_remote_branch(branch_name: str) -> None:
378
- try:
379
- subprocess.run(
380
- [
381
- "gh",
382
- "api",
383
- "--method",
384
- "DELETE",
385
- f"repos/{LIVE_TEST_OWNER}/{LIVE_TEST_REPO}/git/refs/heads/{branch_name}",
386
- ],
387
- capture_output=True,
388
- text=True,
389
- check=False,
390
- )
391
- except OSError as deletion_error:
392
- sys.stderr.write(
393
- f"best_effort_delete_remote_branch: could not delete "
394
- f"{branch_name}: {deletion_error}\n"
395
- )
396
-
397
-
398
- def write_findings_json(findings_payload: list[dict[str, Any]]) -> Path:
399
- handle, findings_path_str = tempfile.mkstemp(
400
- suffix=".json", prefix="post-audit-findings-"
401
- )
402
- os.close(handle)
403
- findings_path = Path(findings_path_str)
404
- findings_path.write_text(json.dumps(findings_payload), encoding="utf-8")
405
- return findings_path
406
-
407
-
408
- def invoke_post_audit_thread_script(
409
- pr_number: int,
410
- head_sha: str,
411
- state_argument: str,
412
- findings_json_path: Path,
413
- audit_token: str,
414
- ) -> subprocess.CompletedProcess[str]:
415
- child_environment = dict(os.environ)
416
- child_environment[GH_TOKEN_ENV_VAR_NAME] = audit_token
417
- return subprocess.run(
418
- [
419
- sys.executable,
420
- str(SCRIPT_PATH),
421
- CLI_FLAG_SKILL,
422
- SKILL_BUGTEAM,
423
- CLI_FLAG_OWNER,
424
- LIVE_TEST_OWNER,
425
- CLI_FLAG_REPO,
426
- LIVE_TEST_REPO,
427
- CLI_FLAG_PR_NUMBER,
428
- str(pr_number),
429
- CLI_FLAG_COMMIT,
430
- head_sha,
431
- CLI_FLAG_STATE,
432
- state_argument,
433
- CLI_FLAG_FINDINGS_JSON,
434
- str(findings_json_path),
435
- ],
436
- capture_output=True,
437
- text=True,
438
- encoding="utf-8",
439
- check=False,
440
- env=child_environment,
441
- )
442
-
443
-
444
- STUB_SERVER_HOST = "127.0.0.1"
445
- STUB_SERVER_PORT_DYNAMIC = 0
446
- STUB_RESPONSE_HEADER_CONTENT_TYPE = "Content-Type"
447
- STUB_RESPONSE_HEADER_CONTENT_LENGTH = "Content-Length"
448
- STUB_RESPONSE_CONTENT_TYPE_VALUE = "application/json"
449
- STUB_HTTP_STATUS_BAD_GATEWAY = 502
450
- STUB_HTTP_STATUS_OK = 200
451
- STUB_502_RESPONSE_BODY_BYTES = json.dumps(
452
- {"message": "stub server: simulated transient 502 for retry test"}
453
- ).encode("utf-8")
454
- STUB_200_RESPONSE_BODY_BYTES = json.dumps(
455
- {
456
- "html_url": (
457
- "https://github.com/stub-host/stub-repo/pull/0#pullrequestreview-1"
458
- )
459
- }
460
- ).encode("utf-8")
461
- STUB_SERVER_SHUTDOWN_JOIN_TIMEOUT_SECONDS = 5.0
462
-
463
- FAILURE_COUNT_FOR_RETRY_SUCCESS = 1
464
- TOTAL_REQUEST_COUNT_FOR_RETRY_SUCCESS = 2
465
- FAILURE_COUNT_FOR_RETRY_EXHAUSTION = MAX_RETRY_ATTEMPTS + 1
466
- TOTAL_REQUEST_COUNT_FOR_RETRY_EXHAUSTION = MAX_RETRY_ATTEMPTS + 1
467
-
468
- BACKOFF_TIMING_EPSILON_SECONDS = 0.1
469
- TOTAL_BACKOFF_SECONDS = sum(ALL_RETRY_BACKOFF_SECONDS)
470
-
471
- EXPECTED_RETRY_SUCCESS_ELAPSED_LOWER_BOUND_SECONDS = (
472
- ALL_RETRY_BACKOFF_SECONDS[0] - BACKOFF_TIMING_EPSILON_SECONDS
473
- )
474
- EXPECTED_RETRY_EXHAUSTION_ELAPSED_LOWER_BOUND_SECONDS = (
475
- TOTAL_BACKOFF_SECONDS - BACKOFF_TIMING_EPSILON_SECONDS
476
- )
477
-
478
- EXIT_CODE_SUCCESS = 0
479
-
480
- LAUNCHER_SOURCE_CODE = textwrap.dedent(
481
- """
482
- import sys
483
- sys.path.insert(0, sys.argv[1])
484
- import post_audit_thread
485
- post_audit_thread.GITHUB_API_BASE_URL = sys.argv[2]
486
- sys.exit(post_audit_thread.main(sys.argv[3:]))
487
- """
488
- ).strip()
489
-
490
-
491
- class _StubReviewsServer(http.server.HTTPServer):
492
- """HTTP server that records POST count and serves canned 502/200 responses."""
493
-
494
- request_count: int = 0
495
- failure_count: int = 0
496
- recorded_request_path: str = ""
497
-
498
-
499
- class _StubReviewsHandler(http.server.BaseHTTPRequestHandler):
500
- """Returns 502 for the first ``failure_count`` POSTs, then 200 thereafter.
501
-
502
- State lives on the owning :class:`_StubReviewsServer` instance so the
503
- test can inspect the final request count and the path of the last
504
- received POST after the script exits.
505
- """
506
-
507
- def do_POST(self) -> None:
508
- owning_server = self.server
509
- owning_server.request_count += 1
510
- owning_server.recorded_request_path = self.path
511
- if owning_server.request_count <= owning_server.failure_count:
512
- response_status = STUB_HTTP_STATUS_BAD_GATEWAY
513
- response_body_bytes = STUB_502_RESPONSE_BODY_BYTES
514
- else:
515
- response_status = STUB_HTTP_STATUS_OK
516
- response_body_bytes = STUB_200_RESPONSE_BODY_BYTES
517
- self.send_response(response_status)
518
- self.send_header(
519
- STUB_RESPONSE_HEADER_CONTENT_TYPE, STUB_RESPONSE_CONTENT_TYPE_VALUE
520
- )
521
- self.send_header(
522
- STUB_RESPONSE_HEADER_CONTENT_LENGTH, str(len(response_body_bytes))
523
- )
524
- self.end_headers()
525
- self.wfile.write(response_body_bytes)
526
-
527
- def log_message(self, format: str, *args: Any) -> None:
528
- return
529
-
530
-
531
- def spawn_stub_reviews_server(
532
- failure_count: int,
533
- ) -> tuple[_StubReviewsServer, threading.Thread]:
534
- """Start a localhost stub server returning leading 502s then 200s.
535
-
536
- Args:
537
- failure_count: Number of leading POSTs the stub responds to with
538
- 502. Subsequent POSTs receive a synthetic 200 carrying a fake
539
- ``html_url``. Set above the script's total retry budget to
540
- force the retry-exhaustion path end-to-end.
541
-
542
- Returns:
543
- Tuple of the stub server (bound to a random port on
544
- ``127.0.0.1``) and its serving thread.
545
- """
546
- stub_server = _StubReviewsServer(
547
- (STUB_SERVER_HOST, STUB_SERVER_PORT_DYNAMIC), _StubReviewsHandler
548
- )
549
- stub_server.request_count = 0
550
- stub_server.failure_count = failure_count
551
- stub_server.recorded_request_path = ""
552
- stub_thread = threading.Thread(target=stub_server.serve_forever, daemon=True)
553
- stub_thread.start()
554
- return stub_server, stub_thread
555
-
556
-
557
- def shutdown_stub_reviews_server(
558
- stub_server: _StubReviewsServer,
559
- stub_thread: threading.Thread,
560
- ) -> None:
561
- """Stop the stub server and join its serving thread.
562
-
563
- Args:
564
- stub_server: Server returned by :func:`spawn_stub_reviews_server`.
565
- stub_thread: Serving thread returned by
566
- :func:`spawn_stub_reviews_server`.
567
- """
568
- stub_server.shutdown()
569
- stub_server.server_close()
570
- stub_thread.join(timeout=STUB_SERVER_SHUTDOWN_JOIN_TIMEOUT_SECONDS)
571
-
572
-
573
- def stub_reviews_server_base_url(stub_server: _StubReviewsServer) -> str:
574
- """Return ``http://host:port`` for a bound stub server.
575
-
576
- Args:
577
- stub_server: Server returned by :func:`spawn_stub_reviews_server`.
578
-
579
- Returns:
580
- Base URL suitable for assigning to
581
- ``post_audit_thread.GITHUB_API_BASE_URL`` inside the launcher
582
- subprocess so the script targets the stub instead of api.github.com.
583
- """
584
- host_address, bound_port = stub_server.server_address[:2]
585
- return f"http://{host_address}:{bound_port}"
586
-
587
-
588
- def invoke_post_audit_thread_with_url_override(
589
- pr_number: int,
590
- head_sha: str,
591
- state_argument: str,
592
- findings_json_path: Path,
593
- audit_token: str,
594
- overridden_base_url: str,
595
- ) -> subprocess.CompletedProcess[str]:
596
- """Subprocess-invoke the script with ``GITHUB_API_BASE_URL`` redirected.
597
-
598
- The subprocess runs a short launcher (``LAUNCHER_SOURCE_CODE``) that
599
- imports ``post_audit_thread`` as a module, rebinds its
600
- ``GITHUB_API_BASE_URL`` attribute to ``overridden_base_url``, then
601
- delegates to ``main()``. Lets the retry tests point the script at the
602
- local stub server without modifying production source.
603
-
604
- Args:
605
- pr_number: Throwaway PR number created by ``setUpClass``.
606
- head_sha: HEAD SHA the script attaches the review to.
607
- state_argument: ``CLEAN`` or ``DIRTY``.
608
- findings_json_path: Path to the (empty-list) findings JSON.
609
- audit_token: Token assigned to ``GH_TOKEN`` in the child env.
610
- overridden_base_url: Base URL handed to the launcher (the local
611
- stub server URL).
612
-
613
- Returns:
614
- Completed subprocess result with ``returncode``, ``stdout``, and
615
- ``stderr`` for the test to inspect.
616
- """
617
- child_environment = dict(os.environ)
618
- child_environment[GH_TOKEN_ENV_VAR_NAME] = audit_token
619
- return subprocess.run(
620
- [
621
- sys.executable,
622
- "-c",
623
- LAUNCHER_SOURCE_CODE,
624
- str(SCRIPT_DIRECTORY),
625
- overridden_base_url,
626
- CLI_FLAG_SKILL,
627
- SKILL_BUGTEAM,
628
- CLI_FLAG_OWNER,
629
- LIVE_TEST_OWNER,
630
- CLI_FLAG_REPO,
631
- LIVE_TEST_REPO,
632
- CLI_FLAG_PR_NUMBER,
633
- str(pr_number),
634
- CLI_FLAG_COMMIT,
635
- head_sha,
636
- CLI_FLAG_STATE,
637
- state_argument,
638
- CLI_FLAG_FINDINGS_JSON,
639
- str(findings_json_path),
640
- ],
641
- capture_output=True,
642
- text=True,
643
- encoding="utf-8",
644
- check=False,
645
- env=child_environment,
646
- )
647
-
648
-
649
- class LivePostAuditThreadTests(unittest.TestCase):
650
- """Live smoke tests for post_audit_thread.py against JonEcho/tests.
651
-
652
- Every test in this class reuses a single throwaway draft PR created
653
- in :meth:`setUpClass` and closed in :meth:`tearDownClass`. The CLEAN
654
- and DIRTY tests post real reviews against that PR; the retry tests
655
- redirect the script's HTTP layer to a localhost stub server so the
656
- retry loop runs deterministically without touching api.github.com,
657
- while still consuming the shared PR's number and HEAD SHA.
658
- """
659
-
660
- audit_account_token: str
661
- local_clone_directory: Path
662
- branch_name: str
663
- pr_number: int
664
- head_sha: str
665
-
666
- @classmethod
667
- def setUpClass(cls) -> None:
668
- resolve_gh_auth_token()
669
- cls.audit_account_token = resolve_audit_account_token(
670
- LIVE_TEST_AUDIT_ACCOUNT_NAME
671
- )
672
- unique_suffix = uuid.uuid4().hex[:UUID_SUFFIX_LENGTH]
673
- cls.branch_name = f"{LIVE_TEST_BRANCH_PREFIX}/{unique_suffix}"
674
- cls.local_clone_directory = Path(
675
- tempfile.mkdtemp(prefix=f"post-audit-thread-test-{unique_suffix}-")
676
- )
677
- cls.pr_number = 0
678
- cls.head_sha = ""
679
- try:
680
- cls.pr_number, cls.head_sha = create_throwaway_pr(
681
- cls.local_clone_directory,
682
- cls.branch_name,
683
- )
684
- except Exception:
685
- try:
686
- remove_local_clone(cls.local_clone_directory)
687
- finally:
688
- best_effort_delete_remote_branch(cls.branch_name)
689
- raise
690
-
691
- @classmethod
692
- def tearDownClass(cls) -> None:
693
- try:
694
- if cls.pr_number > 0:
695
- close_throwaway_pr(cls.pr_number)
696
- finally:
697
- try:
698
- remove_local_clone(cls.local_clone_directory)
699
- finally:
700
- best_effort_delete_remote_branch(cls.branch_name)
701
-
702
- def _assert_review_state_for_url(
703
- self, html_url: str, expected_state: str
704
- ) -> dict[str, Any]:
705
- review_id = review_id_from_html_url(html_url)
706
- single_review_api_path = SINGLE_REVIEW_API_PATH_TEMPLATE.format(
707
- owner=LIVE_TEST_OWNER,
708
- repo=LIVE_TEST_REPO,
709
- pr_number=self.pr_number,
710
- review_id=review_id,
711
- )
712
- single_review = gh_api_object_json(single_review_api_path)
713
- self.assertEqual(
714
- single_review.get("state"),
715
- expected_state,
716
- f"unexpected review state for {html_url!r}: {single_review!r}",
717
- )
718
- return single_review
719
-
720
- def _fetch_comments_for_review(self, html_url: str) -> list[dict[str, Any]]:
721
- review_id = review_id_from_html_url(html_url)
722
- review_comments_api_path = SINGLE_REVIEW_COMMENTS_API_PATH_TEMPLATE.format(
723
- owner=LIVE_TEST_OWNER,
724
- repo=LIVE_TEST_REPO,
725
- pr_number=self.pr_number,
726
- review_id=review_id,
727
- )
728
- return gh_api_paginated_json(review_comments_api_path)
729
-
730
- def _run_script_capturing_html_url(
731
- self,
732
- state_argument: str,
733
- findings_payload: list[dict[str, Any]],
734
- ) -> str:
735
- findings_path = write_findings_json(findings_payload)
736
- try:
737
- completion = invoke_post_audit_thread_script(
738
- pr_number=self.pr_number,
739
- head_sha=self.head_sha,
740
- state_argument=state_argument,
741
- findings_json_path=findings_path,
742
- audit_token=self.audit_account_token,
743
- )
744
- finally:
745
- try:
746
- findings_path.unlink()
747
- except OSError:
748
- pass
749
- self.assertEqual(
750
- completion.returncode,
751
- 0,
752
- f"script exited {completion.returncode}; stdout={completion.stdout!r} "
753
- f"stderr={completion.stderr!r}",
754
- )
755
- emitted_html_url = completion.stdout.strip().splitlines()[-1]
756
- self.assertTrue(
757
- emitted_html_url.startswith("https://github.com/"),
758
- f"expected an html_url on stdout, got {emitted_html_url!r}",
759
- )
760
- return emitted_html_url
761
-
762
- def test_clean_state_posts_approved_review_with_empty_comments(self) -> None:
763
- emitted_html_url = self._run_script_capturing_html_url(
764
- state_argument=STATE_CLEAN, findings_payload=[]
765
- )
766
- self._assert_review_state_for_url(emitted_html_url, GH_EVENT_APPROVED)
767
- review_comments = self._fetch_comments_for_review(emitted_html_url)
768
- self.assertEqual(
769
- len(review_comments),
770
- 0,
771
- f"CLEAN state should produce zero inline comments on this review; "
772
- f"saw {len(review_comments)}: {review_comments!r}",
773
- )
774
-
775
- def test_dirty_state_with_three_findings_posts_changes_requested_with_three_inline_threads(
776
- self,
777
- ) -> None:
778
- findings_payload: list[dict[str, Any]] = [
779
- {
780
- JSON_FIELD_PATH: LIVE_TEST_FIXTURE_FILENAME,
781
- JSON_FIELD_LINE: LIVE_TEST_FIXTURE_LINE_FOR_FINDING_ONE,
782
- JSON_FIELD_SIDE: INLINE_COMMENT_SIDE_RIGHT,
783
- JSON_FIELD_SEVERITY: SEVERITY_TAG_P0,
784
- JSON_FIELD_DESCRIPTION: "Smoke finding one (heading line).",
785
- JSON_FIELD_FIX_SUMMARY: "Trim the leading marker (smoke fix one).",
786
- },
787
- {
788
- JSON_FIELD_PATH: LIVE_TEST_FIXTURE_FILENAME,
789
- JSON_FIELD_LINE: LIVE_TEST_FIXTURE_LINE_FOR_FINDING_TWO,
790
- JSON_FIELD_SIDE: INLINE_COMMENT_SIDE_RIGHT,
791
- JSON_FIELD_SEVERITY: SEVERITY_TAG_P1,
792
- JSON_FIELD_DESCRIPTION: "Smoke finding two (blank-line anchor).",
793
- JSON_FIELD_FIX_SUMMARY: "Collapse the blank separator (smoke fix two).",
794
- },
795
- {
796
- JSON_FIELD_PATH: LIVE_TEST_FIXTURE_FILENAME,
797
- JSON_FIELD_LINE: LIVE_TEST_FIXTURE_LINE_FOR_FINDING_THREE,
798
- JSON_FIELD_SIDE: INLINE_COMMENT_SIDE_RIGHT,
799
- JSON_FIELD_SEVERITY: SEVERITY_TAG_P2,
800
- JSON_FIELD_DESCRIPTION: "Smoke finding three (body-line anchor).",
801
- JSON_FIELD_FIX_SUMMARY: "Tighten the description (smoke fix three).",
802
- },
803
- ]
804
- emitted_html_url = self._run_script_capturing_html_url(
805
- state_argument=STATE_DIRTY, findings_payload=findings_payload
806
- )
807
- self._assert_review_state_for_url(
808
- emitted_html_url, GH_EVENT_CHANGES_REQUESTED
809
- )
810
- review_comments = self._fetch_comments_for_review(emitted_html_url)
811
- self.assertEqual(
812
- len(review_comments),
813
- len(findings_payload),
814
- f"DIRTY state should produce one inline comment per finding on "
815
- f"this review; expected {len(findings_payload)} got "
816
- f"{len(review_comments)}: {review_comments!r}",
817
- )
818
-
819
- def _expected_reviews_request_path(self) -> str:
820
- full_endpoint_url = build_reviews_endpoint_url(
821
- LIVE_TEST_OWNER, LIVE_TEST_REPO, self.pr_number
822
- )
823
- parsed_endpoint = urllib.parse.urlparse(full_endpoint_url)
824
- return parsed_endpoint.path
825
-
826
- def _run_retry_simulation_and_measure_elapsed(
827
- self,
828
- failure_count: int,
829
- ) -> tuple[subprocess.CompletedProcess[str], _StubReviewsServer, float]:
830
- findings_path = write_findings_json([])
831
- try:
832
- stub_server, stub_thread = spawn_stub_reviews_server(
833
- failure_count=failure_count
834
- )
835
- try:
836
- overridden_base_url = stub_reviews_server_base_url(stub_server)
837
- start_time = time.perf_counter()
838
- completion = invoke_post_audit_thread_with_url_override(
839
- pr_number=self.pr_number,
840
- head_sha=self.head_sha,
841
- state_argument=STATE_CLEAN,
842
- findings_json_path=findings_path,
843
- audit_token=self.audit_account_token,
844
- overridden_base_url=overridden_base_url,
845
- )
846
- elapsed_seconds = time.perf_counter() - start_time
847
- finally:
848
- shutdown_stub_reviews_server(stub_server, stub_thread)
849
- finally:
850
- try:
851
- findings_path.unlink()
852
- except OSError:
853
- pass
854
- return completion, stub_server, elapsed_seconds
855
-
856
- def test_retry_succeeds_after_one_transient_502_response(self) -> None:
857
- completion, stub_server, elapsed_seconds = (
858
- self._run_retry_simulation_and_measure_elapsed(
859
- failure_count=FAILURE_COUNT_FOR_RETRY_SUCCESS
860
- )
861
- )
862
- self.assertEqual(
863
- completion.returncode,
864
- EXIT_CODE_SUCCESS,
865
- f"retry-success: expected exit {EXIT_CODE_SUCCESS}; got "
866
- f"{completion.returncode}; stdout={completion.stdout!r} "
867
- f"stderr={completion.stderr!r}",
868
- )
869
- self.assertEqual(
870
- stub_server.request_count,
871
- TOTAL_REQUEST_COUNT_FOR_RETRY_SUCCESS,
872
- f"retry-success: stub should have received exactly "
873
- f"{TOTAL_REQUEST_COUNT_FOR_RETRY_SUCCESS} POSTs (one 502 + "
874
- f"one 200); got {stub_server.request_count}",
875
- )
876
- expected_request_path = self._expected_reviews_request_path()
877
- self.assertEqual(
878
- stub_server.recorded_request_path,
879
- expected_request_path,
880
- f"retry-success: stub received POST at "
881
- f"{stub_server.recorded_request_path!r}; expected "
882
- f"{expected_request_path!r} per build_reviews_endpoint_url",
883
- )
884
- self.assertGreaterEqual(
885
- elapsed_seconds,
886
- EXPECTED_RETRY_SUCCESS_ELAPSED_LOWER_BOUND_SECONDS,
887
- f"retry-success should observe at least the first 1s backoff; "
888
- f"elapsed={elapsed_seconds:.2f}s",
889
- )
890
-
891
- def test_retry_exhausts_and_exits_two_after_four_consecutive_502_responses(
892
- self,
893
- ) -> None:
894
- completion, stub_server, elapsed_seconds = (
895
- self._run_retry_simulation_and_measure_elapsed(
896
- failure_count=FAILURE_COUNT_FOR_RETRY_EXHAUSTION
897
- )
898
- )
899
- self.assertEqual(
900
- completion.returncode,
901
- EXIT_CODE_RETRY_EXHAUSTED,
902
- f"retry-exhaustion: expected exit "
903
- f"{EXIT_CODE_RETRY_EXHAUSTED}; got "
904
- f"{completion.returncode}; stdout={completion.stdout!r} "
905
- f"stderr={completion.stderr!r}",
906
- )
907
- self.assertEqual(
908
- stub_server.request_count,
909
- TOTAL_REQUEST_COUNT_FOR_RETRY_EXHAUSTION,
910
- f"retry-exhaustion: stub should have received exactly "
911
- f"{TOTAL_REQUEST_COUNT_FOR_RETRY_EXHAUSTION} POSTs (one "
912
- f"initial plus three retries); got "
913
- f"{stub_server.request_count}",
914
- )
915
- expected_request_path = self._expected_reviews_request_path()
916
- self.assertEqual(
917
- stub_server.recorded_request_path,
918
- expected_request_path,
919
- f"retry-exhaustion: stub received POST at "
920
- f"{stub_server.recorded_request_path!r}; expected "
921
- f"{expected_request_path!r} per build_reviews_endpoint_url",
922
- )
923
- self.assertGreaterEqual(
924
- elapsed_seconds,
925
- EXPECTED_RETRY_EXHAUSTION_ELAPSED_LOWER_BOUND_SECONDS,
926
- f"retry-exhaustion should observe ~21s of backoff "
927
- f"(1s + 4s + 16s); elapsed={elapsed_seconds:.2f}s",
928
- )
929
-
930
- def _isolate_auth_env_vars(self) -> dict[str, str | None]:
931
- all_managed_env_var_names = (
932
- GH_TOKEN_ENV_VAR_NAME,
933
- GITHUB_TOKEN_ENV_VAR_NAME,
934
- BUGTEAM_REVIEWER_ACCOUNT_ENV_VAR_NAME,
935
- )
936
- previous_env_state: dict[str, str | None] = {
937
- each_name: os.environ.get(each_name)
938
- for each_name in all_managed_env_var_names
939
- }
940
- for each_name in all_managed_env_var_names:
941
- os.environ.pop(each_name, None)
942
- return previous_env_state
943
-
944
- def _restore_auth_env_vars(
945
- self, previous_env_state: dict[str, str | None]
946
- ) -> None:
947
- for each_name, prior_value in previous_env_state.items():
948
- if prior_value is None:
949
- os.environ.pop(each_name, None)
950
- else:
951
- os.environ[each_name] = prior_value
952
-
953
- def test_query_active_gh_user_login_matches_gh_api_user_login_field(self) -> None:
954
- active_login = query_active_gh_user_login()
955
- self.assertTrue(
956
- active_login,
957
- "query_active_gh_user_login() returned empty",
958
- )
959
- gh_api_user_response = gh_api_object_json("user")
960
- self.assertEqual(active_login, gh_api_user_response.get("login"))
961
-
962
- def test_query_pull_request_author_login_matches_throwaway_pr_author(self) -> None:
963
- author_login = query_pull_request_author_login(
964
- owner=LIVE_TEST_OWNER,
965
- repo=LIVE_TEST_REPO,
966
- pr_number=self.pr_number,
967
- )
968
- pr_detail_path = f"repos/{LIVE_TEST_OWNER}/{LIVE_TEST_REPO}/pulls/{self.pr_number}"
969
- pr_detail_object = gh_api_object_json(pr_detail_path)
970
- user_field_object = pr_detail_object.get("user")
971
- self.assertIsInstance(user_field_object, dict)
972
- if isinstance(user_field_object, dict):
973
- self.assertEqual(author_login, user_field_object.get("login"))
974
-
975
- def test_list_authenticated_gh_account_logins_includes_active_and_audit_accounts(
976
- self,
977
- ) -> None:
978
- all_logins = list_authenticated_gh_account_logins()
979
- active_login = query_active_gh_user_login()
980
- self.assertIn(active_login, all_logins)
981
- self.assertIn(LIVE_TEST_AUDIT_ACCOUNT_NAME, all_logins)
982
-
983
- def test_fetch_gh_token_for_account_returns_audit_account_cached_token(self) -> None:
984
- fetched_token = fetch_gh_token_for_account(LIVE_TEST_AUDIT_ACCOUNT_NAME)
985
- self.assertEqual(fetched_token, self.audit_account_token)
986
-
987
- def test_resolve_reviewer_token_returns_env_var_when_gh_token_is_set(self) -> None:
988
- sentinel_env_token = "sentinel-gh-token-from-env-var-precedence-test"
989
- previous_env_state = self._isolate_auth_env_vars()
990
- try:
991
- os.environ[GH_TOKEN_ENV_VAR_NAME] = sentinel_env_token
992
- returned_token = resolve_reviewer_token(
993
- owner=LIVE_TEST_OWNER,
994
- repo=LIVE_TEST_REPO,
995
- pr_number=self.pr_number,
996
- )
997
- self.assertEqual(returned_token, sentinel_env_token)
998
- finally:
999
- self._restore_auth_env_vars(previous_env_state)
1000
-
1001
- def test_resolve_reviewer_token_toggles_to_alternate_token_on_self_pr(self) -> None:
1002
- previous_env_state = self._isolate_auth_env_vars()
1003
- try:
1004
- returned_token = resolve_reviewer_token(
1005
- owner=LIVE_TEST_OWNER,
1006
- repo=LIVE_TEST_REPO,
1007
- pr_number=self.pr_number,
1008
- )
1009
- active_login = query_active_gh_user_login()
1010
- pr_author_login = query_pull_request_author_login(
1011
- owner=LIVE_TEST_OWNER,
1012
- repo=LIVE_TEST_REPO,
1013
- pr_number=self.pr_number,
1014
- )
1015
- self.assertEqual(
1016
- active_login.lower(),
1017
- pr_author_login.lower(),
1018
- "throwaway PR author must equal active gh account so the "
1019
- "self-PR toggle branch is exercised",
1020
- )
1021
- all_alternates = [
1022
- each_login
1023
- for each_login in list_authenticated_gh_account_logins()
1024
- if each_login.lower() != pr_author_login.lower()
1025
- ]
1026
- self.assertTrue(
1027
- all_alternates,
1028
- "test setup requires at least one alternate authenticated account",
1029
- )
1030
- expected_first_alternate_token = fetch_gh_token_for_account(
1031
- all_alternates[0]
1032
- )
1033
- self.assertEqual(returned_token, expected_first_alternate_token)
1034
- active_account_token = resolve_gh_auth_token()
1035
- self.assertNotEqual(
1036
- returned_token,
1037
- active_account_token,
1038
- "self-PR toggle must not return the active (author) token",
1039
- )
1040
- finally:
1041
- self._restore_auth_env_vars(previous_env_state)
1042
-
1043
- def test_resolve_reviewer_token_honors_bugteam_reviewer_account_pin(self) -> None:
1044
- previous_env_state = self._isolate_auth_env_vars()
1045
- try:
1046
- pr_author_login = query_pull_request_author_login(
1047
- owner=LIVE_TEST_OWNER,
1048
- repo=LIVE_TEST_REPO,
1049
- pr_number=self.pr_number,
1050
- )
1051
- all_alternates_excluding_pr_author = [
1052
- each_login
1053
- for each_login in list_authenticated_gh_account_logins()
1054
- if each_login.lower() != pr_author_login.lower()
1055
- ]
1056
- self.assertTrue(
1057
- all_alternates_excluding_pr_author,
1058
- "test setup requires at least one authenticated account that "
1059
- "is not the PR author so the pin has a valid target",
1060
- )
1061
- chosen_pin_login = all_alternates_excluding_pr_author[0]
1062
- os.environ[BUGTEAM_REVIEWER_ACCOUNT_ENV_VAR_NAME] = chosen_pin_login
1063
- returned_token = resolve_reviewer_token(
1064
- owner=LIVE_TEST_OWNER,
1065
- repo=LIVE_TEST_REPO,
1066
- pr_number=self.pr_number,
1067
- )
1068
- expected_pinned_token = fetch_gh_token_for_account(chosen_pin_login)
1069
- self.assertEqual(returned_token, expected_pinned_token)
1070
- finally:
1071
- self._restore_auth_env_vars(previous_env_state)
1072
-
1073
- def test_resolve_reviewer_token_error_excludes_pr_author_from_candidate_set(
1074
- self,
1075
- ) -> None:
1076
- unauthenticated_account_name = "intentionally-not-authenticated-account-zzz"
1077
- previous_env_state = self._isolate_auth_env_vars()
1078
- try:
1079
- os.environ[BUGTEAM_REVIEWER_ACCOUNT_ENV_VAR_NAME] = (
1080
- unauthenticated_account_name
1081
- )
1082
- with self.assertRaises(UserInputError) as raised_context:
1083
- resolve_reviewer_token(
1084
- owner=LIVE_TEST_OWNER,
1085
- repo=LIVE_TEST_REPO,
1086
- pr_number=self.pr_number,
1087
- )
1088
- error_message_text = str(raised_context.exception)
1089
- self.assertIn(unauthenticated_account_name, error_message_text)
1090
- pr_author_login = query_pull_request_author_login(
1091
- owner=LIVE_TEST_OWNER,
1092
- repo=LIVE_TEST_REPO,
1093
- pr_number=self.pr_number,
1094
- )
1095
- all_alternates_at_call_time = [
1096
- each_login
1097
- for each_login in list_authenticated_gh_account_logins()
1098
- if each_login.lower() != pr_author_login.lower()
1099
- ]
1100
- self.assertIn(
1101
- repr(all_alternates_at_call_time),
1102
- error_message_text,
1103
- "error must show the alternate-reviewer set actually searched",
1104
- )
1105
- self.assertNotIn(
1106
- f"authenticated set [{repr(pr_author_login)}",
1107
- error_message_text,
1108
- "error must not show a set whose head is the excluded PR author",
1109
- )
1110
- finally:
1111
- self._restore_auth_env_vars(previous_env_state)
1112
-
1113
-
1114
- if __name__ == "__main__":
1115
- unittest.main()