claude-dev-env 4.2.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/.agents/agents/pr-description-writer.md +2 -2
  2. package/.agents/agents/test_agent_frontmatter.py +2 -2
  3. package/.agents/agents/test_pr_description_writer_style.py +1 -3
  4. package/.agents/skills/e-code-review/SKILL.md +12 -12
  5. package/.agents/skills/e-code-review/preflight-proposal.contract.test.mjs +20 -0
  6. package/.agents/skills/e-code-review/reference/fix.md +15 -17
  7. package/.agents/skills/e-code-review/reference/loop.md +35 -39
  8. package/.agents/skills/e-code-review/reference/mode-contract.test.mjs +2 -1
  9. package/.agents/skills/e-code-review/reference/preflight-proposal.md +44 -9
  10. package/.agents/skills/e-code-review/reference/runner-selection.md +13 -14
  11. package/.agents/skills/issue-tracker/SKILL.md +7 -0
  12. package/.agents/skills/pull-request/SKILL.md +216 -0
  13. package/.agents/skills/pull-request/reference/publication-tasks.md +18 -0
  14. package/.agents/skills/pull-request/scripts/github_pr_command_constants/__init__.py +1 -0
  15. package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/__init__.py +1 -0
  16. package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/constants.py +69 -0
  17. package/.agents/skills/pull-request/scripts/pull_request.py +265 -0
  18. package/.agents/skills/pull-request/scripts/recover_legacy_author.py +251 -0
  19. package/.agents/skills/pull-request/scripts/test_github_pr_command_constants.py +58 -0
  20. package/.agents/skills/pull-request/scripts/test_pull_request.py +396 -0
  21. package/.agents/skills/pull-request/scripts/test_recover_legacy_author.py +221 -0
  22. package/.agents/skills/skill-builder/SKILL.md +3 -196
  23. package/.agents/skills/test-runner/SKILL.md +69 -0
  24. package/.agents/skills/test-runner/scripts/preflight_checks.py +365 -0
  25. package/.agents/skills/test-runner/scripts/run_tests.py +316 -0
  26. package/.agents/skills/test-runner/scripts/test_preflight_checks.py +185 -0
  27. package/.agents/skills/test-runner/scripts/test_run_tests.py +281 -0
  28. package/.agents/skills/test-runner/scripts/test_runner_constants/__init__.py +1 -0
  29. package/.agents/skills/test-runner/scripts/test_runner_constants/config/__init__.py +1 -0
  30. package/.agents/skills/test-runner/scripts/test_runner_constants/config/constants.py +96 -0
  31. package/.agents/skills-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
  32. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +6 -0
  33. package/_shared/pr-loop/scripts/terminology_sweep.py +64 -19
  34. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +59 -0
  35. package/bin/ever-shipped-skills.mjs +2 -0
  36. package/bin/ever-shipped-skills.test.mjs +8 -0
  37. package/bin/install.agents-home.test.mjs +15 -13
  38. package/bin/install.mjs +37 -1
  39. package/bin/install.policy-lint.test.mjs +78 -0
  40. package/bin/install.prune.test.mjs +149 -20
  41. package/bin/install.pstack-plugin.test.mjs +114 -0
  42. package/bin/install.test.mjs +61 -7
  43. package/bin/refresh-pstack-plugin-skills.test.mjs +211 -0
  44. package/hooks/advisory/pr_done_reminder.py +342 -0
  45. package/hooks/advisory/test_pr_done_reminder.py +249 -0
  46. package/hooks/blocking/bash_post_call_dispatcher.py +53 -6
  47. package/hooks/blocking/code_rules_paired_test.py +22 -15
  48. package/hooks/blocking/pii_prevention_blocker.py +32 -51
  49. package/hooks/blocking/test_bash_post_call_dispatcher.py +55 -17
  50. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +1 -4
  51. package/hooks/blocking/test_code_rules_command_dispatch.py +2 -2
  52. package/hooks/blocking/test_code_rules_enforcer_paired_test.py +36 -29
  53. package/hooks/blocking/test_code_rules_js_resume_task_enumeration.py +4 -4
  54. package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +5 -5
  55. package/hooks/blocking/test_pii_prevention_blocker.py +37 -89
  56. package/hooks/blocking/test_pre_tool_use_dispatcher.py +68 -133
  57. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +20 -51
  58. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +18 -18
  59. package/hooks/gh_post_body_texts.py +195 -0
  60. package/hooks/git-hooks/git_hooks_constants/staged_policy_lint.py +12 -0
  61. package/hooks/git-hooks/pre_commit.py +52 -11
  62. package/hooks/git-hooks/test_installed_policy_lint.py +159 -0
  63. package/hooks/git-hooks/test_native_policy_commit.py +87 -0
  64. package/hooks/git-hooks/test_pre_commit.py +246 -0
  65. package/hooks/hooks.json +1 -16
  66. package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +14 -2
  67. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -8
  68. package/hooks/hooks_constants/gh_post_body_texts_constants.py +12 -0
  69. package/hooks/hooks_constants/pr_done_reminder_constants.py +98 -0
  70. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -43
  71. package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +17 -7
  72. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +0 -7
  73. package/hooks/hooks_constants/test_failure_recorder_constants.py +3 -3
  74. package/hooks/hooks_constants/test_gh_post_body_texts_constants.py +19 -0
  75. package/hooks/hooks_constants/test_pr_done_reminder_constants.py +40 -0
  76. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +20 -32
  77. package/hooks/observability/test_failure_recorder.py +1 -1
  78. package/hooks/pyproject.toml +1 -1
  79. package/hooks/test_gh_post_body_texts.py +79 -0
  80. package/package.json +2 -2
  81. package/rules/durable-post-artifacts.md +32 -20
  82. package/rules/gh-cli-conventions.md +23 -13
  83. package/rules/git-workflow.md +21 -8
  84. package/scripts/check.ps1 +0 -1
  85. package/{.agents/skills/run-claude-dev-env/driver.mjs → scripts/ci/installer-lifecycle.mjs} +3 -28
  86. package/scripts/ci/installer-lifecycle.test.mjs +26 -0
  87. package/scripts/ci/windows-installer-lifecycle.ps1 +1 -11
  88. package/scripts/dev_env_scripts_constants/shared_tree_constants.py +4 -5
  89. package/scripts/durable_post_lint.py +206 -0
  90. package/scripts/durable_post_lint_config/config/constants.py +96 -0
  91. package/scripts/durable_post_lint_config/config/test_constants.py +25 -0
  92. package/scripts/policy_lint/adapter_configuration.py +53 -12
  93. package/scripts/policy_lint/adapter_detectors.py +13 -6
  94. package/scripts/policy_lint/adapters.py +7 -4
  95. package/scripts/policy_lint/config/constants.py +5 -0
  96. package/scripts/policy_lint/engine.py +48 -4
  97. package/scripts/policy_lint/model.py +1 -0
  98. package/scripts/policy_lint/registry.py +1 -1
  99. package/scripts/policy_lint/selection.py +2 -1
  100. package/scripts/policy_lint/selection_git.py +2 -0
  101. package/scripts/refresh_pstack_plugin_skills.mjs +200 -0
  102. package/scripts/repository_checks/config/constants.py +0 -5
  103. package/scripts/shared_tree_paths.py +32 -3
  104. package/scripts/test_durable_post_lint.py +332 -0
  105. package/scripts/test_shared_tree_paths.py +33 -0
  106. package/scripts/tests/test_adapter_configuration.py +54 -0
  107. package/scripts/tests/test_policy_lint_rules_archive.py +120 -0
  108. package/scripts/tests/test_policy_lint_rules_engine.py +25 -4
  109. package/scripts/tests/test_policy_lint_rules_installed_loader.py +29 -0
  110. package/scripts/tests/test_policy_lint_rules_registry.py +227 -13
  111. package/scripts/tests/test_policy_lint_selection.py +25 -2
  112. package/scripts/tests/test_policy_lint_selection_native_index.py +30 -0
  113. package/scripts/tests/test_rules.py +3 -7
  114. package/.agents/skills/_shared/pr-loop/.claude/CLAUDE.md +0 -1
  115. package/.agents/skills/_shared/pr-loop/AGENTS.md +0 -1
  116. package/.agents/skills/_shared/pr-loop/audit-contract.md +0 -5
  117. package/.agents/skills/_shared/pr-loop/audit-reply-template.md +0 -5
  118. package/.agents/skills/_shared/pr-loop/code-rules-gate.md +0 -5
  119. package/.agents/skills/_shared/pr-loop/fix-protocol.md +0 -5
  120. package/.agents/skills/_shared/pr-loop/gh-payloads.md +0 -5
  121. package/.agents/skills/_shared/pr-loop/portable-driver.md +0 -149
  122. package/.agents/skills/_shared/pr-loop/post-audit-thread-contract.md +0 -5
  123. package/.agents/skills/_shared/pr-loop/precatch-rubric.md +0 -5
  124. package/.agents/skills/_shared/pr-loop/preflight-proposal.contract.test.mjs +0 -119
  125. package/.agents/skills/_shared/pr-loop/preflight-proposal.md +0 -5
  126. package/.agents/skills/_shared/pr-loop/prompts/.claude/CLAUDE.md +0 -1
  127. package/.agents/skills/_shared/pr-loop/prompts/AGENTS.md +0 -1
  128. package/.agents/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +0 -324
  129. package/.agents/skills/_shared/pr-loop/scripts/.claude/CLAUDE.md +0 -1
  130. package/.agents/skills/_shared/pr-loop/scripts/AGENTS.md +0 -1
  131. package/.agents/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +0 -29
  132. package/.agents/skills/_shared/pr-loop/scripts/_cli_utils.py +0 -22
  133. package/.agents/skills/_shared/pr-loop/scripts/_path_resolver.py +0 -186
  134. package/.agents/skills/_shared/pr-loop/scripts/_xml_utils.py +0 -20
  135. package/.agents/skills/_shared/pr-loop/scripts/audit_category_schema.py +0 -355
  136. package/.agents/skills/_shared/pr-loop/scripts/build_audit_prompt.py +0 -289
  137. package/.agents/skills/_shared/pr-loop/scripts/build_converge_task_list.py +0 -310
  138. package/.agents/skills/_shared/pr-loop/scripts/build_fix_prompt.py +0 -292
  139. package/.agents/skills/_shared/pr-loop/scripts/init_loop_state.py +0 -134
  140. package/.agents/skills/_shared/pr-loop/scripts/portable_converge_driver.py +0 -1646
  141. package/.agents/skills/_shared/pr-loop/scripts/preflight_worktree.py +0 -392
  142. package/.agents/skills/_shared/pr-loop/scripts/select_converge_pacer.py +0 -215
  143. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/.claude/CLAUDE.md +0 -1
  144. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +0 -1
  145. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/__init__.py +0 -0
  146. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +0 -37
  147. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/converge_task_list_constants.py +0 -56
  148. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/handoff_constants.py +0 -45
  149. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/pacer_constants.py +0 -47
  150. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +0 -157
  151. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/portable_driver_constants.py +0 -177
  152. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +0 -70
  153. package/.agents/skills/_shared/pr-loop/scripts/teardown_worktrees.py +0 -172
  154. package/.agents/skills/_shared/pr-loop/scripts/test__path_resolver.py +0 -106
  155. package/.agents/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +0 -94
  156. package/.agents/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +0 -499
  157. package/.agents/skills/_shared/pr-loop/scripts/test_build_converge_task_list.py +0 -140
  158. package/.agents/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +0 -262
  159. package/.agents/skills/_shared/pr-loop/scripts/test_init_loop_state.py +0 -71
  160. package/.agents/skills/_shared/pr-loop/scripts/test_portable_converge_driver.py +0 -1119
  161. package/.agents/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +0 -287
  162. package/.agents/skills/_shared/pr-loop/scripts/test_select_converge_pacer.py +0 -207
  163. package/.agents/skills/_shared/pr-loop/scripts/test_teardown_worktrees.py +0 -59
  164. package/.agents/skills/_shared/pr-loop/scripts/test_write_handoff.py +0 -201
  165. package/.agents/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +0 -182
  166. package/.agents/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +0 -206
  167. package/.agents/skills/_shared/pr-loop/scripts/write_handoff.py +0 -309
  168. package/.agents/skills/_shared/pr-loop/state-schema.md +0 -5
  169. package/.agents/skills/_shared/pr-loop/worker-spawn.md +0 -5
  170. package/.agents/skills/autoconverge/.claude/CLAUDE.md +0 -1
  171. package/.agents/skills/autoconverge/AGENTS.md +0 -1
  172. package/.agents/skills/autoconverge/SKILL.md +0 -431
  173. package/.agents/skills/autoconverge/reference/.claude/CLAUDE.md +0 -1
  174. package/.agents/skills/autoconverge/reference/AGENTS.md +0 -1
  175. package/.agents/skills/autoconverge/reference/closing-report.md +0 -93
  176. package/.agents/skills/autoconverge/reference/convergence-gates.md +0 -31
  177. package/.agents/skills/autoconverge/reference/convergence.md +0 -219
  178. package/.agents/skills/autoconverge/reference/copilot-findings.md +0 -51
  179. package/.agents/skills/autoconverge/reference/gotchas.md +0 -66
  180. package/.agents/skills/autoconverge/reference/headless-safety.md +0 -44
  181. package/.agents/skills/autoconverge/reference/multi-pr.md +0 -128
  182. package/.agents/skills/autoconverge/reference/self-closing-loop.md +0 -84
  183. package/.agents/skills/autoconverge/reference/stop-conditions.md +0 -127
  184. package/.agents/skills/autoconverge/test_portable_pacer_gate.py +0 -54
  185. package/.agents/skills/autoconverge/workflow/.claude/CLAUDE.md +0 -1
  186. package/.agents/skills/autoconverge/workflow/AGENTS.md +0 -1
  187. package/.agents/skills/autoconverge/workflow/aggregate_runs.py +0 -371
  188. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/.claude/CLAUDE.md +0 -1
  189. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +0 -1
  190. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/__init__.py +0 -0
  191. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +0 -250
  192. package/.agents/skills/autoconverge/workflow/converge.clean-audit.test.mjs +0 -665
  193. package/.agents/skills/autoconverge/workflow/converge.codex-gate.test.mjs +0 -472
  194. package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +0 -64
  195. package/.agents/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +0 -1009
  196. package/.agents/skills/autoconverge/workflow/converge.fix-progress.test.mjs +0 -107
  197. package/.agents/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +0 -33
  198. package/.agents/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +0 -15
  199. package/.agents/skills/autoconverge/workflow/converge.mjs +0 -3132
  200. package/.agents/skills/autoconverge/workflow/converge.p2-advance.test.mjs +0 -202
  201. package/.agents/skills/autoconverge/workflow/converge.path-aware.test.mjs +0 -47
  202. package/.agents/skills/autoconverge/workflow/converge.precatch.test.mjs +0 -152
  203. package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +0 -116
  204. package/.agents/skills/autoconverge/workflow/converge_multi.mjs +0 -169
  205. package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +0 -173
  206. package/.agents/skills/autoconverge/workflow/convergence_summary.py +0 -110
  207. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +0 -2
  208. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +0 -2
  209. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +0 -2
  210. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +0 -2
  211. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +0 -2
  212. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +0 -2
  213. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +0 -2
  214. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +0 -2
  215. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +0 -2
  216. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +0 -266
  217. package/.agents/skills/autoconverge/workflow/render_report.py +0 -1024
  218. package/.agents/skills/autoconverge/workflow/test_aggregate_runs.py +0 -134
  219. package/.agents/skills/autoconverge/workflow/test_convergence_summary.py +0 -149
  220. package/.agents/skills/autoconverge/workflow/test_render_report.py +0 -812
  221. package/.agents/skills/hitl/SKILL.md +0 -45
  222. package/.agents/skills/pr-cleanup/SKILL.md +0 -140
  223. package/.agents/skills/pr-cleanup/reference/process-inventory.md +0 -17
  224. package/.agents/skills/pr-cleanup/reference/task-seeds.md +0 -22
  225. package/.agents/skills/pr-name-by-capability/SKILL.md +0 -73
  226. package/.agents/skills/pr-name-by-capability/reference/fetch-commands.md +0 -29
  227. package/.agents/skills/pr-name-by-capability/reference/offense-examples.md +0 -32
  228. package/.agents/skills/pr-name-by-capability/reference/report-template.md +0 -29
  229. package/.agents/skills/pr-name-by-capability/reference/rule-checklist.md +0 -38
  230. package/.agents/skills/pr-name-by-capability/reference/task-seeds.md +0 -10
  231. package/.agents/skills/pr-plain-language-cleanup/SKILL.md +0 -93
  232. package/.agents/skills/pr-refinement/SKILL.md +0 -31
  233. package/.agents/skills/pr-shared-extraction/SKILL.md +0 -177
  234. package/.agents/skills/pr-shared-extraction/reference/examples.md +0 -46
  235. package/.agents/skills/pr-shared-extraction/reference/offense-taxonomy.md +0 -100
  236. package/.agents/skills/pr-small-cl/SKILL.md +0 -52
  237. package/.agents/skills/pr-title-description/SKILL.md +0 -96
  238. package/.agents/skills/prototype/SKILL.md +0 -85
  239. package/.agents/skills/prototype/reference/honest-limitations.md +0 -23
  240. package/.agents/skills/prototype/reference/promotion-tasks.md +0 -23
  241. package/.agents/skills/prototype/scripts/build_sandbox_settings.py +0 -249
  242. package/.agents/skills/prototype/scripts/conftest.py +0 -15
  243. package/.agents/skills/prototype/scripts/launch_sandbox.py +0 -205
  244. package/.agents/skills/prototype/scripts/probe_sandbox_safety.py +0 -311
  245. package/.agents/skills/prototype/scripts/prototype_scripts_constants/__init__.py +0 -1
  246. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
  247. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +0 -41
  248. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +0 -23
  249. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +0 -45
  250. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +0 -10
  251. package/.agents/skills/prototype/scripts/test_build_sandbox_settings.py +0 -275
  252. package/.agents/skills/prototype/scripts/test_launch_sandbox.py +0 -303
  253. package/.agents/skills/prototype/scripts/test_probe_sandbox_safety.py +0 -312
  254. package/.agents/skills/prototype/workflows/promotion.md +0 -25
  255. package/.agents/skills/prototype/workflows/sandbox.md +0 -35
  256. package/.agents/skills/rebase/.claude/CLAUDE.md +0 -1
  257. package/.agents/skills/rebase/AGENTS.md +0 -1
  258. package/.agents/skills/rebase/SKILL.md +0 -178
  259. package/.agents/skills/review-router/SKILL.md +0 -51
  260. package/.agents/skills/review-router/reference/route-policy.json +0 -7
  261. package/.agents/skills/review-router/reference/task-seeds.md +0 -1
  262. package/.agents/skills/review-router/scripts/conftest.py +0 -4
  263. package/.agents/skills/review-router/scripts/review_router.py +0 -50
  264. package/.agents/skills/review-router/scripts/review_router_cli.py +0 -211
  265. package/.agents/skills/review-router/scripts/review_router_constants/__init__.py +0 -1
  266. package/.agents/skills/review-router/scripts/review_router_constants/config/__init__.py +0 -0
  267. package/.agents/skills/review-router/scripts/review_router_constants/config/constants.py +0 -13
  268. package/.agents/skills/review-router/scripts/test_review_router.py +0 -23
  269. package/.agents/skills/review-router/scripts/test_review_router_cli.py +0 -47
  270. package/.agents/skills/review-tier/SKILL.md +0 -56
  271. package/.agents/skills/review-tier/reference/task-seeds.md +0 -1
  272. package/.agents/skills/review-tier/reference/tier-policy.json +0 -9
  273. package/.agents/skills/review-tier/scripts/conftest.py +0 -4
  274. package/.agents/skills/review-tier/scripts/review_tier.py +0 -359
  275. package/.agents/skills/review-tier/scripts/review_tier_constants/__init__.py +0 -1
  276. package/.agents/skills/review-tier/scripts/review_tier_constants/config/__init__.py +0 -0
  277. package/.agents/skills/review-tier/scripts/review_tier_constants/config/constants.py +0 -29
  278. package/.agents/skills/review-tier/scripts/test_generation.py +0 -159
  279. package/.agents/skills/review-tier/scripts/test_review_tier.py +0 -33
  280. package/.agents/skills/run-claude-dev-env/SKILL.md +0 -84
  281. package/.agents/skills/session-log/.claude/CLAUDE.md +0 -1
  282. package/.agents/skills/session-log/AGENTS.md +0 -1
  283. package/.agents/skills/session-log/SKILL.md +0 -170
  284. package/.agents/skills/session-log/templates/frontmatter.md +0 -40
  285. package/.agents/skills/session-tidy/.claude/CLAUDE.md +0 -1
  286. package/.agents/skills/session-tidy/AGENTS.md +0 -1
  287. package/.agents/skills/session-tidy/SKILL.md +0 -191
  288. package/.agents/skills/skill-builder/references/.claude/CLAUDE.md +0 -1
  289. package/.agents/skills/skill-builder/references/AGENTS.md +0 -1
  290. package/.agents/skills/skill-builder/references/delegation-map.md +0 -130
  291. package/.agents/skills/skill-builder/references/description-field.md +0 -111
  292. package/.agents/skills/skill-builder/references/deterministic-elements.md +0 -218
  293. package/.agents/skills/skill-builder/references/progressive-disclosure.md +0 -122
  294. package/.agents/skills/skill-builder/references/self-audit-checklist.md +0 -126
  295. package/.agents/skills/skill-builder/references/skill-modularity.md +0 -111
  296. package/.agents/skills/skill-builder/references/skill-types.md +0 -232
  297. package/.agents/skills/skill-builder/references/thariq-x-post-skills.json +0 -33
  298. package/.agents/skills/skill-builder/templates/.claude/CLAUDE.md +0 -1
  299. package/.agents/skills/skill-builder/templates/AGENTS.md +0 -1
  300. package/.agents/skills/skill-builder/templates/gap-analysis.md +0 -87
  301. package/.agents/skills/skill-builder/workflows/.claude/CLAUDE.md +0 -1
  302. package/.agents/skills/skill-builder/workflows/AGENTS.md +0 -1
  303. package/.agents/skills/skill-builder/workflows/improve-skill.md +0 -136
  304. package/.agents/skills/skill-builder/workflows/new-skill.md +0 -179
  305. package/.agents/skills/skill-builder/workflows/polish-skill.md +0 -139
  306. package/.agents/skills/source-command-sr-loop/SKILL.md +0 -82
  307. package/.agents/skills/update/.claude/CLAUDE.md +0 -1
  308. package/.agents/skills/update/AGENTS.md +0 -1
  309. package/.agents/skills/update/SKILL.md +0 -190
  310. package/_shared/pr-loop/AGENTS.md +0 -1
  311. package/_shared/pr-loop/CLAUDE.md +0 -1
  312. package/_shared/pr-loop/audit-contract.md +0 -183
  313. package/_shared/pr-loop/audit-reply-template.md +0 -151
  314. package/_shared/pr-loop/code-rules-gate.md +0 -65
  315. package/_shared/pr-loop/fix-protocol.md +0 -60
  316. package/_shared/pr-loop/gh-payloads.md +0 -75
  317. package/_shared/pr-loop/post-audit-thread-contract.md +0 -51
  318. package/_shared/pr-loop/precatch-rubric.md +0 -66
  319. package/_shared/pr-loop/preflight-proposal.md +0 -56
  320. package/_shared/pr-loop/scripts/post_audit_thread.py +0 -1242
  321. package/_shared/pr-loop/scripts/pr_loop_shared_constants/post_audit_thread_constants.py +0 -199
  322. package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +0 -1115
  323. package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +0 -129
  324. package/_shared/pr-loop/state-schema.md +0 -45
  325. package/_shared/pr-loop/worker-spawn.md +0 -189
  326. package/hooks/_gh_pr_author_swap_utils.py +0 -1225
  327. package/hooks/blocking/conventional_pr_title_gate.py +0 -443
  328. package/hooks/blocking/gh_body_arg_blocker.py +0 -176
  329. package/hooks/blocking/gh_pr_author_enforcer.py +0 -487
  330. package/hooks/blocking/gh_pr_author_restore.py +0 -105
  331. package/hooks/blocking/pr_description_writer_gate.py +0 -256
  332. package/hooks/blocking/test_conventional_pr_title_gate.py +0 -640
  333. package/hooks/blocking/test_gh_body_arg_blocker.py +0 -441
  334. package/hooks/blocking/test_gh_pr_author_enforcer.py +0 -1166
  335. package/hooks/blocking/test_gh_pr_author_restore.py +0 -513
  336. package/hooks/blocking/test_gh_pr_author_swap_utils.py +0 -910
  337. package/hooks/blocking/test_pr_description_writer_gate.py +0 -234
  338. package/hooks/blocking/test_preflight_check.py +0 -343
  339. package/hooks/blocking/test_test_preflight_check.py +0 -80
  340. package/hooks/blocking/test_volatile_path_in_post_blocker.py +0 -322
  341. package/hooks/blocking/volatile_path_in_post_blocker.py +0 -461
  342. package/hooks/hooks_constants/conventional_pr_title_gate_constants.py +0 -58
  343. package/hooks/hooks_constants/gh_pr_author_swap_constants.py +0 -76
  344. package/hooks/hooks_constants/pr_description_writer_gate_constants.py +0 -73
  345. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +0 -55
  346. package/hooks/observability/pr_description_writer_spawn_tracker.py +0 -113
  347. package/hooks/observability/test_pr_description_writer_spawn_tracker.py +0 -150
  348. package/hooks/session/gh_pr_author_session_cleanup.py +0 -175
  349. package/hooks/session/test_gh_pr_author_session_cleanup.py +0 -576
  350. package/hooks/test__gh_pr_author_swap_utils.py +0 -333
@@ -0,0 +1,251 @@
1
+ """Restore one explicitly selected legacy GitHub author-swap record."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import json
7
+ import os
8
+ import stat
9
+ import subprocess
10
+ import sys
11
+ import time
12
+ from collections.abc import Callable, Sequence
13
+ from pathlib import Path
14
+
15
+ from github_pr_command_constants.config.constants import (
16
+ ALL_GH_AUTH_SWITCH_COMMAND_HEAD,
17
+ LEGACY_RECORD_ACTIVE_MESSAGE,
18
+ LEGACY_RECORD_CHANGED_MESSAGE,
19
+ LEGACY_RECORD_CONFIRMATION_MESSAGE,
20
+ LEGACY_RECORD_INACTIVE_CONFIRMATION_FLAG,
21
+ LEGACY_RECORD_NAME_PATTERN,
22
+ LEGACY_RECORD_ORIGINAL_ACCOUNT_KEY,
23
+ LEGACY_RECORD_PERMISSION_MODE,
24
+ LEGACY_RECORD_REJECTED_MESSAGE,
25
+ LEGACY_RECORD_STALE_AGE_SECONDS,
26
+ LEGACY_RESTORE_FAILED_MESSAGE,
27
+ RECOVERY_CLEAN_EXIT_CODE,
28
+ RECOVERY_FAILED_EXIT_CODE,
29
+ RECOVERY_REJECTED_EXIT_CODE,
30
+ RECOVERY_UNRESOLVED_EXIT_CODE,
31
+ )
32
+
33
+ CommandRunner = Callable[..., subprocess.CompletedProcess[str]]
34
+ FileFingerprint = tuple[int, int, int, int, int]
35
+ VerifiedRecord = tuple[str, FileFingerprint]
36
+
37
+
38
+ class LegacyRecordRejected(ValueError):
39
+ """Report an invalid legacy state record."""
40
+
41
+
42
+ class LegacyRecordActive(ValueError):
43
+ """Report a legacy state record that is not old enough for recovery."""
44
+
45
+
46
+ class LegacyRecordChanged(ValueError):
47
+ """Report a legacy state record that changed during recovery."""
48
+
49
+
50
+ def _build_parser() -> argparse.ArgumentParser:
51
+ parser = argparse.ArgumentParser(description=__doc__, allow_abbrev=False)
52
+ parser.add_argument("state_file", type=Path)
53
+ parser.add_argument(LEGACY_RECORD_INACTIVE_CONFIRMATION_FLAG, action="store_true")
54
+ return parser
55
+
56
+
57
+ def _posix_metadata_is_secure(
58
+ file_mode: int,
59
+ file_owner_id: int,
60
+ current_user_id: int | None,
61
+ ) -> bool:
62
+ if not stat.S_ISREG(file_mode):
63
+ return False
64
+ if current_user_id is None:
65
+ return True
66
+ return (
67
+ stat.S_IMODE(file_mode) == LEGACY_RECORD_PERMISSION_MODE
68
+ and file_owner_id == current_user_id
69
+ )
70
+
71
+
72
+ def _secure_metadata(
73
+ state_file: Path,
74
+ current_user_id: int | None,
75
+ ) -> os.stat_result:
76
+ try:
77
+ file_metadata = state_file.lstat()
78
+ except OSError as error:
79
+ raise LegacyRecordRejected from error
80
+ if not _posix_metadata_is_secure(
81
+ file_metadata.st_mode,
82
+ file_metadata.st_uid,
83
+ current_user_id,
84
+ ):
85
+ raise LegacyRecordRejected
86
+ return file_metadata
87
+
88
+
89
+ def _fingerprint(file_metadata: os.stat_result) -> FileFingerprint:
90
+ return (
91
+ file_metadata.st_dev,
92
+ file_metadata.st_ino,
93
+ file_metadata.st_mode,
94
+ file_metadata.st_size,
95
+ file_metadata.st_mtime_ns,
96
+ )
97
+
98
+
99
+ def _parse_original_account(record_text: str) -> str:
100
+ try:
101
+ decoded_record = json.loads(record_text)
102
+ except json.JSONDecodeError as error:
103
+ raise LegacyRecordRejected from error
104
+ if not isinstance(decoded_record, dict):
105
+ raise LegacyRecordRejected
106
+ if set(decoded_record) != {LEGACY_RECORD_ORIGINAL_ACCOUNT_KEY}:
107
+ raise LegacyRecordRejected
108
+ original_account = decoded_record[LEGACY_RECORD_ORIGINAL_ACCOUNT_KEY]
109
+ if not isinstance(original_account, str) or not original_account.strip():
110
+ raise LegacyRecordRejected
111
+ return original_account.strip()
112
+
113
+
114
+ def _read_verified_record(
115
+ state_file: Path,
116
+ now_seconds: float,
117
+ current_user_id: int | None,
118
+ ) -> VerifiedRecord:
119
+ if LEGACY_RECORD_NAME_PATTERN.fullmatch(state_file.name) is None:
120
+ raise LegacyRecordRejected
121
+ initial_metadata = _secure_metadata(state_file, current_user_id)
122
+ if now_seconds - initial_metadata.st_mtime < LEGACY_RECORD_STALE_AGE_SECONDS:
123
+ raise LegacyRecordActive
124
+ try:
125
+ record_text = state_file.read_text(encoding="utf-8")
126
+ except (OSError, UnicodeError) as error:
127
+ raise LegacyRecordRejected from error
128
+ original_account = _parse_original_account(record_text)
129
+ initial_fingerprint = _fingerprint(initial_metadata)
130
+ verified_metadata = _secure_metadata(state_file, current_user_id)
131
+ if _fingerprint(verified_metadata) != initial_fingerprint:
132
+ raise LegacyRecordChanged
133
+ return original_account, initial_fingerprint
134
+
135
+
136
+ def _delete_unchanged_record(
137
+ state_file: Path,
138
+ expected_fingerprint: FileFingerprint,
139
+ current_user_id: int | None,
140
+ ) -> None:
141
+ current_metadata = _secure_metadata(state_file, current_user_id)
142
+ if _fingerprint(current_metadata) != expected_fingerprint:
143
+ raise LegacyRecordChanged
144
+ try:
145
+ state_file.unlink()
146
+ except OSError as error:
147
+ raise LegacyRecordChanged from error
148
+
149
+
150
+ def _load_record_with_diagnostics(
151
+ state_file: Path,
152
+ now_seconds: float,
153
+ current_user_id: int | None,
154
+ ) -> tuple[VerifiedRecord | None, int]:
155
+ try:
156
+ return _read_verified_record(state_file, now_seconds, current_user_id), 0
157
+ except LegacyRecordActive:
158
+ sys.stderr.write(LEGACY_RECORD_ACTIVE_MESSAGE)
159
+ return None, RECOVERY_REJECTED_EXIT_CODE
160
+ except LegacyRecordChanged:
161
+ sys.stderr.write(LEGACY_RECORD_CHANGED_MESSAGE)
162
+ return None, RECOVERY_FAILED_EXIT_CODE
163
+ except LegacyRecordRejected:
164
+ sys.stderr.write(LEGACY_RECORD_REJECTED_MESSAGE)
165
+ return None, RECOVERY_REJECTED_EXIT_CODE
166
+
167
+
168
+ def _restore_and_delete(
169
+ state_file: Path,
170
+ verified_record: VerifiedRecord,
171
+ current_user_id: int | None,
172
+ command_runner: CommandRunner,
173
+ ) -> int:
174
+ original_account, expected_fingerprint = verified_record
175
+ try:
176
+ completed_restore = command_runner(
177
+ [*ALL_GH_AUTH_SWITCH_COMMAND_HEAD, original_account],
178
+ capture_output=True,
179
+ check=False,
180
+ shell=False,
181
+ text=True,
182
+ )
183
+ except OSError:
184
+ sys.stderr.write(LEGACY_RESTORE_FAILED_MESSAGE)
185
+ return RECOVERY_FAILED_EXIT_CODE
186
+ if completed_restore.returncode != 0:
187
+ sys.stderr.write(LEGACY_RESTORE_FAILED_MESSAGE)
188
+ return RECOVERY_FAILED_EXIT_CODE
189
+ try:
190
+ _delete_unchanged_record(state_file, expected_fingerprint, current_user_id)
191
+ except (LegacyRecordChanged, LegacyRecordRejected):
192
+ sys.stderr.write(LEGACY_RECORD_CHANGED_MESSAGE)
193
+ return RECOVERY_FAILED_EXIT_CODE
194
+ return RECOVERY_CLEAN_EXIT_CODE
195
+
196
+
197
+ def _recover_selected_record(
198
+ state_file: Path,
199
+ now_seconds: float,
200
+ current_user_id: int | None,
201
+ command_runner: CommandRunner,
202
+ ) -> int:
203
+ verified_record, exit_code = _load_record_with_diagnostics(
204
+ state_file, now_seconds, current_user_id
205
+ )
206
+ if verified_record is None:
207
+ return exit_code
208
+ return _restore_and_delete(
209
+ state_file, verified_record, current_user_id, command_runner
210
+ )
211
+
212
+
213
+ def main(
214
+ all_arguments: Sequence[str],
215
+ *,
216
+ now_seconds: float,
217
+ current_user_id: int | None,
218
+ command_runner: CommandRunner,
219
+ ) -> int:
220
+ """Recover one confirmed-inactive legacy state record.
221
+
222
+ Args:
223
+ all_arguments: Command arguments after the script name.
224
+ now_seconds: Current wall-clock time in seconds.
225
+ current_user_id: Current POSIX user identifier, or None on Windows.
226
+ command_runner: Process runner used by tests and production.
227
+
228
+ Returns:
229
+ Zero on success, one on failure, two on rejection, or three when unresolved.
230
+ """
231
+ arguments = _build_parser().parse_args(list(all_arguments))
232
+ if not arguments.confirm_inactive:
233
+ sys.stderr.write(LEGACY_RECORD_CONFIRMATION_MESSAGE)
234
+ return RECOVERY_UNRESOLVED_EXIT_CODE
235
+ return _recover_selected_record(
236
+ arguments.state_file,
237
+ now_seconds,
238
+ current_user_id,
239
+ command_runner,
240
+ )
241
+
242
+
243
+ if __name__ == "__main__":
244
+ raise SystemExit(
245
+ main(
246
+ sys.argv[1:],
247
+ now_seconds=time.time(),
248
+ current_user_id=os.getuid() if hasattr(os, "getuid") else None,
249
+ command_runner=subprocess.run,
250
+ )
251
+ )
@@ -0,0 +1,58 @@
1
+ """Tests for explicit GitHub pull request command constants."""
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ _SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
7
+ if str(_SCRIPTS_DIRECTORY) not in sys.path:
8
+ sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
9
+
10
+ from github_pr_command_constants.config import constants
11
+
12
+
13
+ def test_action_and_linter_mappings_are_complete() -> None:
14
+ assert constants.ALL_LINTER_ACTIONS_BY_COMMAND == {
15
+ constants.ACTION_COMMENT: constants.LINTER_ACTION_COMMENT,
16
+ constants.ACTION_CREATE: constants.LINTER_ACTION_CREATE,
17
+ constants.ACTION_EDIT: constants.LINTER_ACTION_EDIT,
18
+ constants.ACTION_REVIEW: constants.LINTER_ACTION_REVIEW,
19
+ }
20
+
21
+
22
+ def test_review_event_flags_are_complete() -> None:
23
+ assert constants.ALL_REVIEW_FLAGS_BY_EVENT == {
24
+ constants.REVIEW_EVENT_APPROVE: "--approve",
25
+ constants.REVIEW_EVENT_COMMENT: "--comment",
26
+ constants.REVIEW_EVENT_REQUEST_CHANGES: "--request-changes",
27
+ }
28
+
29
+
30
+ def test_account_boundary_constants_are_safe_and_stable() -> None:
31
+ assert constants.SELECTED_ACCOUNT_ENVIRONMENT_KEY == "GITHUB_DEFAULT_ACCOUNT"
32
+ assert constants.ACCOUNT_LOOKUP_FAILURE_EXIT_CODE == 1
33
+ assert constants.ACCOUNT_LOOKUP_FAILED_MESSAGE.endswith("lookup failed\n")
34
+ assert constants.ACCOUNT_LOOKUP_EMPTY_MESSAGE.endswith("returned no value\n")
35
+
36
+
37
+ def test_durable_post_linter_location_uses_managed_root() -> None:
38
+ assert constants.CONFIG_DIR_OVERRIDE_ENVIRONMENT_KEY == "CLAUDE_CONFIG_DIR"
39
+ assert constants.DEFAULT_MANAGED_ROOT_DIRECTORY_NAME == ".claude"
40
+ assert constants.ALL_DURABLE_POST_LINTER_RELATIVE_PATH_PARTS == (
41
+ "scripts",
42
+ "durable_post_lint.py",
43
+ )
44
+
45
+
46
+ def test_legacy_recovery_constants_define_one_record_boundary() -> None:
47
+ assert constants.ALL_GH_AUTH_SWITCH_COMMAND_HEAD == (
48
+ "gh",
49
+ "auth",
50
+ "switch",
51
+ "--user",
52
+ )
53
+ assert constants.LEGACY_RECORD_NAME_PATTERN.fullmatch(
54
+ "gh_pr_author_swap_session-a.json"
55
+ )
56
+ assert not constants.LEGACY_RECORD_NAME_PATTERN.fullmatch("other.json")
57
+ assert constants.LEGACY_RECORD_STALE_AGE_SECONDS == 1800
58
+ assert constants.RECOVERY_UNRESOLVED_EXIT_CODE == 3
@@ -0,0 +1,396 @@
1
+ """Contract tests for process-local pull request publication."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import subprocess
7
+ import sys
8
+ import threading
9
+ from collections.abc import Callable
10
+ from concurrent.futures import ThreadPoolExecutor
11
+ from pathlib import Path
12
+ from unittest.mock import Mock
13
+
14
+ import pytest
15
+
16
+ _SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
17
+ if str(_SCRIPTS_DIRECTORY) not in sys.path:
18
+ sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
19
+
20
+ import pull_request
21
+
22
+ REPOSITORY = "jl-cmd/claude-dev-env"
23
+ PR_NUMBER = "2518"
24
+ PR_TITLE = "feat(skills): add guarded pull request publication"
25
+ ACCOUNT_A = "JonEcho"
26
+ ACCOUNT_B = "ReviewBot"
27
+ BODY_FILENAME = "pull-request-body.md"
28
+ LINTER_PATH = _SCRIPTS_DIRECTORY.parents[3] / "scripts/durable_post_lint.py"
29
+ COMMENT_CLI = ["comment", "--repo", REPOSITORY, "--number", PR_NUMBER, "--body-file"]
30
+ ACTION_CASES = [
31
+ (
32
+ [
33
+ "create",
34
+ "--repo",
35
+ REPOSITORY,
36
+ "--base",
37
+ "main",
38
+ "--head",
39
+ "feature/post",
40
+ "--title",
41
+ PR_TITLE,
42
+ "--body-file",
43
+ BODY_FILENAME,
44
+ ],
45
+ "pr-create",
46
+ [
47
+ "gh",
48
+ "pr",
49
+ "create",
50
+ "--draft",
51
+ "--repo",
52
+ REPOSITORY,
53
+ "--base",
54
+ "main",
55
+ "--head",
56
+ "feature/post",
57
+ "--title",
58
+ PR_TITLE,
59
+ "--body-file",
60
+ BODY_FILENAME,
61
+ ],
62
+ ),
63
+ (
64
+ [
65
+ "edit",
66
+ "--repo",
67
+ REPOSITORY,
68
+ "--number",
69
+ PR_NUMBER,
70
+ "--title",
71
+ PR_TITLE,
72
+ "--body-file",
73
+ BODY_FILENAME,
74
+ ],
75
+ "pr-edit",
76
+ [
77
+ "gh",
78
+ "pr",
79
+ "edit",
80
+ PR_NUMBER,
81
+ "--repo",
82
+ REPOSITORY,
83
+ "--title",
84
+ PR_TITLE,
85
+ "--body-file",
86
+ BODY_FILENAME,
87
+ ],
88
+ ),
89
+ (
90
+ [*COMMENT_CLI, BODY_FILENAME],
91
+ "pr-comment",
92
+ [
93
+ "gh",
94
+ "pr",
95
+ "comment",
96
+ PR_NUMBER,
97
+ "--repo",
98
+ REPOSITORY,
99
+ "--body-file",
100
+ BODY_FILENAME,
101
+ ],
102
+ ),
103
+ (
104
+ [
105
+ "review",
106
+ "--repo",
107
+ REPOSITORY,
108
+ "--number",
109
+ PR_NUMBER,
110
+ "--body-file",
111
+ BODY_FILENAME,
112
+ ],
113
+ "pr-review",
114
+ [
115
+ "gh",
116
+ "pr",
117
+ "review",
118
+ PR_NUMBER,
119
+ "--repo",
120
+ REPOSITORY,
121
+ "--comment",
122
+ "--body-file",
123
+ BODY_FILENAME,
124
+ ],
125
+ ),
126
+ ]
127
+
128
+
129
+ def _completion(
130
+ all_arguments: list[str], code: int = 0, stdout: str = "", stderr: str = ""
131
+ ) -> subprocess.CompletedProcess[str]:
132
+ return subprocess.CompletedProcess(all_arguments, code, stdout, stderr)
133
+
134
+
135
+ @pytest.fixture
136
+ def body_file(tmp_path: Path) -> Path:
137
+ post_file = tmp_path / BODY_FILENAME
138
+ post_file.write_text("Durable body text.", encoding="utf-8")
139
+ return post_file
140
+
141
+
142
+ def _comment_arguments(body_file: Path) -> list[str]:
143
+ return [*COMMENT_CLI, str(body_file)]
144
+
145
+
146
+ def _account_text(account_name: str) -> str:
147
+ return account_name[::-1]
148
+
149
+
150
+ def _calls(command_runner: Mock) -> list[list[str]]:
151
+ return [each_call.args[0] for each_call in command_runner.call_args_list]
152
+
153
+
154
+ def _run_comment(
155
+ body_file: Path, command_runner: Mock, environment: dict[str, str]
156
+ ) -> int:
157
+ return pull_request.main(
158
+ _comment_arguments(body_file),
159
+ all_environment=environment,
160
+ command_runner=command_runner,
161
+ )
162
+
163
+
164
+ def _account_runner(
165
+ *, auth_code: int = 0, auth_stdout: str, action_code: int = 0, auth_stderr: str = ""
166
+ ) -> Mock:
167
+ return Mock(
168
+ side_effect=[
169
+ _completion([]),
170
+ _completion([], auth_code, auth_stdout, auth_stderr),
171
+ _completion([], action_code),
172
+ ]
173
+ )
174
+
175
+
176
+ @pytest.mark.parametrize(
177
+ ("all_cli_arguments", "linter_action", "expected_gh"), ACTION_CASES
178
+ )
179
+ def test_actions_run_exact_linter_then_gh_arguments(
180
+ all_cli_arguments: list[str], linter_action: str, expected_gh: list[str]
181
+ ) -> None:
182
+ command_runner = Mock(side_effect=[_completion([]), _completion([])])
183
+ exit_code = pull_request.main(
184
+ all_cli_arguments, all_environment={}, command_runner=command_runner
185
+ )
186
+ title_arguments = ["--title", PR_TITLE] if "--title" in all_cli_arguments else []
187
+ expected_linter = [
188
+ sys.executable,
189
+ str(LINTER_PATH),
190
+ "--action",
191
+ linter_action,
192
+ *title_arguments,
193
+ "--body-file",
194
+ BODY_FILENAME,
195
+ ]
196
+ assert exit_code == 0
197
+ assert _calls(command_runner) == [expected_linter, expected_gh]
198
+
199
+
200
+ def test_body_transport_uses_files_only(body_file: Path) -> None:
201
+ command_runner = Mock(side_effect=[_completion([]), _completion([])])
202
+ assert _run_comment(body_file, command_runner, {}) == 0
203
+ for each_command in _calls(command_runner):
204
+ assert "--body-file" in each_command
205
+ assert "--body" not in each_command
206
+ assert "Durable body text." not in each_command
207
+
208
+
209
+ def test_linter_failure_stops_credential_lookup_and_action(body_file: Path) -> None:
210
+ command_runner = Mock(return_value=_completion([], 1))
211
+ exit_code = _run_comment(
212
+ body_file, command_runner, {"GITHUB_DEFAULT_ACCOUNT": ACCOUNT_A}
213
+ )
214
+ assert exit_code == 1
215
+ assert command_runner.call_count == 1
216
+ assert Path(_calls(command_runner)[0][1]).name == "durable_post_lint.py"
217
+
218
+
219
+ def test_account_lookup_scopes_action_environment(body_file: Path) -> None:
220
+ account_text = _account_text(ACCOUNT_A)
221
+ command_runner = _account_runner(auth_stdout=f"{account_text}\n")
222
+ parent_environment = {
223
+ "GITHUB_DEFAULT_ACCOUNT": ACCOUNT_A,
224
+ "GITHUB_TOKEN": "parent-setting",
225
+ "UNCHANGED": "same",
226
+ }
227
+ assert _run_comment(body_file, command_runner, parent_environment) == 0
228
+ assert _calls(command_runner)[1] == ["gh", "auth", "token", "--user", ACCOUNT_A]
229
+ action_environment = command_runner.call_args_list[2].kwargs["env"]
230
+ assert action_environment["GH_TOKEN"] == account_text
231
+ assert "GITHUB_TOKEN" not in action_environment
232
+ assert action_environment["UNCHANGED"] == "same"
233
+
234
+
235
+ def test_absent_account_uses_copied_parent_environment(body_file: Path) -> None:
236
+ command_runner = Mock(side_effect=[_completion([]), _completion([])])
237
+ parent_environment = {"GITHUB_TOKEN": "parent-setting", "UNCHANGED": "same"}
238
+ assert _run_comment(body_file, command_runner, parent_environment) == 0
239
+ action_environment = command_runner.call_args_list[1].kwargs["env"]
240
+ assert action_environment == parent_environment
241
+ assert action_environment is not parent_environment
242
+ assert command_runner.call_count == 2
243
+
244
+
245
+ def test_token_lookup_failure_prints_generic_safe_stderr(
246
+ body_file: Path, capsys: pytest.CaptureFixture[str]
247
+ ) -> None:
248
+ account_text = _account_text(ACCOUNT_A)
249
+ command_runner = _account_runner(
250
+ auth_code=1, auth_stdout=account_text, auth_stderr=f"unsafe {account_text}"
251
+ )
252
+ exit_code = _run_comment(
253
+ body_file, command_runner, {"GITHUB_DEFAULT_ACCOUNT": ACCOUNT_A}
254
+ )
255
+ captured = capsys.readouterr()
256
+ assert exit_code == 1
257
+ assert captured.err == "error: GitHub account lookup failed\n"
258
+ assert account_text not in captured.out + captured.err
259
+ assert command_runner.call_count == 2
260
+ command_runner = Mock(side_effect=[_completion([]), OSError("unavailable")])
261
+ assert (
262
+ _run_comment(body_file, command_runner, {"GITHUB_DEFAULT_ACCOUNT": ACCOUNT_A})
263
+ == 1
264
+ )
265
+ assert capsys.readouterr().err == "error: GitHub account lookup failed\n"
266
+
267
+
268
+ def test_empty_account_token_stops_before_action(
269
+ body_file: Path, capsys: pytest.CaptureFixture[str]
270
+ ) -> None:
271
+ command_runner = _account_runner(auth_stdout=" \n")
272
+ exit_code = _run_comment(
273
+ body_file, command_runner, {"GITHUB_DEFAULT_ACCOUNT": ACCOUNT_A}
274
+ )
275
+ captured = capsys.readouterr()
276
+ assert exit_code == 1
277
+ assert captured.err == "error: GitHub account lookup returned no value\n"
278
+ assert command_runner.call_count == 2
279
+
280
+
281
+ def test_action_child_results_pass_through_or_report_safely(
282
+ body_file: Path, capsys: pytest.CaptureFixture[str]
283
+ ) -> None:
284
+ command_runner = Mock(side_effect=[_completion([]), _completion([], 7)])
285
+ assert _run_comment(body_file, command_runner, {}) == 7
286
+ command_runner = Mock(side_effect=[_completion([]), OSError("unavailable")])
287
+ assert _run_comment(body_file, command_runner, {}) == 1
288
+ assert capsys.readouterr().err == "error: GitHub action failed\n"
289
+
290
+
291
+ def test_supplied_parent_environment_remains_unchanged(body_file: Path) -> None:
292
+ command_runner = _account_runner(auth_stdout=_account_text(ACCOUNT_A))
293
+ parent_environment = {
294
+ "GITHUB_DEFAULT_ACCOUNT": ACCOUNT_A,
295
+ "GITHUB_TOKEN": "parent-setting",
296
+ "GH_TOKEN": "parent-setting",
297
+ }
298
+ original_environment = dict(parent_environment)
299
+ assert _run_comment(body_file, command_runner, parent_environment) == 0
300
+ assert parent_environment == original_environment
301
+
302
+
303
+ def test_process_environment_remains_unchanged(
304
+ body_file: Path, monkeypatch: pytest.MonkeyPatch
305
+ ) -> None:
306
+ monkeypatch.setenv("GITHUB_DEFAULT_ACCOUNT", ACCOUNT_A)
307
+ monkeypatch.setenv("GITHUB_TOKEN", "parent-setting")
308
+ monkeypatch.setenv("GH_TOKEN", "parent-setting")
309
+ original_environment = dict(os.environ)
310
+ command_runner = _account_runner(auth_stdout=_account_text(ACCOUNT_A))
311
+ exit_code = pull_request.main(
312
+ _comment_arguments(body_file),
313
+ all_environment=os.environ,
314
+ command_runner=command_runner,
315
+ )
316
+ assert exit_code == 0
317
+ assert dict(os.environ) == original_environment
318
+
319
+
320
+ def _concurrent_runner(
321
+ all_environments: list[dict[str, str]],
322
+ all_commands: list[list[str]],
323
+ record_lock: threading.Lock,
324
+ ) -> Callable[..., subprocess.CompletedProcess[str]]:
325
+ def run_command(
326
+ all_arguments: list[str], **keyword_arguments: object
327
+ ) -> subprocess.CompletedProcess[str]:
328
+ with record_lock:
329
+ all_commands.append(all_arguments)
330
+ if all_arguments[0] == sys.executable:
331
+ return _completion(all_arguments)
332
+ if all_arguments[:3] == ["gh", "auth", "token"]:
333
+ if all_arguments[-1] == ACCOUNT_A:
334
+ return _completion(all_arguments, stdout=_account_text(ACCOUNT_A))
335
+ return _completion(all_arguments, 1)
336
+ child_environment = keyword_arguments["env"]
337
+ assert isinstance(child_environment, dict)
338
+ with record_lock:
339
+ all_environments.append(dict(child_environment))
340
+ return _completion(all_arguments)
341
+
342
+ return run_command
343
+
344
+
345
+ def test_concurrent_accounts_keep_tokens_isolated(body_file: Path) -> None:
346
+ all_environments: list[dict[str, str]] = []
347
+ all_commands: list[list[str]] = []
348
+ command_runner = _concurrent_runner(
349
+ all_environments, all_commands, threading.Lock()
350
+ )
351
+ with ThreadPoolExecutor(max_workers=2) as executor:
352
+ first_run = executor.submit(
353
+ pull_request.main,
354
+ _comment_arguments(body_file),
355
+ all_environment={"GITHUB_DEFAULT_ACCOUNT": ACCOUNT_A},
356
+ command_runner=command_runner,
357
+ )
358
+ second_run = executor.submit(
359
+ pull_request.main,
360
+ _comment_arguments(body_file),
361
+ all_environment={"GITHUB_DEFAULT_ACCOUNT": ACCOUNT_B},
362
+ command_runner=command_runner,
363
+ )
364
+ assert {first_run.result(), second_run.result()} == {0, 1}
365
+ assert len(all_environments) == 1
366
+ assert all_environments[0]["GH_TOKEN"] == _account_text(ACCOUNT_A)
367
+ assert all(
368
+ each_command[:3] != ["gh", "auth", "switch"] for each_command in all_commands
369
+ )
370
+
371
+
372
+ def test_token_never_reaches_captured_output(
373
+ body_file: Path, capsys: pytest.CaptureFixture[str]
374
+ ) -> None:
375
+ account_text = _account_text(ACCOUNT_A)
376
+ command_runner = _account_runner(auth_stdout=f"{account_text}\n")
377
+ assert (
378
+ _run_comment(body_file, command_runner, {"GITHUB_DEFAULT_ACCOUNT": ACCOUNT_A})
379
+ == 0
380
+ )
381
+ captured = capsys.readouterr()
382
+ assert account_text not in captured.out + captured.err
383
+
384
+
385
+ def test_cli_rejects_inline_body(capsys: pytest.CaptureFixture[str]) -> None:
386
+ command_runner = Mock()
387
+ with pytest.raises(SystemExit) as raised_exit:
388
+ pull_request.main(
389
+ [*COMMENT_CLI[:-1], "--body", "Inline body text."],
390
+ all_environment={},
391
+ command_runner=command_runner,
392
+ )
393
+ captured = capsys.readouterr()
394
+ assert raised_exit.value.code == 2
395
+ assert "--body-file" in captured.err
396
+ command_runner.assert_not_called()