claude-dev-env 4.2.0 → 7.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (350) hide show
  1. package/.agents/agents/pr-description-writer.md +2 -2
  2. package/.agents/agents/test_agent_frontmatter.py +2 -2
  3. package/.agents/agents/test_pr_description_writer_style.py +1 -3
  4. package/.agents/skills/e-code-review/SKILL.md +12 -12
  5. package/.agents/skills/e-code-review/preflight-proposal.contract.test.mjs +20 -0
  6. package/.agents/skills/e-code-review/reference/fix.md +15 -17
  7. package/.agents/skills/e-code-review/reference/loop.md +35 -39
  8. package/.agents/skills/e-code-review/reference/mode-contract.test.mjs +2 -1
  9. package/.agents/skills/e-code-review/reference/preflight-proposal.md +44 -9
  10. package/.agents/skills/e-code-review/reference/runner-selection.md +13 -14
  11. package/.agents/skills/issue-tracker/SKILL.md +7 -0
  12. package/.agents/skills/pull-request/SKILL.md +216 -0
  13. package/.agents/skills/pull-request/reference/publication-tasks.md +18 -0
  14. package/.agents/skills/pull-request/scripts/github_pr_command_constants/__init__.py +1 -0
  15. package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/__init__.py +1 -0
  16. package/.agents/skills/pull-request/scripts/github_pr_command_constants/config/constants.py +69 -0
  17. package/.agents/skills/pull-request/scripts/pull_request.py +265 -0
  18. package/.agents/skills/pull-request/scripts/recover_legacy_author.py +251 -0
  19. package/.agents/skills/pull-request/scripts/test_github_pr_command_constants.py +58 -0
  20. package/.agents/skills/pull-request/scripts/test_pull_request.py +396 -0
  21. package/.agents/skills/pull-request/scripts/test_recover_legacy_author.py +221 -0
  22. package/.agents/skills/skill-builder/SKILL.md +3 -196
  23. package/.agents/skills/test-runner/SKILL.md +69 -0
  24. package/.agents/skills/test-runner/scripts/preflight_checks.py +365 -0
  25. package/.agents/skills/test-runner/scripts/run_tests.py +316 -0
  26. package/.agents/skills/test-runner/scripts/test_preflight_checks.py +185 -0
  27. package/.agents/skills/test-runner/scripts/test_run_tests.py +281 -0
  28. package/.agents/skills/test-runner/scripts/test_runner_constants/__init__.py +1 -0
  29. package/.agents/skills/test-runner/scripts/test_runner_constants/config/__init__.py +1 -0
  30. package/.agents/skills/test-runner/scripts/test_runner_constants/config/constants.py +96 -0
  31. package/.agents/skills-archived/pr-loop-cloud-transport/reference/identity-and-hooks.md +1 -1
  32. package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +6 -0
  33. package/_shared/pr-loop/scripts/terminology_sweep.py +64 -19
  34. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +59 -0
  35. package/bin/ever-shipped-skills.mjs +2 -0
  36. package/bin/ever-shipped-skills.test.mjs +8 -0
  37. package/bin/install.agents-home.test.mjs +15 -13
  38. package/bin/install.mjs +37 -1
  39. package/bin/install.policy-lint.test.mjs +78 -0
  40. package/bin/install.prune.test.mjs +149 -20
  41. package/bin/install.pstack-plugin.test.mjs +114 -0
  42. package/bin/install.test.mjs +61 -7
  43. package/bin/refresh-pstack-plugin-skills.test.mjs +211 -0
  44. package/hooks/advisory/pr_done_reminder.py +342 -0
  45. package/hooks/advisory/test_pr_done_reminder.py +249 -0
  46. package/hooks/blocking/bash_post_call_dispatcher.py +53 -6
  47. package/hooks/blocking/code_rules_paired_test.py +22 -15
  48. package/hooks/blocking/pii_prevention_blocker.py +32 -51
  49. package/hooks/blocking/test_bash_post_call_dispatcher.py +55 -17
  50. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +1 -4
  51. package/hooks/blocking/test_code_rules_command_dispatch.py +2 -2
  52. package/hooks/blocking/test_code_rules_enforcer_paired_test.py +36 -29
  53. package/hooks/blocking/test_code_rules_js_resume_task_enumeration.py +4 -4
  54. package/hooks/blocking/test_code_rules_js_returns_object_schemaless.py +5 -5
  55. package/hooks/blocking/test_pii_prevention_blocker.py +37 -89
  56. package/hooks/blocking/test_pre_tool_use_dispatcher.py +68 -133
  57. package/hooks/blocking/test_pre_tool_use_dispatcher_native.py +20 -51
  58. package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +18 -18
  59. package/hooks/gh_post_body_texts.py +195 -0
  60. package/hooks/git-hooks/git_hooks_constants/staged_policy_lint.py +12 -0
  61. package/hooks/git-hooks/pre_commit.py +52 -11
  62. package/hooks/git-hooks/test_installed_policy_lint.py +159 -0
  63. package/hooks/git-hooks/test_native_policy_commit.py +87 -0
  64. package/hooks/git-hooks/test_pre_commit.py +246 -0
  65. package/hooks/hooks.json +1 -16
  66. package/hooks/hooks_constants/bash_post_call_dispatcher_constants.py +14 -2
  67. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -8
  68. package/hooks/hooks_constants/gh_post_body_texts_constants.py +12 -0
  69. package/hooks/hooks_constants/pr_done_reminder_constants.py +98 -0
  70. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -43
  71. package/hooks/hooks_constants/test_bash_post_call_dispatcher_constants.py +17 -7
  72. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +0 -7
  73. package/hooks/hooks_constants/test_failure_recorder_constants.py +3 -3
  74. package/hooks/hooks_constants/test_gh_post_body_texts_constants.py +19 -0
  75. package/hooks/hooks_constants/test_pr_done_reminder_constants.py +40 -0
  76. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +20 -32
  77. package/hooks/observability/test_failure_recorder.py +1 -1
  78. package/hooks/pyproject.toml +1 -1
  79. package/hooks/test_gh_post_body_texts.py +79 -0
  80. package/package.json +2 -2
  81. package/rules/durable-post-artifacts.md +32 -20
  82. package/rules/gh-cli-conventions.md +23 -13
  83. package/rules/git-workflow.md +21 -8
  84. package/scripts/check.ps1 +0 -1
  85. package/{.agents/skills/run-claude-dev-env/driver.mjs → scripts/ci/installer-lifecycle.mjs} +3 -28
  86. package/scripts/ci/installer-lifecycle.test.mjs +26 -0
  87. package/scripts/ci/windows-installer-lifecycle.ps1 +1 -11
  88. package/scripts/dev_env_scripts_constants/shared_tree_constants.py +4 -5
  89. package/scripts/durable_post_lint.py +206 -0
  90. package/scripts/durable_post_lint_config/config/constants.py +96 -0
  91. package/scripts/durable_post_lint_config/config/test_constants.py +25 -0
  92. package/scripts/policy_lint/adapter_configuration.py +53 -12
  93. package/scripts/policy_lint/adapter_detectors.py +13 -6
  94. package/scripts/policy_lint/adapters.py +7 -4
  95. package/scripts/policy_lint/config/constants.py +5 -0
  96. package/scripts/policy_lint/engine.py +48 -4
  97. package/scripts/policy_lint/model.py +1 -0
  98. package/scripts/policy_lint/registry.py +1 -1
  99. package/scripts/policy_lint/selection.py +2 -1
  100. package/scripts/policy_lint/selection_git.py +2 -0
  101. package/scripts/refresh_pstack_plugin_skills.mjs +200 -0
  102. package/scripts/repository_checks/config/constants.py +0 -5
  103. package/scripts/shared_tree_paths.py +32 -3
  104. package/scripts/test_durable_post_lint.py +332 -0
  105. package/scripts/test_shared_tree_paths.py +33 -0
  106. package/scripts/tests/test_adapter_configuration.py +54 -0
  107. package/scripts/tests/test_policy_lint_rules_archive.py +120 -0
  108. package/scripts/tests/test_policy_lint_rules_engine.py +25 -4
  109. package/scripts/tests/test_policy_lint_rules_installed_loader.py +29 -0
  110. package/scripts/tests/test_policy_lint_rules_registry.py +227 -13
  111. package/scripts/tests/test_policy_lint_selection.py +25 -2
  112. package/scripts/tests/test_policy_lint_selection_native_index.py +30 -0
  113. package/scripts/tests/test_rules.py +3 -7
  114. package/.agents/skills/_shared/pr-loop/.claude/CLAUDE.md +0 -1
  115. package/.agents/skills/_shared/pr-loop/AGENTS.md +0 -1
  116. package/.agents/skills/_shared/pr-loop/audit-contract.md +0 -5
  117. package/.agents/skills/_shared/pr-loop/audit-reply-template.md +0 -5
  118. package/.agents/skills/_shared/pr-loop/code-rules-gate.md +0 -5
  119. package/.agents/skills/_shared/pr-loop/fix-protocol.md +0 -5
  120. package/.agents/skills/_shared/pr-loop/gh-payloads.md +0 -5
  121. package/.agents/skills/_shared/pr-loop/portable-driver.md +0 -149
  122. package/.agents/skills/_shared/pr-loop/post-audit-thread-contract.md +0 -5
  123. package/.agents/skills/_shared/pr-loop/precatch-rubric.md +0 -5
  124. package/.agents/skills/_shared/pr-loop/preflight-proposal.contract.test.mjs +0 -119
  125. package/.agents/skills/_shared/pr-loop/preflight-proposal.md +0 -5
  126. package/.agents/skills/_shared/pr-loop/prompts/.claude/CLAUDE.md +0 -1
  127. package/.agents/skills/_shared/pr-loop/prompts/AGENTS.md +0 -1
  128. package/.agents/skills/_shared/pr-loop/prompts/pr-consistency-audit.xml +0 -324
  129. package/.agents/skills/_shared/pr-loop/scripts/.claude/CLAUDE.md +0 -1
  130. package/.agents/skills/_shared/pr-loop/scripts/AGENTS.md +0 -1
  131. package/.agents/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +0 -29
  132. package/.agents/skills/_shared/pr-loop/scripts/_cli_utils.py +0 -22
  133. package/.agents/skills/_shared/pr-loop/scripts/_path_resolver.py +0 -186
  134. package/.agents/skills/_shared/pr-loop/scripts/_xml_utils.py +0 -20
  135. package/.agents/skills/_shared/pr-loop/scripts/audit_category_schema.py +0 -355
  136. package/.agents/skills/_shared/pr-loop/scripts/build_audit_prompt.py +0 -289
  137. package/.agents/skills/_shared/pr-loop/scripts/build_converge_task_list.py +0 -310
  138. package/.agents/skills/_shared/pr-loop/scripts/build_fix_prompt.py +0 -292
  139. package/.agents/skills/_shared/pr-loop/scripts/init_loop_state.py +0 -134
  140. package/.agents/skills/_shared/pr-loop/scripts/portable_converge_driver.py +0 -1646
  141. package/.agents/skills/_shared/pr-loop/scripts/preflight_worktree.py +0 -392
  142. package/.agents/skills/_shared/pr-loop/scripts/select_converge_pacer.py +0 -215
  143. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/.claude/CLAUDE.md +0 -1
  144. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +0 -1
  145. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/__init__.py +0 -0
  146. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/audit_category_schema_constants.py +0 -37
  147. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/converge_task_list_constants.py +0 -56
  148. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/handoff_constants.py +0 -45
  149. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/pacer_constants.py +0 -47
  150. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/path_resolver_constants.py +0 -157
  151. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/portable_driver_constants.py +0 -177
  152. package/.agents/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/preflight_constants.py +0 -70
  153. package/.agents/skills/_shared/pr-loop/scripts/teardown_worktrees.py +0 -172
  154. package/.agents/skills/_shared/pr-loop/scripts/test__path_resolver.py +0 -106
  155. package/.agents/skills/_shared/pr-loop/scripts/test_audit_category_schema.py +0 -94
  156. package/.agents/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +0 -499
  157. package/.agents/skills/_shared/pr-loop/scripts/test_build_converge_task_list.py +0 -140
  158. package/.agents/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +0 -262
  159. package/.agents/skills/_shared/pr-loop/scripts/test_init_loop_state.py +0 -71
  160. package/.agents/skills/_shared/pr-loop/scripts/test_portable_converge_driver.py +0 -1119
  161. package/.agents/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +0 -287
  162. package/.agents/skills/_shared/pr-loop/scripts/test_select_converge_pacer.py +0 -207
  163. package/.agents/skills/_shared/pr-loop/scripts/test_teardown_worktrees.py +0 -59
  164. package/.agents/skills/_shared/pr-loop/scripts/test_write_handoff.py +0 -201
  165. package/.agents/skills/_shared/pr-loop/scripts/write_audit_outcomes.py +0 -182
  166. package/.agents/skills/_shared/pr-loop/scripts/write_fix_outcomes.py +0 -206
  167. package/.agents/skills/_shared/pr-loop/scripts/write_handoff.py +0 -309
  168. package/.agents/skills/_shared/pr-loop/state-schema.md +0 -5
  169. package/.agents/skills/_shared/pr-loop/worker-spawn.md +0 -5
  170. package/.agents/skills/autoconverge/.claude/CLAUDE.md +0 -1
  171. package/.agents/skills/autoconverge/AGENTS.md +0 -1
  172. package/.agents/skills/autoconverge/SKILL.md +0 -431
  173. package/.agents/skills/autoconverge/reference/.claude/CLAUDE.md +0 -1
  174. package/.agents/skills/autoconverge/reference/AGENTS.md +0 -1
  175. package/.agents/skills/autoconverge/reference/closing-report.md +0 -93
  176. package/.agents/skills/autoconverge/reference/convergence-gates.md +0 -31
  177. package/.agents/skills/autoconverge/reference/convergence.md +0 -219
  178. package/.agents/skills/autoconverge/reference/copilot-findings.md +0 -51
  179. package/.agents/skills/autoconverge/reference/gotchas.md +0 -66
  180. package/.agents/skills/autoconverge/reference/headless-safety.md +0 -44
  181. package/.agents/skills/autoconverge/reference/multi-pr.md +0 -128
  182. package/.agents/skills/autoconverge/reference/self-closing-loop.md +0 -84
  183. package/.agents/skills/autoconverge/reference/stop-conditions.md +0 -127
  184. package/.agents/skills/autoconverge/test_portable_pacer_gate.py +0 -54
  185. package/.agents/skills/autoconverge/workflow/.claude/CLAUDE.md +0 -1
  186. package/.agents/skills/autoconverge/workflow/AGENTS.md +0 -1
  187. package/.agents/skills/autoconverge/workflow/aggregate_runs.py +0 -371
  188. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/.claude/CLAUDE.md +0 -1
  189. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +0 -1
  190. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/__init__.py +0 -0
  191. package/.agents/skills/autoconverge/workflow/autoconverge_report_constants/render_report_constants.py +0 -250
  192. package/.agents/skills/autoconverge/workflow/converge.clean-audit.test.mjs +0 -665
  193. package/.agents/skills/autoconverge/workflow/converge.codex-gate.test.mjs +0 -472
  194. package/.agents/skills/autoconverge/workflow/converge.contract.test.mjs +0 -64
  195. package/.agents/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +0 -1009
  196. package/.agents/skills/autoconverge/workflow/converge.fix-progress.test.mjs +0 -107
  197. package/.agents/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +0 -33
  198. package/.agents/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +0 -15
  199. package/.agents/skills/autoconverge/workflow/converge.mjs +0 -3132
  200. package/.agents/skills/autoconverge/workflow/converge.p2-advance.test.mjs +0 -202
  201. package/.agents/skills/autoconverge/workflow/converge.path-aware.test.mjs +0 -47
  202. package/.agents/skills/autoconverge/workflow/converge.precatch.test.mjs +0 -152
  203. package/.agents/skills/autoconverge/workflow/converge.run-input.test.mjs +0 -116
  204. package/.agents/skills/autoconverge/workflow/converge_multi.mjs +0 -169
  205. package/.agents/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +0 -173
  206. package/.agents/skills/autoconverge/workflow/convergence_summary.py +0 -110
  207. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a11d903476b803493.jsonl +0 -2
  208. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a26213978adeef6fb.jsonl +0 -2
  209. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a3def0d15ed9d9110.jsonl +0 -2
  210. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a41f41b1b708ee3b7.jsonl +0 -2
  211. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a758b880abecc3ff7.jsonl +0 -2
  212. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-a8897b89656b1bd16.jsonl +0 -2
  213. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ab1c2d3e4f5a6b7c8.jsonl +0 -2
  214. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-abd463d744a1437bc.jsonl +0 -2
  215. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/subagents/workflows/wf_881252e6-700/agent-ad19d027ae8ee1816.jsonl +0 -2
  216. package/.agents/skills/autoconverge/workflow/fixtures/wf_run/workflows/wf_881252e6-700.json +0 -266
  217. package/.agents/skills/autoconverge/workflow/render_report.py +0 -1024
  218. package/.agents/skills/autoconverge/workflow/test_aggregate_runs.py +0 -134
  219. package/.agents/skills/autoconverge/workflow/test_convergence_summary.py +0 -149
  220. package/.agents/skills/autoconverge/workflow/test_render_report.py +0 -812
  221. package/.agents/skills/hitl/SKILL.md +0 -45
  222. package/.agents/skills/pr-cleanup/SKILL.md +0 -140
  223. package/.agents/skills/pr-cleanup/reference/process-inventory.md +0 -17
  224. package/.agents/skills/pr-cleanup/reference/task-seeds.md +0 -22
  225. package/.agents/skills/pr-name-by-capability/SKILL.md +0 -73
  226. package/.agents/skills/pr-name-by-capability/reference/fetch-commands.md +0 -29
  227. package/.agents/skills/pr-name-by-capability/reference/offense-examples.md +0 -32
  228. package/.agents/skills/pr-name-by-capability/reference/report-template.md +0 -29
  229. package/.agents/skills/pr-name-by-capability/reference/rule-checklist.md +0 -38
  230. package/.agents/skills/pr-name-by-capability/reference/task-seeds.md +0 -10
  231. package/.agents/skills/pr-plain-language-cleanup/SKILL.md +0 -93
  232. package/.agents/skills/pr-refinement/SKILL.md +0 -31
  233. package/.agents/skills/pr-shared-extraction/SKILL.md +0 -177
  234. package/.agents/skills/pr-shared-extraction/reference/examples.md +0 -46
  235. package/.agents/skills/pr-shared-extraction/reference/offense-taxonomy.md +0 -100
  236. package/.agents/skills/pr-small-cl/SKILL.md +0 -52
  237. package/.agents/skills/pr-title-description/SKILL.md +0 -96
  238. package/.agents/skills/prototype/SKILL.md +0 -85
  239. package/.agents/skills/prototype/reference/honest-limitations.md +0 -23
  240. package/.agents/skills/prototype/reference/promotion-tasks.md +0 -23
  241. package/.agents/skills/prototype/scripts/build_sandbox_settings.py +0 -249
  242. package/.agents/skills/prototype/scripts/conftest.py +0 -15
  243. package/.agents/skills/prototype/scripts/launch_sandbox.py +0 -205
  244. package/.agents/skills/prototype/scripts/probe_sandbox_safety.py +0 -311
  245. package/.agents/skills/prototype/scripts/prototype_scripts_constants/__init__.py +0 -1
  246. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/__init__.py +0 -0
  247. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/build_sandbox_settings_constants.py +0 -41
  248. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/launch_sandbox_constants.py +0 -23
  249. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/probe_sandbox_safety_constants.py +0 -45
  250. package/.agents/skills/prototype/scripts/prototype_scripts_constants/config/prototype_common_constants.py +0 -10
  251. package/.agents/skills/prototype/scripts/test_build_sandbox_settings.py +0 -275
  252. package/.agents/skills/prototype/scripts/test_launch_sandbox.py +0 -303
  253. package/.agents/skills/prototype/scripts/test_probe_sandbox_safety.py +0 -312
  254. package/.agents/skills/prototype/workflows/promotion.md +0 -25
  255. package/.agents/skills/prototype/workflows/sandbox.md +0 -35
  256. package/.agents/skills/rebase/.claude/CLAUDE.md +0 -1
  257. package/.agents/skills/rebase/AGENTS.md +0 -1
  258. package/.agents/skills/rebase/SKILL.md +0 -178
  259. package/.agents/skills/review-router/SKILL.md +0 -51
  260. package/.agents/skills/review-router/reference/route-policy.json +0 -7
  261. package/.agents/skills/review-router/reference/task-seeds.md +0 -1
  262. package/.agents/skills/review-router/scripts/conftest.py +0 -4
  263. package/.agents/skills/review-router/scripts/review_router.py +0 -50
  264. package/.agents/skills/review-router/scripts/review_router_cli.py +0 -211
  265. package/.agents/skills/review-router/scripts/review_router_constants/__init__.py +0 -1
  266. package/.agents/skills/review-router/scripts/review_router_constants/config/__init__.py +0 -0
  267. package/.agents/skills/review-router/scripts/review_router_constants/config/constants.py +0 -13
  268. package/.agents/skills/review-router/scripts/test_review_router.py +0 -23
  269. package/.agents/skills/review-router/scripts/test_review_router_cli.py +0 -47
  270. package/.agents/skills/review-tier/SKILL.md +0 -56
  271. package/.agents/skills/review-tier/reference/task-seeds.md +0 -1
  272. package/.agents/skills/review-tier/reference/tier-policy.json +0 -9
  273. package/.agents/skills/review-tier/scripts/conftest.py +0 -4
  274. package/.agents/skills/review-tier/scripts/review_tier.py +0 -359
  275. package/.agents/skills/review-tier/scripts/review_tier_constants/__init__.py +0 -1
  276. package/.agents/skills/review-tier/scripts/review_tier_constants/config/__init__.py +0 -0
  277. package/.agents/skills/review-tier/scripts/review_tier_constants/config/constants.py +0 -29
  278. package/.agents/skills/review-tier/scripts/test_generation.py +0 -159
  279. package/.agents/skills/review-tier/scripts/test_review_tier.py +0 -33
  280. package/.agents/skills/run-claude-dev-env/SKILL.md +0 -84
  281. package/.agents/skills/session-log/.claude/CLAUDE.md +0 -1
  282. package/.agents/skills/session-log/AGENTS.md +0 -1
  283. package/.agents/skills/session-log/SKILL.md +0 -170
  284. package/.agents/skills/session-log/templates/frontmatter.md +0 -40
  285. package/.agents/skills/session-tidy/.claude/CLAUDE.md +0 -1
  286. package/.agents/skills/session-tidy/AGENTS.md +0 -1
  287. package/.agents/skills/session-tidy/SKILL.md +0 -191
  288. package/.agents/skills/skill-builder/references/.claude/CLAUDE.md +0 -1
  289. package/.agents/skills/skill-builder/references/AGENTS.md +0 -1
  290. package/.agents/skills/skill-builder/references/delegation-map.md +0 -130
  291. package/.agents/skills/skill-builder/references/description-field.md +0 -111
  292. package/.agents/skills/skill-builder/references/deterministic-elements.md +0 -218
  293. package/.agents/skills/skill-builder/references/progressive-disclosure.md +0 -122
  294. package/.agents/skills/skill-builder/references/self-audit-checklist.md +0 -126
  295. package/.agents/skills/skill-builder/references/skill-modularity.md +0 -111
  296. package/.agents/skills/skill-builder/references/skill-types.md +0 -232
  297. package/.agents/skills/skill-builder/references/thariq-x-post-skills.json +0 -33
  298. package/.agents/skills/skill-builder/templates/.claude/CLAUDE.md +0 -1
  299. package/.agents/skills/skill-builder/templates/AGENTS.md +0 -1
  300. package/.agents/skills/skill-builder/templates/gap-analysis.md +0 -87
  301. package/.agents/skills/skill-builder/workflows/.claude/CLAUDE.md +0 -1
  302. package/.agents/skills/skill-builder/workflows/AGENTS.md +0 -1
  303. package/.agents/skills/skill-builder/workflows/improve-skill.md +0 -136
  304. package/.agents/skills/skill-builder/workflows/new-skill.md +0 -179
  305. package/.agents/skills/skill-builder/workflows/polish-skill.md +0 -139
  306. package/.agents/skills/source-command-sr-loop/SKILL.md +0 -82
  307. package/.agents/skills/update/.claude/CLAUDE.md +0 -1
  308. package/.agents/skills/update/AGENTS.md +0 -1
  309. package/.agents/skills/update/SKILL.md +0 -190
  310. package/_shared/pr-loop/AGENTS.md +0 -1
  311. package/_shared/pr-loop/CLAUDE.md +0 -1
  312. package/_shared/pr-loop/audit-contract.md +0 -183
  313. package/_shared/pr-loop/audit-reply-template.md +0 -151
  314. package/_shared/pr-loop/code-rules-gate.md +0 -65
  315. package/_shared/pr-loop/fix-protocol.md +0 -60
  316. package/_shared/pr-loop/gh-payloads.md +0 -75
  317. package/_shared/pr-loop/post-audit-thread-contract.md +0 -51
  318. package/_shared/pr-loop/precatch-rubric.md +0 -66
  319. package/_shared/pr-loop/preflight-proposal.md +0 -56
  320. package/_shared/pr-loop/scripts/post_audit_thread.py +0 -1242
  321. package/_shared/pr-loop/scripts/pr_loop_shared_constants/post_audit_thread_constants.py +0 -199
  322. package/_shared/pr-loop/scripts/tests/test_post_audit_thread.py +0 -1115
  323. package/_shared/pr-loop/scripts/tests/test_post_audit_thread_constants.py +0 -129
  324. package/_shared/pr-loop/state-schema.md +0 -45
  325. package/_shared/pr-loop/worker-spawn.md +0 -189
  326. package/hooks/_gh_pr_author_swap_utils.py +0 -1225
  327. package/hooks/blocking/conventional_pr_title_gate.py +0 -443
  328. package/hooks/blocking/gh_body_arg_blocker.py +0 -176
  329. package/hooks/blocking/gh_pr_author_enforcer.py +0 -487
  330. package/hooks/blocking/gh_pr_author_restore.py +0 -105
  331. package/hooks/blocking/pr_description_writer_gate.py +0 -256
  332. package/hooks/blocking/test_conventional_pr_title_gate.py +0 -640
  333. package/hooks/blocking/test_gh_body_arg_blocker.py +0 -441
  334. package/hooks/blocking/test_gh_pr_author_enforcer.py +0 -1166
  335. package/hooks/blocking/test_gh_pr_author_restore.py +0 -513
  336. package/hooks/blocking/test_gh_pr_author_swap_utils.py +0 -910
  337. package/hooks/blocking/test_pr_description_writer_gate.py +0 -234
  338. package/hooks/blocking/test_preflight_check.py +0 -343
  339. package/hooks/blocking/test_test_preflight_check.py +0 -80
  340. package/hooks/blocking/test_volatile_path_in_post_blocker.py +0 -322
  341. package/hooks/blocking/volatile_path_in_post_blocker.py +0 -461
  342. package/hooks/hooks_constants/conventional_pr_title_gate_constants.py +0 -58
  343. package/hooks/hooks_constants/gh_pr_author_swap_constants.py +0 -76
  344. package/hooks/hooks_constants/pr_description_writer_gate_constants.py +0 -73
  345. package/hooks/hooks_constants/volatile_path_in_post_blocker_constants.py +0 -55
  346. package/hooks/observability/pr_description_writer_spawn_tracker.py +0 -113
  347. package/hooks/observability/test_pr_description_writer_spawn_tracker.py +0 -150
  348. package/hooks/session/gh_pr_author_session_cleanup.py +0 -175
  349. package/hooks/session/test_gh_pr_author_session_cleanup.py +0 -576
  350. package/hooks/test__gh_pr_author_swap_utils.py +0 -333
@@ -1,576 +0,0 @@
1
- """Unit tests for gh-pr-author-session-cleanup SessionStart hook."""
2
-
3
- from __future__ import annotations
4
-
5
- import importlib.util
6
- import json
7
- import os
8
- import pathlib
9
- import stat
10
- import sys
11
- import time
12
- from typing import Iterator
13
- from unittest import mock
14
-
15
- import pytest
16
-
17
- _SESSION_DIR = pathlib.Path(__file__).resolve().parent
18
- _HOOKS_ROOT = _SESSION_DIR.parent
19
- for each_sys_path_entry in (str(_SESSION_DIR), str(_HOOKS_ROOT)):
20
- if each_sys_path_entry not in sys.path:
21
- sys.path.insert(0, each_sys_path_entry)
22
-
23
- hook_module_spec = importlib.util.spec_from_file_location(
24
- "gh_pr_author_session_cleanup",
25
- _SESSION_DIR / "gh_pr_author_session_cleanup.py",
26
- )
27
- assert hook_module_spec is not None
28
- assert hook_module_spec.loader is not None
29
- hook_module = importlib.util.module_from_spec(hook_module_spec)
30
- hook_module_spec.loader.exec_module(hook_module)
31
-
32
- import _gh_pr_author_swap_utils as swap_utils_module # noqa: E402
33
-
34
- from hooks_constants.gh_pr_author_swap_constants import ( # noqa: E402
35
- STATE_FILE_PERMISSION_MODE,
36
- STATE_FILE_STALE_AGE_SECONDS,
37
- )
38
-
39
-
40
- _BACKDATE_SECONDS_BEFORE_NOW: int = STATE_FILE_STALE_AGE_SECONDS * 2
41
-
42
-
43
- def _backdate_file(state_file: pathlib.Path) -> None:
44
- backdated_time_seconds = time.time() - _BACKDATE_SECONDS_BEFORE_NOW
45
- os.utime(state_file, (backdated_time_seconds, backdated_time_seconds))
46
-
47
-
48
- def _chmod_like_enforcer(state_file: pathlib.Path) -> None:
49
- """Apply the same 0o600 mode the production enforcer sets on its write.
50
-
51
- Tests must mirror the enforcer's write contract so the cleanup
52
- hook's ownership / mode security check sees a "trustworthy" file.
53
- Without this chmod, every backdated state file is silently skipped
54
- on POSIX as if it were attacker-planted.
55
- """
56
- os.chmod(state_file, STATE_FILE_PERMISSION_MODE)
57
-
58
-
59
- def _write_state_file(state_file: pathlib.Path, original_account: str) -> None:
60
- state_file.write_text(
61
- json.dumps(
62
- {
63
- "original_account": original_account,
64
- "primary_account": "JonEcho",
65
- }
66
- ),
67
- encoding="utf-8",
68
- )
69
- _chmod_like_enforcer(state_file)
70
- _backdate_file(state_file)
71
-
72
-
73
- @pytest.fixture
74
- def required_account_jonecho(monkeypatch: pytest.MonkeyPatch) -> Iterator[str]:
75
- monkeypatch.setenv("GITHUB_DEFAULT_ACCOUNT", "JonEcho")
76
- yield "JonEcho"
77
-
78
-
79
- @pytest.fixture
80
- def isolated_temp_directory(
81
- monkeypatch: pytest.MonkeyPatch,
82
- tmp_path: pathlib.Path,
83
- ) -> Iterator[pathlib.Path]:
84
- monkeypatch.setattr(hook_module.tempfile, "gettempdir", lambda: str(tmp_path))
85
- yield tmp_path
86
-
87
-
88
- def _install_fake_switch(monkeypatch: pytest.MonkeyPatch, switch_succeeds: bool) -> list[str]:
89
- switch_invocations: list[str] = []
90
-
91
- def _fake_switch(to_account: str) -> bool:
92
- switch_invocations.append(to_account)
93
- return switch_succeeds
94
-
95
- monkeypatch.setattr(hook_module, "_switch_gh_account", _fake_switch)
96
- return switch_invocations
97
-
98
-
99
- def test_main_no_op_when_no_state_files_present(
100
- monkeypatch: pytest.MonkeyPatch,
101
- required_account_jonecho: str,
102
- isolated_temp_directory: pathlib.Path,
103
- ) -> None:
104
- switch_invocations = _install_fake_switch(monkeypatch, switch_succeeds=True)
105
- hook_module.main()
106
- assert switch_invocations == []
107
-
108
-
109
- def test_main_restores_one_stale_state_file(
110
- monkeypatch: pytest.MonkeyPatch,
111
- required_account_jonecho: str,
112
- isolated_temp_directory: pathlib.Path,
113
- ) -> None:
114
- state_file = isolated_temp_directory / "gh_pr_author_swap_session-A.json"
115
- _write_state_file(state_file, original_account="jl-cmd")
116
- switch_invocations = _install_fake_switch(monkeypatch, switch_succeeds=True)
117
-
118
- hook_module.main()
119
-
120
- assert switch_invocations == ["jl-cmd"]
121
- assert not state_file.exists()
122
-
123
-
124
- def test_main_restores_multiple_stale_state_files(
125
- monkeypatch: pytest.MonkeyPatch,
126
- required_account_jonecho: str,
127
- isolated_temp_directory: pathlib.Path,
128
- ) -> None:
129
- state_file_a = isolated_temp_directory / "gh_pr_author_swap_session-A.json"
130
- state_file_b = isolated_temp_directory / "gh_pr_author_swap_session-B.json"
131
- state_file_c = isolated_temp_directory / "gh_pr_author_swap_session-C.json"
132
- _write_state_file(state_file_a, original_account="jl-cmd")
133
- _write_state_file(state_file_b, original_account="other-user")
134
- _write_state_file(state_file_c, original_account="third-user")
135
- switch_invocations = _install_fake_switch(monkeypatch, switch_succeeds=True)
136
-
137
- hook_module.main()
138
-
139
- assert sorted(switch_invocations) == ["jl-cmd", "other-user", "third-user"]
140
- assert not state_file_a.exists()
141
- assert not state_file_b.exists()
142
- assert not state_file_c.exists()
143
-
144
-
145
- def test_main_deletes_malformed_state_file_without_switching(
146
- monkeypatch: pytest.MonkeyPatch,
147
- required_account_jonecho: str,
148
- isolated_temp_directory: pathlib.Path,
149
- ) -> None:
150
- malformed_state_file = isolated_temp_directory / "gh_pr_author_swap_broken.json"
151
- malformed_state_file.write_text("{not valid json", encoding="utf-8")
152
- _chmod_like_enforcer(malformed_state_file)
153
- _backdate_file(malformed_state_file)
154
- switch_invocations = _install_fake_switch(monkeypatch, switch_succeeds=True)
155
-
156
- hook_module.main()
157
-
158
- assert switch_invocations == []
159
- assert not malformed_state_file.exists()
160
-
161
-
162
- def test_main_no_op_when_required_account_unset(
163
- monkeypatch: pytest.MonkeyPatch,
164
- isolated_temp_directory: pathlib.Path,
165
- ) -> None:
166
- monkeypatch.delenv("GITHUB_DEFAULT_ACCOUNT", raising=False)
167
- state_file = isolated_temp_directory / "gh_pr_author_swap_session-A.json"
168
- _write_state_file(state_file, original_account="jl-cmd")
169
- switch_invocations = _install_fake_switch(monkeypatch, switch_succeeds=True)
170
-
171
- hook_module.main()
172
-
173
- assert switch_invocations == []
174
- assert state_file.exists()
175
-
176
-
177
- def test_main_preserves_state_file_when_switch_fails(
178
- monkeypatch: pytest.MonkeyPatch,
179
- required_account_jonecho: str,
180
- isolated_temp_directory: pathlib.Path,
181
- ) -> None:
182
- state_file = isolated_temp_directory / "gh_pr_author_swap_session-A.json"
183
- _write_state_file(state_file, original_account="jl-cmd")
184
- switch_invocations = _install_fake_switch(monkeypatch, switch_succeeds=False)
185
-
186
- hook_module.main()
187
-
188
- assert switch_invocations == ["jl-cmd"]
189
- assert state_file.exists()
190
-
191
-
192
- def test_main_no_op_when_required_account_blank(
193
- monkeypatch: pytest.MonkeyPatch,
194
- isolated_temp_directory: pathlib.Path,
195
- ) -> None:
196
- monkeypatch.setenv("GITHUB_DEFAULT_ACCOUNT", " ")
197
- state_file = isolated_temp_directory / "gh_pr_author_swap_session-A.json"
198
- _write_state_file(state_file, original_account="jl-cmd")
199
- switch_invocations = _install_fake_switch(monkeypatch, switch_succeeds=True)
200
-
201
- hook_module.main()
202
-
203
- assert switch_invocations == []
204
- assert state_file.exists()
205
-
206
-
207
- def test_main_ignores_unrelated_temp_files(
208
- monkeypatch: pytest.MonkeyPatch,
209
- required_account_jonecho: str,
210
- isolated_temp_directory: pathlib.Path,
211
- ) -> None:
212
- unrelated_file = isolated_temp_directory / "unrelated-tempfile.txt"
213
- unrelated_file.write_text("not a swap state file", encoding="utf-8")
214
- sibling_swap_file = isolated_temp_directory / "gh_pr_author_swap_session-A.json"
215
- _write_state_file(sibling_swap_file, original_account="jl-cmd")
216
- switch_invocations = _install_fake_switch(monkeypatch, switch_succeeds=True)
217
-
218
- hook_module.main()
219
-
220
- assert switch_invocations == ["jl-cmd"]
221
- assert not sibling_swap_file.exists()
222
- assert unrelated_file.exists()
223
-
224
-
225
- def test_main_continues_after_per_file_switch_failure(
226
- monkeypatch: pytest.MonkeyPatch,
227
- required_account_jonecho: str,
228
- isolated_temp_directory: pathlib.Path,
229
- ) -> None:
230
- state_file_a = isolated_temp_directory / "gh_pr_author_swap_session-A.json"
231
- state_file_b = isolated_temp_directory / "gh_pr_author_swap_session-B.json"
232
- _write_state_file(state_file_a, original_account="failing-user")
233
- _write_state_file(state_file_b, original_account="succeeding-user")
234
-
235
- switch_invocations: list[str] = []
236
-
237
- def _fake_switch(to_account: str) -> bool:
238
- switch_invocations.append(to_account)
239
- return to_account == "succeeding-user"
240
-
241
- monkeypatch.setattr(hook_module, "_switch_gh_account", _fake_switch)
242
-
243
- hook_module.main()
244
-
245
- assert sorted(switch_invocations) == ["failing-user", "succeeding-user"]
246
- assert state_file_a.exists()
247
- assert not state_file_b.exists()
248
-
249
-
250
- def test_read_original_account_returns_none_for_missing_file(
251
- isolated_temp_directory: pathlib.Path,
252
- ) -> None:
253
- missing_file = isolated_temp_directory / "does_not_exist.json"
254
- assert hook_module._read_original_account(missing_file) is None
255
-
256
-
257
- def test_read_original_account_returns_none_for_non_dict_payload(
258
- isolated_temp_directory: pathlib.Path,
259
- ) -> None:
260
- list_payload_file = isolated_temp_directory / "list_payload.json"
261
- list_payload_file.write_text(json.dumps(["jl-cmd"]), encoding="utf-8")
262
- assert hook_module._read_original_account(list_payload_file) is None
263
-
264
-
265
- def test_read_original_account_returns_none_for_non_string_value(
266
- isolated_temp_directory: pathlib.Path,
267
- ) -> None:
268
- bad_type_file = isolated_temp_directory / "bad_type.json"
269
- bad_type_file.write_text(json.dumps({"original_account": 42}), encoding="utf-8")
270
- assert hook_module._read_original_account(bad_type_file) is None
271
-
272
-
273
- def test_read_original_account_returns_none_for_blank_value(
274
- isolated_temp_directory: pathlib.Path,
275
- ) -> None:
276
- blank_value_file = isolated_temp_directory / "blank.json"
277
- blank_value_file.write_text(json.dumps({"original_account": " "}), encoding="utf-8")
278
- assert hook_module._read_original_account(blank_value_file) is None
279
-
280
-
281
- def test_switch_gh_account_returns_true_on_success() -> None:
282
- completed = mock.Mock(returncode=0, stdout="", stderr="")
283
- with mock.patch.object(swap_utils_module.subprocess, "run", return_value=completed):
284
- assert hook_module._switch_gh_account("jl-cmd") is True
285
-
286
-
287
- def test_switch_gh_account_returns_false_on_nonzero_exit() -> None:
288
- completed = mock.Mock(returncode=1, stdout="", stderr="boom")
289
- with mock.patch.object(swap_utils_module.subprocess, "run", return_value=completed):
290
- assert hook_module._switch_gh_account("jl-cmd") is False
291
-
292
-
293
- def test_switch_gh_account_returns_false_when_gh_missing() -> None:
294
- with mock.patch.object(swap_utils_module.subprocess, "run", side_effect=FileNotFoundError):
295
- assert hook_module._switch_gh_account("jl-cmd") is False
296
-
297
-
298
- def test_switch_gh_account_returns_false_on_timeout() -> None:
299
- with mock.patch.object(
300
- swap_utils_module.subprocess,
301
- "run",
302
- side_effect=swap_utils_module.subprocess.TimeoutExpired(cmd="gh", timeout=10),
303
- ):
304
- assert hook_module._switch_gh_account("jl-cmd") is False
305
-
306
-
307
- def test_delete_state_file_is_silent_when_already_absent(
308
- isolated_temp_directory: pathlib.Path,
309
- ) -> None:
310
- missing_file = isolated_temp_directory / "does_not_exist.json"
311
- hook_module._delete_state_file(missing_file)
312
- assert not missing_file.exists()
313
-
314
-
315
- def test_collect_stale_state_files_matches_prefix_and_suffix(
316
- isolated_temp_directory: pathlib.Path,
317
- ) -> None:
318
- matching_a = isolated_temp_directory / "gh_pr_author_swap_session-A.json"
319
- matching_b = isolated_temp_directory / "gh_pr_author_swap_session-B.json"
320
- wrong_prefix = isolated_temp_directory / "other_swap_session-C.json"
321
- wrong_suffix = isolated_temp_directory / "gh_pr_author_swap_session-D.txt"
322
- for each_file in (matching_a, matching_b, wrong_prefix, wrong_suffix):
323
- each_file.write_text("{}", encoding="utf-8")
324
- _chmod_like_enforcer(each_file)
325
- _backdate_file(each_file)
326
-
327
- matched_files = hook_module._collect_stale_state_files(isolated_temp_directory)
328
- matched_names = {each_file.name for each_file in matched_files}
329
-
330
- assert matched_names == {matching_a.name, matching_b.name}
331
-
332
-
333
- def test_restore_stale_state_file_logs_when_switch_fails(
334
- monkeypatch: pytest.MonkeyPatch,
335
- capsys: pytest.CaptureFixture[str],
336
- isolated_temp_directory: pathlib.Path,
337
- ) -> None:
338
- state_file = isolated_temp_directory / "gh_pr_author_swap_session-A.json"
339
- _write_state_file(state_file, original_account="jl-cmd")
340
- _install_fake_switch(monkeypatch, switch_succeeds=False)
341
-
342
- hook_module._restore_stale_state_file(state_file)
343
-
344
- captured_streams = capsys.readouterr()
345
- assert state_file.exists()
346
- assert "[gh-pr-author-cleanup] Restore the gh account" in captured_streams.err
347
- assert "gh account switch requires attention" in captured_streams.err
348
- assert "'jl-cmd'" in captured_streams.err
349
- assert str(state_file) in captured_streams.err
350
-
351
-
352
- def test_collect_stale_state_files_excludes_recent_files(
353
- isolated_temp_directory: pathlib.Path,
354
- ) -> None:
355
- recent_state_file = isolated_temp_directory / "gh_pr_author_swap_session-recent.json"
356
- recent_state_file.write_text(
357
- json.dumps({"original_account": "jl-cmd", "primary_account": "JonEcho"}),
358
- encoding="utf-8",
359
- )
360
- _chmod_like_enforcer(recent_state_file)
361
-
362
- matched_files = hook_module._collect_stale_state_files(isolated_temp_directory)
363
-
364
- assert recent_state_file not in matched_files
365
-
366
-
367
- def test_collect_stale_state_files_includes_old_files(
368
- isolated_temp_directory: pathlib.Path,
369
- ) -> None:
370
- old_state_file = isolated_temp_directory / "gh_pr_author_swap_session-old.json"
371
- old_state_file.write_text(
372
- json.dumps({"original_account": "jl-cmd", "primary_account": "JonEcho"}),
373
- encoding="utf-8",
374
- )
375
- _chmod_like_enforcer(old_state_file)
376
- backdated_time_seconds = time.time() - _BACKDATE_SECONDS_BEFORE_NOW
377
- os.utime(old_state_file, (backdated_time_seconds, backdated_time_seconds))
378
-
379
- matched_files = hook_module._collect_stale_state_files(isolated_temp_directory)
380
-
381
- assert old_state_file in matched_files
382
-
383
-
384
- def test_collect_stale_state_files_skips_unreadable_stat(
385
- monkeypatch: pytest.MonkeyPatch,
386
- isolated_temp_directory: pathlib.Path,
387
- ) -> None:
388
- unreadable_state_file = isolated_temp_directory / "gh_pr_author_swap_session-unreadable.json"
389
- readable_state_file = isolated_temp_directory / "gh_pr_author_swap_session-readable.json"
390
- for each_file in (unreadable_state_file, readable_state_file):
391
- each_file.write_text(
392
- json.dumps({"original_account": "jl-cmd", "primary_account": "JonEcho"}),
393
- encoding="utf-8",
394
- )
395
- _chmod_like_enforcer(each_file)
396
- _backdate_file(each_file)
397
-
398
- original_lstat_method = pathlib.Path.lstat
399
-
400
- def _lstat_with_failure_for_unreadable(
401
- self: pathlib.Path,
402
- *call_arguments: object,
403
- **call_keyword_arguments: object,
404
- ) -> os.stat_result:
405
- if self == unreadable_state_file:
406
- raise OSError("simulated lstat failure")
407
- return original_lstat_method(self, *call_arguments, **call_keyword_arguments) # type: ignore[arg-type] # forwarding mixed positional/keyword to stdlib Path.lstat
408
-
409
- monkeypatch.setattr(pathlib.Path, "lstat", _lstat_with_failure_for_unreadable)
410
-
411
- matched_files = hook_module._collect_stale_state_files(isolated_temp_directory)
412
-
413
- assert unreadable_state_file not in matched_files
414
- assert readable_state_file in matched_files
415
-
416
-
417
- def test_collect_stale_state_files_skips_world_readable_file(
418
- isolated_temp_directory: pathlib.Path,
419
- ) -> None:
420
- """A backdated state file written with 0o644 mode is silently skipped on POSIX.
421
-
422
- The enforcer creates every file at 0o600. A divergent mode means
423
- the file was not written by an enforcer running as this user — most
424
- likely an attacker plant — and must not be allowed to drive
425
- ``gh auth switch``.
426
- """
427
- if not hasattr(os, "getuid"):
428
- return
429
- world_readable_state_file = (
430
- isolated_temp_directory / "gh_pr_author_swap_session-attacker.json"
431
- )
432
- world_readable_state_file.write_text(
433
- json.dumps({"original_account": "attacker", "primary_account": "JonEcho"}),
434
- encoding="utf-8",
435
- )
436
- os.chmod(world_readable_state_file, 0o644)
437
- _backdate_file(world_readable_state_file)
438
-
439
- matched_files = hook_module._collect_stale_state_files(isolated_temp_directory)
440
-
441
- assert world_readable_state_file not in matched_files
442
-
443
-
444
- def test_session_cleanup_uses_shared_lstat_helper() -> None:
445
- """Session cleanup must reuse the shared ``_lstat_indicates_attacker_planted``.
446
-
447
- Two implementations of the lstat-based security check would let the
448
- permission and ownership logic drift between the restore hook and
449
- the cleanup hook. The session cleanup module must import the same
450
- callable the shared utils exposes so a future fix to the check in
451
- ``_gh_pr_author_swap_utils.py`` lands on both consumers from a
452
- single edit.
453
- """
454
- assert (
455
- hook_module._lstat_indicates_attacker_planted
456
- is swap_utils_module._lstat_indicates_attacker_planted
457
- )
458
-
459
-
460
- def test_collect_stale_state_files_skips_other_user_owned_file(
461
- monkeypatch: pytest.MonkeyPatch,
462
- isolated_temp_directory: pathlib.Path,
463
- ) -> None:
464
- """A POSIX file owned by a different uid is silently skipped.
465
-
466
- The cleanup hook cannot chown without root, so the test fakes a
467
- foreign uid by monkeypatching ``Path.stat`` to return a synthetic
468
- ``stat_result`` whose ``st_uid`` does not match ``os.getuid()``.
469
- """
470
- if not hasattr(os, "getuid"):
471
- return
472
- foreign_owned_state_file = (
473
- isolated_temp_directory / "gh_pr_author_swap_session-foreign.json"
474
- )
475
- foreign_owned_state_file.write_text(
476
- json.dumps({"original_account": "attacker", "primary_account": "JonEcho"}),
477
- encoding="utf-8",
478
- )
479
- _chmod_like_enforcer(foreign_owned_state_file)
480
- _backdate_file(foreign_owned_state_file)
481
-
482
- real_lstat_result = os.lstat(foreign_owned_state_file)
483
- current_user_id = os.getuid()
484
- foreign_user_id = current_user_id + 1
485
- synthetic_stat_fields = (
486
- stat.S_IFREG | STATE_FILE_PERMISSION_MODE,
487
- real_lstat_result.st_ino,
488
- real_lstat_result.st_dev,
489
- real_lstat_result.st_nlink,
490
- foreign_user_id,
491
- real_lstat_result.st_gid,
492
- real_lstat_result.st_size,
493
- real_lstat_result.st_atime,
494
- real_lstat_result.st_mtime,
495
- real_lstat_result.st_ctime,
496
- )
497
- synthetic_stat_result = os.stat_result(synthetic_stat_fields)
498
- original_lstat_method = pathlib.Path.lstat
499
-
500
- def _lstat_returning_foreign_uid_for_target(
501
- self: pathlib.Path,
502
- *call_arguments: object,
503
- **call_keyword_arguments: object,
504
- ) -> os.stat_result:
505
- if self == foreign_owned_state_file:
506
- return synthetic_stat_result
507
- return original_lstat_method(self, *call_arguments, **call_keyword_arguments) # type: ignore[arg-type] # forwarding mixed positional/keyword to stdlib Path.lstat
508
-
509
- monkeypatch.setattr(pathlib.Path, "lstat", _lstat_returning_foreign_uid_for_target)
510
-
511
- matched_files = hook_module._collect_stale_state_files(isolated_temp_directory)
512
-
513
- assert foreign_owned_state_file not in matched_files
514
-
515
-
516
- def test_collect_stale_state_files_sorts_by_mtime_ascending(
517
- isolated_temp_directory: pathlib.Path,
518
- ) -> None:
519
- """Regression for finding 2: returned paths must be sorted by mtime ascending.
520
-
521
- A name-order sort would mismatch real age: a session_id starting
522
- with ``z`` (created earliest) would sort after one starting with
523
- ``a`` (created latest). Since the caller iterates and runs
524
- ``gh auth switch`` for each file in order, the LAST switch wins
525
- globally. Ordering by mtime ascending guarantees the newest stale
526
- file's original account is the active gh account after the sweep.
527
- """
528
- earliest_state_file = isolated_temp_directory / "gh_pr_author_swap_session-z-earliest.json"
529
- middle_state_file = isolated_temp_directory / "gh_pr_author_swap_session-a-middle.json"
530
- latest_state_file = isolated_temp_directory / "gh_pr_author_swap_session-m-latest.json"
531
- for each_state_file in (earliest_state_file, middle_state_file, latest_state_file):
532
- _write_state_file(each_state_file, original_account="any-account")
533
- base_backdate_seconds = time.time() - _BACKDATE_SECONDS_BEFORE_NOW
534
- os.utime(earliest_state_file, (base_backdate_seconds - 100, base_backdate_seconds - 100))
535
- os.utime(middle_state_file, (base_backdate_seconds - 50, base_backdate_seconds - 50))
536
- os.utime(latest_state_file, (base_backdate_seconds - 10, base_backdate_seconds - 10))
537
-
538
- matched_files = hook_module._collect_stale_state_files(isolated_temp_directory)
539
-
540
- assert matched_files == [earliest_state_file, middle_state_file, latest_state_file]
541
-
542
-
543
- def test_main_leaves_newest_stale_files_account_active_when_multiple_crashed(
544
- monkeypatch: pytest.MonkeyPatch,
545
- required_account_jonecho: str,
546
- isolated_temp_directory: pathlib.Path,
547
- ) -> None:
548
- """Regression for finding 2: the final ``gh auth switch`` must target the newest file's account.
549
-
550
- Three sessions crashed with different original accounts. The
551
- cleanup hook must iterate them oldest-first so the final
552
- invocation of ``gh auth switch`` (the only one that actually wins
553
- in the global gh CLI state) targets the original account from the
554
- newest stale file.
555
- """
556
- oldest_state_file = isolated_temp_directory / "gh_pr_author_swap_session-z-oldest.json"
557
- middle_state_file = isolated_temp_directory / "gh_pr_author_swap_session-a-middle.json"
558
- newest_state_file = isolated_temp_directory / "gh_pr_author_swap_session-m-newest.json"
559
- _write_state_file(oldest_state_file, original_account="oldest-original-user")
560
- _write_state_file(middle_state_file, original_account="middle-original-user")
561
- _write_state_file(newest_state_file, original_account="newest-original-user")
562
- base_backdate_seconds = time.time() - _BACKDATE_SECONDS_BEFORE_NOW
563
- os.utime(oldest_state_file, (base_backdate_seconds - 100, base_backdate_seconds - 100))
564
- os.utime(middle_state_file, (base_backdate_seconds - 50, base_backdate_seconds - 50))
565
- os.utime(newest_state_file, (base_backdate_seconds - 10, base_backdate_seconds - 10))
566
-
567
- switch_invocations = _install_fake_switch(monkeypatch, switch_succeeds=True)
568
-
569
- hook_module.main()
570
-
571
- assert switch_invocations == [
572
- "oldest-original-user",
573
- "middle-original-user",
574
- "newest-original-user",
575
- ]
576
- assert switch_invocations[-1] == "newest-original-user"