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
@@ -172,7 +172,7 @@ def _documents_from_changes(
172
172
  selection_kind = SelectionKind.STAGED if is_staged else SelectionKind.BASE
173
173
  return DocumentSet(
174
174
  tuple(all_documents), selection_kind, repository_root,
175
- tuple(all_deleted_paths), tuple(all_renamed_paths),
175
+ tuple(all_deleted_paths), tuple(all_renamed_paths), base_revision,
176
176
  )
177
177
 
178
178
 
@@ -354,6 +354,7 @@ def _normalize_document_set(repository_root: Path, document_set: DocumentSet) ->
354
354
  repository_root,
355
355
  all_deleted_paths,
356
356
  all_renamed_paths,
357
+ document_set.base_revision,
357
358
  )
358
359
 
359
360
 
@@ -10,6 +10,7 @@ from .config.constants import (
10
10
  ALL_MISSING_BLOB_ERROR_FRAGMENTS,
11
11
  ALL_UNBORN_HEAD_ERROR_FRAGMENTS,
12
12
  GIT_ENVIRONMENT_VARIABLE_PREFIX,
13
+ GIT_INDEX_ENVIRONMENT_VARIABLE,
13
14
  GIT_EXECUTABLE,
14
15
  NUL_BYTE,
15
16
  UTF8_ENCODING,
@@ -105,6 +106,7 @@ def _git_subprocess_environment() -> dict[str, str]:
105
106
  each_name: each_environment_text
106
107
  for each_name, each_environment_text in os.environ.items()
107
108
  if not each_name.upper().startswith(git_prefix)
109
+ or each_name.upper() == GIT_INDEX_ENVIRONMENT_VARIABLE
108
110
  }
109
111
 
110
112
 
@@ -0,0 +1,200 @@
1
+ /**
2
+ * Keep a pstack skills-directory plugin manifest listing every skill folder.
3
+ *
4
+ * Claude Code loads a folder under `~/.claude/skills` as a plugin when the
5
+ * folder holds `.claude-plugin/plugin.json`, and it namespaces that plugin's
6
+ * skills as `pstack:how`. It does not scan a plugin root for skill folders, so
7
+ * the manifest names each sub-skill path:
8
+ *
9
+ * plugin root holds: how/SKILL.md, why/SKILL.md, docs/, .claude-plugin/
10
+ * manifest before: "skills": ["./how"]
11
+ * manifest after: "skills": ["./how", "./why"] <- why/ picked up
12
+ * second run: no write <- the list matches
13
+ * docs/, .claude-plugin/ <- skipped, no SKILL.md
14
+ *
15
+ * The installer calls `refreshPstackPluginManifest` for a pstack folder the
16
+ * user installed. Running this file directly refreshes the same manifest after
17
+ * a pstack update.
18
+ */
19
+
20
+ import {
21
+ existsSync,
22
+ mkdirSync,
23
+ readdirSync,
24
+ readFileSync,
25
+ realpathSync,
26
+ writeFileSync,
27
+ } from 'node:fs';
28
+ import { homedir } from 'node:os';
29
+ import { join } from 'node:path';
30
+ import { fileURLToPath } from 'node:url';
31
+
32
+ export const PSTACK_PLUGIN_DIRECTORY_NAME = 'pstack';
33
+ export const PSTACK_PLUGIN_MANIFEST_RELATIVE_PATH = join('.claude-plugin', 'plugin.json');
34
+ export const SKILL_MANIFEST_FILE_NAME = 'SKILL.md';
35
+ export const SKILL_PATH_PREFIX = './';
36
+
37
+ const CLAUDE_HOME_DIRECTORY_NAME = '.claude';
38
+ const MANIFEST_SKILLS_KEY = 'skills';
39
+ const MANIFEST_INDENT_SPACES = 2;
40
+ const FILE_ENCODING = 'utf8';
41
+ const HIDDEN_NAME_PREFIX = '.';
42
+
43
+ /**
44
+ * Fields a first write seeds from the published pstack plugin. An existing
45
+ * manifest keeps its own values. This refresher changes only the skills list.
46
+ */
47
+ const SEEDED_MANIFEST_FIELDS = Object.freeze({
48
+ $schema: 'https://anthropic.com/claude-code/plugin.schema.json',
49
+ name: PSTACK_PLUGIN_DIRECTORY_NAME,
50
+ version: '0.14.8',
51
+ description:
52
+ 'if you want to go fast, go deep first. pstack helps you write less, but higher '
53
+ + 'quality code. rigorous agent workflows you can parallelize with confidence.',
54
+ author: Object.freeze({ name: 'Lauren Tan' }),
55
+ homepage: 'https://github.com/cursor/plugins/tree/main/pstack',
56
+ repository: 'https://github.com/cursor/plugins',
57
+ license: 'MIT',
58
+ });
59
+
60
+ /**
61
+ * Name every sub-folder of a plugin root that Claude Code loads as a skill.
62
+ *
63
+ * @param {string} pluginRoot Plugin folder holding one sub-folder per skill.
64
+ * @returns {string[]} The sorted names of each visible sub-folder holding a
65
+ * skill manifest. A hidden folder and a folder without `SKILL.md` are left out.
66
+ */
67
+ export function findPstackSkillDirectoryNames(pluginRoot) {
68
+ return readdirSync(pluginRoot, { withFileTypes: true })
69
+ .filter((eachEntry) => eachEntry.isDirectory())
70
+ .map((eachEntry) => eachEntry.name)
71
+ .filter((eachName) => !eachName.startsWith(HIDDEN_NAME_PREFIX))
72
+ .filter((eachName) => existsSync(join(pluginRoot, eachName, SKILL_MANIFEST_FILE_NAME)))
73
+ .sort();
74
+ }
75
+
76
+ function readManifestSkillPaths(manifestPath) {
77
+ if (!existsSync(manifestPath)) return { manifest: null, previousSkillPaths: [] };
78
+ const manifest = JSON.parse(readFileSync(manifestPath, FILE_ENCODING));
79
+ const listedSkillPaths = manifest[MANIFEST_SKILLS_KEY];
80
+ if (typeof listedSkillPaths === 'string') {
81
+ return { manifest, previousSkillPaths: [listedSkillPaths] };
82
+ }
83
+ if (Array.isArray(listedSkillPaths)) {
84
+ return { manifest, previousSkillPaths: listedSkillPaths };
85
+ }
86
+ return { manifest, previousSkillPaths: [] };
87
+ }
88
+
89
+ /**
90
+ * Rewrite a pstack plugin manifest's skills list to match its folder.
91
+ *
92
+ * Writes only when the list changes. A second call on an unchanged folder
93
+ * leaves the file byte for byte. An existing manifest keeps every field but
94
+ * the skills list.
95
+ *
96
+ * @param {string} pluginRoot Plugin folder holding the skill sub-folders.
97
+ * @returns {{didWrite: boolean, reason: string, previousSkillPaths: string[],
98
+ * currentSkillPaths: string[]}} Whether this call wrote, and the paths
99
+ * before and after. `reason` names a skip, and is empty on a write or an
100
+ * unchanged list.
101
+ */
102
+ export function refreshPstackPluginManifest(pluginRoot) {
103
+ const declined = { didWrite: false, previousSkillPaths: [], currentSkillPaths: [] };
104
+ if (!existsSync(pluginRoot)) {
105
+ return { ...declined, reason: 'plugin root is absent' };
106
+ }
107
+ if (existsSync(join(pluginRoot, SKILL_MANIFEST_FILE_NAME))) {
108
+ return { ...declined, reason: 'plugin root is itself a skill' };
109
+ }
110
+ const allSkillNames = findPstackSkillDirectoryNames(pluginRoot);
111
+ if (allSkillNames.length === 0) {
112
+ return { ...declined, reason: 'plugin root holds no skills' };
113
+ }
114
+ const manifestPath = join(pluginRoot, PSTACK_PLUGIN_MANIFEST_RELATIVE_PATH);
115
+ const { manifest, previousSkillPaths } = readManifestSkillPaths(manifestPath);
116
+ const currentSkillPaths = allSkillNames.map((eachName) => SKILL_PATH_PREFIX + eachName);
117
+ const outcome = { reason: '', previousSkillPaths, currentSkillPaths };
118
+ const isUnchanged = manifest !== null
119
+ && previousSkillPaths.length === currentSkillPaths.length
120
+ && previousSkillPaths.every((eachPath, eachIndex) => eachPath === currentSkillPaths[eachIndex]);
121
+ if (isUnchanged) {
122
+ return { ...outcome, didWrite: false };
123
+ }
124
+ const refreshedManifest = {
125
+ ...(manifest === null ? SEEDED_MANIFEST_FIELDS : manifest),
126
+ [MANIFEST_SKILLS_KEY]: currentSkillPaths,
127
+ };
128
+ mkdirSync(join(pluginRoot, '.claude-plugin'), { recursive: true });
129
+ writeFileSync(
130
+ manifestPath,
131
+ JSON.stringify(refreshedManifest, null, MANIFEST_INDENT_SPACES) + '\n',
132
+ FILE_ENCODING,
133
+ );
134
+ return { ...outcome, didWrite: true };
135
+ }
136
+
137
+ /**
138
+ * Return the pstack plugin folder under a Claude home.
139
+ *
140
+ * @param {string} claudeHome The Claude home directory holding `skills`.
141
+ * @returns {string} The pstack plugin folder path, present or not.
142
+ */
143
+ export function pstackPluginRootFor(claudeHome) {
144
+ return join(claudeHome, 'skills', PSTACK_PLUGIN_DIRECTORY_NAME);
145
+ }
146
+
147
+ /**
148
+ * Return the Claude home used when no folder argument is given.
149
+ *
150
+ * A named profile sets `CLAUDE_CONFIG_DIR`. The installer writes into that
151
+ * root. The command-line form reads the same variable.
152
+ *
153
+ * @param {Record<string, string|undefined>} environment The process environment.
154
+ * @returns {string} The Claude home directory holding `skills`.
155
+ */
156
+ export function claudeHomeFrom(environment) {
157
+ return environment.CLAUDE_CONFIG_DIR || join(homedir(), CLAUDE_HOME_DIRECTORY_NAME);
158
+ }
159
+
160
+ function main() {
161
+ const explicitRoot = process.argv[2];
162
+ const pluginRoot = explicitRoot || pstackPluginRootFor(claudeHomeFrom(process.env));
163
+ const outcome = refreshPstackPluginManifest(pluginRoot);
164
+ if (!outcome.didWrite && outcome.reason) {
165
+ console.log(`No manifest written: ${outcome.reason}.`);
166
+ return;
167
+ }
168
+ if (!outcome.didWrite) {
169
+ console.log(`No change. ${outcome.currentSkillPaths.length} skills listed.`);
170
+ return;
171
+ }
172
+ console.log(`Updated. ${outcome.currentSkillPaths.length} skills listed.`);
173
+ for (const eachPath of outcome.currentSkillPaths) {
174
+ if (!outcome.previousSkillPaths.includes(eachPath)) console.log(` added ${eachPath}`);
175
+ }
176
+ for (const eachPath of outcome.previousSkillPaths) {
177
+ if (!outcome.currentSkillPaths.includes(eachPath)) console.log(` removed ${eachPath}`);
178
+ }
179
+ }
180
+
181
+ /**
182
+ * Return whether node ran this module as its entry point.
183
+ *
184
+ * A directory link to this script gives node an invoked path that differs from
185
+ * this module's own path. Both sides resolve to their real location before
186
+ * the comparison.
187
+ *
188
+ * @param {string|undefined} invokedPath The path node reports as `argv[1]`.
189
+ * @returns {boolean} True when node ran this module as its entry point.
190
+ */
191
+ function isEntryPoint(invokedPath) {
192
+ if (!invokedPath) return false;
193
+ try {
194
+ return realpathSync(fileURLToPath(import.meta.url)) === realpathSync(invokedPath);
195
+ } catch {
196
+ return fileURLToPath(import.meta.url) === invokedPath;
197
+ }
198
+ }
199
+
200
+ if (isEntryPoint(process.argv[1])) main();
@@ -131,11 +131,6 @@ ALL_TRACKED_SECRET_EXACT_EXEMPTIONS: frozenset[tuple[str, str, str]] = frozenset
131
131
  "home-path",
132
132
  "09f83c224a02c370d3d63de5c330e1a830fc92866330fc7963b5b5fd20ffcb91",
133
133
  ),
134
- (
135
- "packages/claude-dev-env/hooks/blocking/volatile_path_in_post_blocker.py",
136
- "home-path",
137
- "ed9203c8078e4c6b832600085f28f24656d679aa8dca82ecc2557afc3df25036",
138
- ),
139
134
  (
140
135
  "packages/claude-dev-env/hooks/hooks_constants/hardcoded_user_path_constants.py",
141
136
  "home-path",
@@ -11,7 +11,9 @@ from collections.abc import Mapping
11
11
  from pathlib import Path
12
12
 
13
13
  from dev_env_scripts_constants.shared_tree_constants import (
14
+ AGENTS_DIRECTORY_SUFFIX,
14
15
  CLAUDE_CONFIG_DIR_ENV_VAR,
16
+ DEFAULT_MANAGED_ROOT_NAME,
15
17
  PROCESS_TREE_DIRECTORY_NAME,
16
18
  PROCESS_TREE_KILL_MODULE_FILENAME,
17
19
  SCRIPTS_DIRECTORY_NAME,
@@ -29,6 +31,29 @@ def _shared_scripts_directory(from_root: Path, shared_subpackage_name: str) -> P
29
31
  )
30
32
 
31
33
 
34
+ def _linked_managed_root(module_path: Path, anchor_depth: int) -> Path | None:
35
+ """Resolve the managed root whose scripts pointer targets this installed tree.
36
+
37
+ Args:
38
+ module_path: The module path inside the managed scripts tree.
39
+ anchor_depth: Parent index of the package or agents root.
40
+
41
+ Returns:
42
+ The Claude managed root when its scripts pointer resolves to this tree.
43
+ """
44
+ package_root = module_path.resolve().parents[anchor_depth]
45
+ if not package_root.name.endswith(AGENTS_DIRECTORY_SUFFIX):
46
+ return None
47
+ managed_name = package_root.name.removesuffix(AGENTS_DIRECTORY_SUFFIX) or DEFAULT_MANAGED_ROOT_NAME
48
+ managed_root = package_root.parent / managed_name
49
+ linked_scripts = managed_root / SCRIPTS_DIRECTORY_NAME
50
+ if not linked_scripts.is_dir():
51
+ return None
52
+ if linked_scripts.resolve() != package_root / SCRIPTS_DIRECTORY_NAME:
53
+ return None
54
+ return managed_root
55
+
56
+
32
57
  def resolve_shared_scripts_directory(
33
58
  module_file: str | Path,
34
59
  all_environment: Mapping[str, str],
@@ -44,9 +69,10 @@ def resolve_shared_scripts_directory(
44
69
  ok: an un-resolved parent keeps the link side, where _shared sits
45
70
  flag: a resolved parent walks the link and misses _shared entirely
46
71
 
47
- Tries the un-resolved parent first, then the configured managed root,
48
- then the resolved parent. When none holds the marker, returns the
49
- un-resolved candidate, so an import failure names a readable path.
72
+ Tries the un-resolved parent first, then a verified installed scripts
73
+ pointer, then the configured managed root, then the resolved parent.
74
+ If none holds the marker, returns the un-resolved candidate so an
75
+ import failure names a readable path.
50
76
 
51
77
  Args:
52
78
  module_file: Path of the importing module (``__file__``).
@@ -63,6 +89,9 @@ def resolve_shared_scripts_directory(
63
89
  module_path.absolute().parents[anchor_depth], shared_subpackage_name
64
90
  )
65
91
  all_candidates = [un_resolved_candidate]
92
+ linked_root = _linked_managed_root(module_path, anchor_depth)
93
+ if linked_root is not None:
94
+ all_candidates.append(_shared_scripts_directory(linked_root, shared_subpackage_name))
66
95
  configured_root_text = all_environment.get(CLAUDE_CONFIG_DIR_ENV_VAR, "").strip()
67
96
  if configured_root_text:
68
97
  all_candidates.append(
@@ -0,0 +1,332 @@
1
+ """Tests for action-aware durable GitHub post linting."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ import sys
7
+ from pathlib import Path
8
+
9
+ import pytest
10
+
11
+ _SCRIPTS_DIRECTORY = Path(__file__).resolve().parent
12
+ if str(_SCRIPTS_DIRECTORY) not in sys.path:
13
+ sys.path.insert(0, str(_SCRIPTS_DIRECTORY))
14
+
15
+ import durable_post_lint
16
+
17
+ SCRIPT_PATH = _SCRIPTS_DIRECTORY / "durable_post_lint.py"
18
+ VALID_PR_BODY = """## Summary
19
+
20
+ Summary text.
21
+
22
+ ## Description
23
+
24
+ Description text.
25
+
26
+ ## Why
27
+
28
+ Why text.
29
+
30
+ ## How
31
+
32
+ How text.
33
+
34
+ ## Verification
35
+
36
+ Verification text.
37
+ """
38
+
39
+
40
+ @pytest.mark.parametrize(
41
+ ("action", "title", "body_text"),
42
+ [
43
+ ("pr-create", "feat: add durable post linting", VALID_PR_BODY),
44
+ ("pr-edit", "fix(cli): validate post input", None),
45
+ ("pr-comment", None, "Comment body."),
46
+ ("pr-review", None, "Review body."),
47
+ ("issue-create", None, "Issue body."),
48
+ ("issue-edit", None, "Updated issue body."),
49
+ ("issue-comment", None, "Issue comment."),
50
+ ("github-mcp-post", None, "MCP body."),
51
+ ],
52
+ )
53
+ def test_all_actions_accept_valid_inputs(
54
+ action: str, title: str | None, body_text: str | None
55
+ ) -> None:
56
+ all_findings = durable_post_lint.lint_durable_post(
57
+ action=action,
58
+ title=title,
59
+ body_text=body_text,
60
+ )
61
+ assert all_findings == ()
62
+
63
+
64
+ def test_unknown_action_raises_usage_error() -> None:
65
+ with pytest.raises(durable_post_lint.DurablePostUsageError):
66
+ durable_post_lint.lint_durable_post(
67
+ action="release-create",
68
+ title=None,
69
+ body_text="Release body.",
70
+ )
71
+
72
+
73
+ @pytest.mark.parametrize(
74
+ ("title", "body_text"),
75
+ [
76
+ (None, VALID_PR_BODY),
77
+ ("feat: add durable post linting", None),
78
+ ],
79
+ )
80
+ def test_pr_create_requires_title_and_body(
81
+ title: str | None, body_text: str | None
82
+ ) -> None:
83
+ with pytest.raises(durable_post_lint.DurablePostUsageError):
84
+ durable_post_lint.lint_durable_post(
85
+ action="pr-create",
86
+ title=title,
87
+ body_text=body_text,
88
+ )
89
+
90
+
91
+ def test_pr_edit_requires_one_edit_field() -> None:
92
+ with pytest.raises(durable_post_lint.DurablePostUsageError):
93
+ durable_post_lint.lint_durable_post(
94
+ action="pr-edit",
95
+ title=None,
96
+ body_text=None,
97
+ )
98
+
99
+
100
+ @pytest.mark.parametrize(
101
+ "action",
102
+ [
103
+ "pr-comment",
104
+ "pr-review",
105
+ "issue-create",
106
+ "issue-edit",
107
+ "issue-comment",
108
+ "github-mcp-post",
109
+ ],
110
+ )
111
+ def test_body_actions_require_body(action: str) -> None:
112
+ with pytest.raises(durable_post_lint.DurablePostUsageError):
113
+ durable_post_lint.lint_durable_post(
114
+ action=action,
115
+ title=None,
116
+ body_text=None,
117
+ )
118
+
119
+
120
+ @pytest.mark.parametrize(
121
+ "title",
122
+ [
123
+ "feat: add durable post linting",
124
+ "fix(cli): validate body files",
125
+ "refactor!: replace post validation",
126
+ "docs(skill)!: explain durable posts",
127
+ ],
128
+ )
129
+ def test_pr_titles_accept_repository_conventional_forms(title: str) -> None:
130
+ all_findings = durable_post_lint.lint_durable_post(
131
+ action="pr-edit",
132
+ title=title,
133
+ body_text=None,
134
+ )
135
+ assert all_findings == ()
136
+
137
+
138
+ @pytest.mark.parametrize(
139
+ "title",
140
+ [
141
+ "Add durable post linting",
142
+ "feature: add durable post linting",
143
+ "fix(): add durable post linting",
144
+ "fix:add durable post linting",
145
+ "fix: ",
146
+ ],
147
+ )
148
+ def test_pr_titles_reject_nonconventional_forms(title: str) -> None:
149
+ all_findings = durable_post_lint.lint_durable_post(
150
+ action="pr-edit",
151
+ title=title,
152
+ body_text=None,
153
+ )
154
+ assert [each_finding.code for each_finding in all_findings] == ["invalid-pr-title"]
155
+
156
+
157
+ @pytest.mark.parametrize(
158
+ "missing_heading",
159
+ ["Summary", "Description", "Why", "How", "Verification"],
160
+ )
161
+ def test_pr_body_reports_each_missing_heading(missing_heading: str) -> None:
162
+ body_text = VALID_PR_BODY.replace(f"## {missing_heading}\n", "")
163
+ all_findings = durable_post_lint.lint_durable_post(
164
+ action="pr-create",
165
+ title="feat: add durable post linting",
166
+ body_text=body_text,
167
+ )
168
+ assert [each_finding.message for each_finding in all_findings] == [
169
+ f"body is missing required heading: {missing_heading}"
170
+ ]
171
+
172
+
173
+ def test_non_pr_body_needs_no_pr_headings() -> None:
174
+ all_findings = durable_post_lint.lint_durable_post(
175
+ action="issue-comment",
176
+ title=None,
177
+ body_text="A concise issue comment.",
178
+ )
179
+ assert all_findings == ()
180
+
181
+
182
+ @pytest.mark.parametrize(
183
+ ("body_text", "expected_marker"),
184
+ [
185
+ (
186
+ r"See C:\Users\example\.claude-profile-a\jobs\job-1\artifact.png",
187
+ ".claude-profile-a/jobs/",
188
+ ),
189
+ (
190
+ r"Edited C:\Users\example\.claude\worktrees\feature\file.py",
191
+ ".claude/worktrees/",
192
+ ),
193
+ (
194
+ r"Saved in C:\Users\example\AppData\Local\Temp\report.txt",
195
+ "appdata/local/temp",
196
+ ),
197
+ ("Saved in /tmp/report.txt", "/tmp/"),
198
+ (r"Saved in %TEMP%\report.txt", "%temp%"),
199
+ (r"Saved in $env:TEMP\report.txt", "$env:temp"),
200
+ ("Saved in $CLAUDE_JOB_DIR/report.txt", "$claude_job_dir"),
201
+ ],
202
+ )
203
+ def test_volatile_path_markers_match_hook_behavior(
204
+ body_text: str, expected_marker: str
205
+ ) -> None:
206
+ assert durable_post_lint.find_volatile_path_marker(body_text) == expected_marker
207
+
208
+
209
+ @pytest.mark.parametrize(
210
+ "body_text",
211
+ [
212
+ "the `.claude/worktrees/` prefix is a manifest key",
213
+ ".claude/worktrees/ entries are skipped by the manifest",
214
+ "(.claude-profile-a/jobs/) is a directory name",
215
+ "worktrees live under `.claude/worktrees/<name>`",
216
+ "the constant is `.claude/worktrees/`. Next.",
217
+ ],
218
+ )
219
+ def test_anchored_marker_near_misses_remain_allowed(body_text: str) -> None:
220
+ assert durable_post_lint.find_volatile_path_marker(body_text) is None
221
+
222
+
223
+ def test_volatile_body_returns_content_finding() -> None:
224
+ all_findings = durable_post_lint.lint_durable_post(
225
+ action="issue-comment",
226
+ title=None,
227
+ body_text="Saved in /tmp/report.txt",
228
+ )
229
+ assert [each_finding.code for each_finding in all_findings] == [
230
+ "volatile-local-path"
231
+ ]
232
+
233
+
234
+ def test_read_body_file_accepts_utf8(tmp_path: Path) -> None:
235
+ body_file = tmp_path / "body.md"
236
+ body_file.write_text("Café", encoding="utf-8")
237
+ assert durable_post_lint.read_body_file(body_file) == "Café"
238
+
239
+
240
+ def test_read_body_file_rejects_missing_file(tmp_path: Path) -> None:
241
+ missing_file = tmp_path / "missing.md"
242
+ with pytest.raises(durable_post_lint.DurablePostInputError):
243
+ durable_post_lint.read_body_file(missing_file)
244
+
245
+
246
+ def test_read_body_file_rejects_invalid_utf8(tmp_path: Path) -> None:
247
+ body_file = tmp_path / "body.md"
248
+ body_file.write_bytes(b"\xff\xfe")
249
+ with pytest.raises(durable_post_lint.DurablePostInputError):
250
+ durable_post_lint.read_body_file(body_file)
251
+
252
+
253
+ def test_cli_returns_clean_exit_for_valid_pr_create(tmp_path: Path) -> None:
254
+ body_file = tmp_path / "body.md"
255
+ body_file.write_text(VALID_PR_BODY, encoding="utf-8")
256
+ completed_process = subprocess.run(
257
+ [
258
+ sys.executable,
259
+ str(SCRIPT_PATH),
260
+ "--action",
261
+ "pr-create",
262
+ "--title",
263
+ "feat: add durable post linting",
264
+ "--body-file",
265
+ str(body_file),
266
+ ],
267
+ check=False,
268
+ capture_output=True,
269
+ text=True,
270
+ )
271
+ assert completed_process.returncode == 0
272
+ assert completed_process.stdout == ""
273
+ assert completed_process.stderr == ""
274
+
275
+
276
+ def test_cli_returns_content_exit_without_echoing_body(tmp_path: Path) -> None:
277
+ body_text = "private body words saved in /tmp/report.txt"
278
+ body_file = tmp_path / "body.md"
279
+ body_file.write_text(body_text, encoding="utf-8")
280
+ completed_process = subprocess.run(
281
+ [
282
+ sys.executable,
283
+ str(SCRIPT_PATH),
284
+ "--action",
285
+ "issue-comment",
286
+ "--body-file",
287
+ str(body_file),
288
+ ],
289
+ check=False,
290
+ capture_output=True,
291
+ text=True,
292
+ )
293
+ assert completed_process.returncode == 1
294
+ assert "volatile local artifact path" in completed_process.stderr
295
+ assert body_text not in completed_process.stderr
296
+
297
+
298
+ def test_cli_returns_usage_exit_for_missing_body_file(tmp_path: Path) -> None:
299
+ missing_file = tmp_path / "missing.md"
300
+ completed_process = subprocess.run(
301
+ [
302
+ sys.executable,
303
+ str(SCRIPT_PATH),
304
+ "--action",
305
+ "issue-comment",
306
+ "--body-file",
307
+ str(missing_file),
308
+ ],
309
+ check=False,
310
+ capture_output=True,
311
+ text=True,
312
+ )
313
+ assert completed_process.returncode == 2
314
+ assert "body file is not readable" in completed_process.stderr
315
+
316
+
317
+ def test_cli_has_no_inline_body_flag() -> None:
318
+ completed_process = subprocess.run(
319
+ [
320
+ sys.executable,
321
+ str(SCRIPT_PATH),
322
+ "--action",
323
+ "issue-comment",
324
+ "--body",
325
+ "Inline body.",
326
+ ],
327
+ check=False,
328
+ capture_output=True,
329
+ text=True,
330
+ )
331
+ assert completed_process.returncode == 2
332
+ assert "unrecognized arguments" in completed_process.stderr
@@ -171,3 +171,36 @@ def test_generalized_resolver_serves_a_deeper_anchor(tmp_path: Path) -> None:
171
171
  3,
172
172
  )
173
173
  assert resolved_scripts_directory == expected_scripts_directory
174
+
175
+
176
+ @pytest.mark.parametrize("managed_name", (".claude", "profile-a"))
177
+ def test_resolved_agents_scripts_find_the_verified_managed_root(
178
+ tmp_path: Path, managed_name: str,
179
+ ) -> None:
180
+ agents_name = ".agents" if managed_name == ".claude" else f"{managed_name}.agents"
181
+ agents_root = tmp_path / agents_name
182
+ scripts_directory = agents_root / "scripts"
183
+ scripts_directory.mkdir(parents=True)
184
+ module_file = scripts_directory / "runner.py"
185
+ module_file.write_text("", encoding="utf-8")
186
+ managed_root = tmp_path / managed_name
187
+ managed_root.mkdir()
188
+ _create_directory_link(from_link=managed_root / "scripts", to_target=scripts_directory)
189
+ expected = _write_process_tree_kill_stub(managed_root)
190
+ resolved_directory = shared_tree_paths.resolve_shared_process_tree_scripts_directory(module_file, {})
191
+ assert resolved_directory == expected
192
+
193
+
194
+ def test_neighboring_managed_root_cannot_supply_a_foreign_shared_tree(tmp_path: Path) -> None:
195
+ agents_root = tmp_path / ".agents"
196
+ scripts_directory = agents_root / "scripts"
197
+ scripts_directory.mkdir(parents=True)
198
+ module_file = scripts_directory / "runner.py"
199
+ module_file.write_text("", encoding="utf-8")
200
+ managed_root = tmp_path / ".claude"
201
+ foreign_scripts = managed_root / "scripts"
202
+ foreign_scripts.mkdir(parents=True)
203
+ _write_process_tree_kill_stub(managed_root)
204
+ resolved_directory = shared_tree_paths.resolve_shared_process_tree_scripts_directory(module_file, {})
205
+ expected = agents_root / "_shared" / "process-tree" / "scripts"
206
+ assert resolved_directory == expected