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,16 +1,8 @@
1
- """Native-equivalence tests for the nativized PreToolUse hosted hooks.
2
-
3
- For the state_description_blocker hook the dispatcher runs natively. This suite
4
- asserts the native evaluate() call and the hook's standalone __main__ subprocess
5
- path decide identically on the same payload: same allow-or-deny, same deny-reason
6
- text. It also asserts the dispatcher reaches the same decision through its native
7
- path.
8
-
9
- The corpus pairs allowing payloads with denying payloads for each hook so the
10
- equivalence holds across both outcomes. Each payload names an absolute file
11
- under this repository and carries the repository root as its ``cwd``, so the
12
- decision belongs to this checkout rather than to the directory pytest runs
13
- from.
1
+ """Preserve detector equivalence while retiring its dispatcher registration.
2
+
3
+ The standalone state-description detector remains callable by the policy linter.
4
+ Its function and script agree on valid and invalid inputs; the write dispatcher
5
+ allows these policy findings to reach the staged linter.
14
6
  """
15
7
 
16
8
  from __future__ import annotations
@@ -195,28 +187,6 @@ def _deny_reason_from_dispatcher(payload_dictionary: dict[str, object]) -> str |
195
187
  )
196
188
  return _deny_reason_from_script_stdout(completed_process.stdout.strip())
197
189
 
198
- def _deny_payload_from_dispatcher(payload_dictionary: dict[str, object]) -> dict[str, object]:
199
- """Run the dispatcher as a subprocess and return its parsed deny payload.
200
-
201
- Args:
202
- payload_dictionary: The payload dict to send as JSON on stdin.
203
-
204
- Returns:
205
- The dispatcher's emitted deny JSON parsed into a dict.
206
- """
207
- completed_process = subprocess.run(
208
- [sys.executable, _DISPATCHER_SCRIPT],
209
- check=False,
210
- input=json.dumps(payload_dictionary),
211
- capture_output=True,
212
- text=True,
213
- encoding="utf-8",
214
- env=_subprocess_environment(),
215
- )
216
- parsed_payload = json.loads(completed_process.stdout.strip())
217
- assert isinstance(parsed_payload, dict)
218
- return parsed_payload
219
-
220
190
  def test_state_description_native_allows_match_script() -> None:
221
191
  """state_description_blocker native allow matches the script's allow."""
222
192
  payload_dictionary = _write_payload_dictionary(_MARKDOWN_PATH, _STATE_DESCRIPTION_ALLOW_CONTENT)
@@ -262,18 +232,17 @@ def test_state_description_native_multi_edit_deny_matches_script() -> None:
262
232
  assert native_reason == script_reason
263
233
 
264
234
 
265
- def test_dispatcher_native_path_denies_state_description() -> None:
266
- """The dispatcher's native path denies a state_description_blocker violation."""
235
+ def test_dispatcher_allows_the_retired_state_description_rule() -> None:
236
+ """The detector still finds the violation, but the write dispatcher allows it."""
267
237
  payload_dictionary = _write_payload_dictionary(_MARKDOWN_PATH, _STATE_DESCRIPTION_DENY_CONTENT)
268
238
  native_reason = _evaluate_with_prose_style_opt_in(state_description_blocker.evaluate, payload_dictionary)
269
239
  dispatcher_reason = _deny_reason_from_dispatcher(payload_dictionary)
270
240
  assert native_reason is not None
271
- assert dispatcher_reason is not None
272
- assert native_reason in dispatcher_reason
241
+ assert dispatcher_reason is None
273
242
 
274
243
 
275
- def test_dispatcher_native_state_description_carries_additional_context() -> None:
276
- """The dispatcher's state-description deny carries the standalone additionalContext."""
244
+ def test_standalone_state_description_retains_additional_context() -> None:
245
+ """The retained standalone interface still supplies its corrective context."""
277
246
  payload_dictionary = _write_payload_dictionary(_MARKDOWN_PATH, _STATE_DESCRIPTION_DENY_CONTENT)
278
247
  deny_reason = _evaluate_with_prose_style_opt_in(state_description_blocker.evaluate, payload_dictionary)
279
248
  assert deny_reason is not None
@@ -281,20 +250,20 @@ def test_dispatcher_native_state_description_carries_additional_context() -> Non
281
250
  standalone_hook_specific = standalone_payload["hookSpecificOutput"]
282
251
  assert isinstance(standalone_hook_specific, dict)
283
252
  expected_additional_context = standalone_hook_specific["additionalContext"]
284
- dispatcher_payload = _deny_payload_from_dispatcher(payload_dictionary)
285
- dispatcher_hook_specific = dispatcher_payload.get("hookSpecificOutput", {})
286
- assert isinstance(dispatcher_hook_specific, dict)
287
- assert dispatcher_hook_specific.get("additionalContext") == expected_additional_context
253
+ script_payload = json.loads(_run_script_subprocess(_STATE_DESCRIPTION_SCRIPT, payload_dictionary))
254
+ script_hook_specific = script_payload.get("hookSpecificOutput", {})
255
+ assert isinstance(script_hook_specific, dict)
256
+ assert script_hook_specific.get("additionalContext") == expected_additional_context
288
257
 
289
- def test_dispatcher_native_state_description_carries_system_message() -> None:
290
- """The dispatcher's state-description deny carries the standalone systemMessage."""
258
+ def test_standalone_state_description_retains_system_message() -> None:
259
+ """The retained standalone interface still supplies its system message."""
291
260
  payload_dictionary = _write_payload_dictionary(_MARKDOWN_PATH, _STATE_DESCRIPTION_DENY_CONTENT)
292
261
  deny_reason = _evaluate_with_prose_style_opt_in(state_description_blocker.evaluate, payload_dictionary)
293
262
  assert deny_reason is not None
294
263
  standalone_payload = state_description_blocker.build_deny_payload(deny_reason)
295
264
  expected_system_message = standalone_payload["systemMessage"]
296
265
  assert isinstance(expected_system_message, str)
297
- dispatcher_payload = _deny_payload_from_dispatcher(payload_dictionary)
298
- dispatcher_system_message = dispatcher_payload.get("systemMessage")
299
- assert isinstance(dispatcher_system_message, str)
300
- assert expected_system_message in dispatcher_system_message
266
+ script_payload = json.loads(_run_script_subprocess(_STATE_DESCRIPTION_SCRIPT, payload_dictionary))
267
+ script_system_message = script_payload.get("systemMessage")
268
+ assert isinstance(script_system_message, str)
269
+ assert expected_system_message in script_system_message
@@ -10,7 +10,6 @@ from __future__ import annotations
10
10
  from collections.abc import Iterator
11
11
  from contextlib import contextmanager
12
12
  import json
13
- import shutil
14
13
  import stat
15
14
  import subprocess
16
15
  import sys
@@ -21,12 +20,20 @@ import pytest
21
20
  HOOKS_ROOT = Path(__file__).resolve().parent.parent
22
21
  BLOCKING_ROOT = HOOKS_ROOT / "blocking"
23
22
  GIT_HOOKS_ROOT = HOOKS_ROOT / "git-hooks"
24
- PRE_COMMIT_SOURCE_PATH = GIT_HOOKS_ROOT / "pre_commit.py"
25
- GATE_UTILS_SOURCE_PATH = GIT_HOOKS_ROOT / "gate_utils.py"
26
- GIT_HOOKS_CONSTANTS_SOURCE_PATH = GIT_HOOKS_ROOT / "git_hooks_constants"
27
23
  DISPATCHER_SOURCE_PATH = BLOCKING_ROOT / "bash_pre_tool_use_dispatcher.py"
28
24
  GIT_COMMAND_TIMEOUT_SECONDS = 60
29
- CLEAN_MODULE_SOURCE = "def add_one(number: int) -> int:\n return number + 1\n"
25
+ CLEAN_MODULE_SOURCE = (
26
+ "def add_one(number: int) -> int:\n"
27
+ ' """Increase the number.\n\n'
28
+ " Args:\n number: The input number.\n\n"
29
+ " Returns:\n The next number.\n"
30
+ ' """\n'
31
+ " return number + 1\n"
32
+ )
33
+ PAIRED_TEST_SOURCE = (
34
+ "from widget import add_one\n\n"
35
+ "def test_add_one() -> None:\n assert add_one(1) == 2\n"
36
+ )
30
37
 
31
38
 
32
39
  def run_git(
@@ -69,21 +76,13 @@ def write_gate_script(gate_path: Path, marker_path: Path, exit_code: int) -> Non
69
76
 
70
77
 
71
78
  def install_native_pre_commit(hooks_path: Path) -> None:
72
- """Install the native pre-commit module and its self-contained imports."""
79
+ """Register the actual package owner with its complete linter dependency tree."""
73
80
  hooks_path.mkdir()
74
- shutil.copyfile(PRE_COMMIT_SOURCE_PATH, hooks_path / "pre_commit.py")
75
- shutil.copyfile(GATE_UTILS_SOURCE_PATH, hooks_path / "gate_utils.py")
76
- shutil.copytree(
77
- GIT_HOOKS_CONSTANTS_SOURCE_PATH,
78
- hooks_path / "git_hooks_constants",
79
- )
80
81
  pre_commit_path = hooks_path / "pre-commit"
81
82
  pre_commit_path.write_text(
82
83
  "#!/usr/bin/env python3\n"
83
84
  "import sys\n"
84
- "from pathlib import Path\n"
85
- "shim_directory = Path(__file__).resolve().parent\n"
86
- "sys.path.insert(0, str(shim_directory))\n"
85
+ f"sys.path.insert(0, {str(GIT_HOOKS_ROOT)!r})\n"
87
86
  "import pre_commit\n"
88
87
  "sys.exit(pre_commit.main())\n",
89
88
  encoding="utf-8",
@@ -133,9 +132,10 @@ def temporary_hooks_path(repository_root: Path, benchmark_hooks_path: Path) -> I
133
132
 
134
133
 
135
134
  def stage_module(repository_root: Path) -> None:
136
- """Write and stage one Python module for the commit fixture."""
135
+ """Stage a valid module and matching test before exercising the legacy gate."""
137
136
  (repository_root / "widget.py").write_text(CLEAN_MODULE_SOURCE, encoding="utf-8")
138
- run_git(repository_root, "add", "widget.py")
137
+ (repository_root / "test_widget.py").write_text(PAIRED_TEST_SOURCE, encoding="utf-8")
138
+ run_git(repository_root, "add", "widget.py", "test_widget.py")
139
139
 
140
140
 
141
141
  def build_bash_payload(repository_root: Path) -> str:
@@ -216,7 +216,7 @@ def test_temporary_hooks_path_restores_shared_worktree_config_on_timeout(
216
216
  initialize_repository(tmp_path)
217
217
  supported_hooks_path = tmp_path / "supported_hooks"
218
218
  supported_hooks_path.mkdir()
219
- run_git(tmp_path, "config", "--local", "core.hooksPath", str(supported_hooks_path))
219
+ run_git(tmp_path, "config", "core.hooksPath", str(supported_hooks_path))
220
220
  unrelated_worktree = tmp_path.parent / "unrelated_worktree"
221
221
  run_git(tmp_path, "worktree", "add", "--detach", str(unrelated_worktree))
222
222
  benchmark_hooks_path = tmp_path / "benchmark_hooks"
@@ -0,0 +1,195 @@
1
+ """Extract durable GitHub post body text for privacy checks."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import shlex
6
+ from pathlib import Path
7
+
8
+ from blocking._gh_body_arg_utils import (
9
+ all_body_flags,
10
+ body_file_flag,
11
+ body_file_short_flag,
12
+ count_extra_tokens_to_skip_for_split_quoted_value,
13
+ get_logical_first_line,
14
+ is_unresolvable_shell_value,
15
+ match_body_file_equals_prefix,
16
+ match_body_flag_equals_prefix,
17
+ strip_surrounding_quotes,
18
+ )
19
+ from hooks_constants.gh_post_body_texts_constants import (
20
+ ALL_GH_POST_SUBCOMMANDS,
21
+ ALL_MCP_BODY_PARAM_NAMES,
22
+ BODY_FILE_ENCODING,
23
+ BODY_FLAG_WITH_VALUE_STEP,
24
+ GH_COMMAND_NAME,
25
+ MINIMUM_POST_SUBCOMMAND_TOKEN_COUNT,
26
+ TOKEN_JOIN_SEPARATOR,
27
+ )
28
+
29
+
30
+ def _is_environment_assignment(token: str) -> bool:
31
+ equals_index = token.find("=")
32
+ if equals_index <= 0:
33
+ return False
34
+ name_part = token[:equals_index]
35
+ if not (name_part[0].isalpha() or name_part[0] == "_"):
36
+ return False
37
+ return all(each_character.isalnum() or each_character == "_" for each_character in name_part)
38
+
39
+
40
+ def _tokens_name_gh_post_command(all_command_tokens: list[str]) -> bool:
41
+ command_index = 0
42
+ while command_index < len(all_command_tokens) and _is_environment_assignment(
43
+ all_command_tokens[command_index]
44
+ ):
45
+ command_index += 1
46
+ if command_index >= len(all_command_tokens):
47
+ return False
48
+ if all_command_tokens[command_index] != GH_COMMAND_NAME:
49
+ return False
50
+ remaining_tokens = all_command_tokens[command_index + 1 :]
51
+ if len(remaining_tokens) < MINIMUM_POST_SUBCOMMAND_TOKEN_COUNT:
52
+ return False
53
+ post_noun, post_verb = remaining_tokens[:MINIMUM_POST_SUBCOMMAND_TOKEN_COUNT]
54
+ return post_verb in ALL_GH_POST_SUBCOMMANDS.get(post_noun, frozenset())
55
+
56
+
57
+ def _reassemble_split_quoted_value(
58
+ value_token: str,
59
+ all_following_tokens: list[str],
60
+ ) -> tuple[str, int]:
61
+ extra_token_count = count_extra_tokens_to_skip_for_split_quoted_value(
62
+ all_following_tokens, value_token
63
+ )
64
+ if not extra_token_count:
65
+ return strip_surrounding_quotes(value_token), 0
66
+ joined_value = (
67
+ value_token
68
+ + TOKEN_JOIN_SEPARATOR
69
+ + TOKEN_JOIN_SEPARATOR.join(all_following_tokens[:extra_token_count])
70
+ )
71
+ return strip_surrounding_quotes(joined_value), extra_token_count
72
+
73
+
74
+ def _extract_flag_value_at(
75
+ all_command_tokens: list[str],
76
+ token_index: int,
77
+ ) -> tuple[bool, str, int] | None:
78
+ current_token = all_command_tokens[token_index]
79
+ following_tokens = all_command_tokens[token_index + 1 :]
80
+ body_prefix = match_body_flag_equals_prefix(current_token)
81
+ if body_prefix is not None:
82
+ value, extra_count = _reassemble_split_quoted_value(
83
+ current_token[len(body_prefix) :], following_tokens
84
+ )
85
+ return False, value, token_index + 1 + extra_count
86
+ body_file_prefix = match_body_file_equals_prefix(current_token)
87
+ if body_file_prefix is not None:
88
+ value, extra_count = _reassemble_split_quoted_value(
89
+ current_token[len(body_file_prefix) :], following_tokens
90
+ )
91
+ return True, value, token_index + 1 + extra_count
92
+ if current_token in all_body_flags and following_tokens:
93
+ value, extra_count = _reassemble_split_quoted_value(
94
+ following_tokens[0], following_tokens[1:]
95
+ )
96
+ return False, value, token_index + BODY_FLAG_WITH_VALUE_STEP + extra_count
97
+ if current_token in (body_file_flag, body_file_short_flag) and following_tokens:
98
+ value, extra_count = _reassemble_split_quoted_value(
99
+ following_tokens[0], following_tokens[1:]
100
+ )
101
+ return True, value, token_index + BODY_FLAG_WITH_VALUE_STEP + extra_count
102
+ return None
103
+
104
+
105
+ def _collect_body_flag_values(
106
+ all_command_tokens: list[str],
107
+ ) -> tuple[list[str], list[str]]:
108
+ all_inline_bodies: list[str] = []
109
+ all_body_file_paths: list[str] = []
110
+ token_index = 0
111
+ while token_index < len(all_command_tokens):
112
+ extraction = _extract_flag_value_at(all_command_tokens, token_index)
113
+ if extraction is None:
114
+ token_index += 1
115
+ continue
116
+ is_body_file, value, token_index = extraction
117
+ if is_body_file:
118
+ all_body_file_paths.append(value)
119
+ else:
120
+ all_inline_bodies.append(value)
121
+ return all_inline_bodies, all_body_file_paths
122
+
123
+
124
+ def _read_body_file(
125
+ body_file_path: str,
126
+ working_directory: str | None,
127
+ ) -> str | None:
128
+ if is_unresolvable_shell_value(body_file_path):
129
+ return None
130
+ resolved_path = Path(body_file_path)
131
+ if not resolved_path.is_absolute() and working_directory:
132
+ resolved_path = Path(working_directory) / body_file_path
133
+ try:
134
+ return resolved_path.read_text(encoding=BODY_FILE_ENCODING)
135
+ except (OSError, UnicodeError):
136
+ return None
137
+
138
+
139
+ def _body_texts_from_tokens(
140
+ all_command_tokens: list[str],
141
+ working_directory: str | None,
142
+ ) -> tuple[list[str], str | None]:
143
+ if not _tokens_name_gh_post_command(all_command_tokens):
144
+ return [], None
145
+ all_body_texts, all_body_file_paths = _collect_body_flag_values(all_command_tokens)
146
+ for each_path in all_body_file_paths:
147
+ file_text = _read_body_file(each_path, working_directory)
148
+ if file_text is None:
149
+ return [], (
150
+ "BLOCKED [pii_prevention_blocker]: durable post uses --body-file "
151
+ f"but '{each_path}' could not be read for PII scanning. Use an "
152
+ "absolute path, ensure the file exists, or pass --body text."
153
+ )
154
+ all_body_texts.append(file_text)
155
+ return all_body_texts, None
156
+
157
+
158
+ def extract_gh_post_body_texts_for_privacy_gate(
159
+ command: str,
160
+ working_directory: str | None = None,
161
+ ) -> tuple[list[str], str | None]:
162
+ """Extract GitHub post text and reject unreadable declared body files.
163
+
164
+ Args:
165
+ command: Raw GitHub CLI command.
166
+ working_directory: Base directory for relative body-file paths.
167
+
168
+ Returns:
169
+ Body texts and no error, or no texts and a rejection message without body text.
170
+ """
171
+ logical_line = get_logical_first_line(command)
172
+ if not logical_line:
173
+ return [], None
174
+ try:
175
+ all_command_tokens = shlex.split(logical_line, posix=False)
176
+ except ValueError:
177
+ return [], None
178
+ return _body_texts_from_tokens(all_command_tokens, working_directory)
179
+
180
+
181
+ def extract_mcp_body_texts(all_tool_input: dict[str, object]) -> list[str]:
182
+ """Extract body and comment strings from a GitHub tool request.
183
+
184
+ Args:
185
+ all_tool_input: GitHub tool input mapping.
186
+
187
+ Returns:
188
+ Nonempty body and comment values.
189
+ """
190
+ all_body_texts: list[str] = []
191
+ for each_name in ALL_MCP_BODY_PARAM_NAMES:
192
+ candidate_text = all_tool_input.get(each_name)
193
+ if isinstance(candidate_text, str) and candidate_text:
194
+ all_body_texts.append(candidate_text)
195
+ return all_body_texts
@@ -0,0 +1,12 @@
1
+ """Constants for the native pre-commit policy-lint step."""
2
+
3
+ POLICY_LINT_PACKAGE_PARENT_INDEX = 2
4
+ POLICY_LINT_SCRIPT_RELATIVE_PATH = "scripts/cde_lint.py"
5
+ POLICY_LINT_STAGED_ARGUMENT = "--staged"
6
+ POLICY_LINT_TIMEOUT_SECONDS = 240
7
+ POLICY_LINT_INFRASTRUCTURE_EXIT_CODE = 2
8
+ POLICY_LINT_UNAVAILABLE_MESSAGE = (
9
+ "cde lint is missing from this hook installation; reinstall claude-dev-env "
10
+ "before committing"
11
+ )
12
+ POLICY_LINT_FAILED_MESSAGE = "cde lint could not complete: {error}"
@@ -1,17 +1,13 @@
1
1
  #!/usr/bin/env python3
2
- """Native Git pre-commit owner for staged CODE_RULES validation.
2
+ """Run staged policy lint, then the existing staged CODE_RULES gate.
3
3
 
4
- Installed to the user's shared git-hooks directory via the claude-dev-env
5
- installer; git invokes this file as `pre-commit` (the installer strips the
6
- `_` and `.py` suffix when copying into the live hooks path).
4
+ Git invokes this module through the installed native pre-commit shim. Policy
5
+ lint reads the Git index, including GIT_INDEX_FILE when Git supplies an
6
+ alternate index. The existing gate still covers checks whose replacement
7
+ coverage is unproven.
7
8
 
8
- The Agent Bash commit surface records passive staged-surface evidence. This
9
- native hook invokes the shared gate and owns the staged commit decision.
10
-
11
- Exit codes:
12
- 0 - staged changes pass the gate (or the gate is not installed locally).
13
- 1 - staged changes introduce one or more blocking violations.
14
- 2 - unexpected invocation failure (e.g., subprocess could not launch).
9
+ A missing linter, launch failure, or timeout is an infrastructure failure.
10
+ Linter diagnostics and failed-rule statuses reach Git.
15
11
  """
16
12
 
17
13
  from __future__ import annotations
@@ -27,6 +23,43 @@ from git_hooks_constants import (
27
23
  IMMEDIATE_SCOPE_ARGUMENT,
28
24
  INVOKE_GATE_FAILURE_MESSAGE,
29
25
  )
26
+ from git_hooks_constants.staged_policy_lint import (
27
+ POLICY_LINT_FAILED_MESSAGE,
28
+ POLICY_LINT_INFRASTRUCTURE_EXIT_CODE,
29
+ POLICY_LINT_PACKAGE_PARENT_INDEX,
30
+ POLICY_LINT_SCRIPT_RELATIVE_PATH,
31
+ POLICY_LINT_STAGED_ARGUMENT,
32
+ POLICY_LINT_TIMEOUT_SECONDS,
33
+ POLICY_LINT_UNAVAILABLE_MESSAGE,
34
+ )
35
+
36
+
37
+ def resolve_policy_lint_script_path() -> Path:
38
+ """Return the policy-linter entry point in this package or managed home."""
39
+ package_root = Path(__file__).resolve().parents[POLICY_LINT_PACKAGE_PARENT_INDEX]
40
+ return package_root / POLICY_LINT_SCRIPT_RELATIVE_PATH
41
+
42
+
43
+ def run_staged_policy_lint() -> int:
44
+ """Run the staged linter with Git's environment and working directory.
45
+
46
+ Returns:
47
+ The linter status, or the infrastructure status when it cannot run.
48
+ """
49
+ try:
50
+ script_path = resolve_policy_lint_script_path()
51
+ if not script_path.is_file():
52
+ sys.stderr.write(POLICY_LINT_UNAVAILABLE_MESSAGE + "\n")
53
+ return POLICY_LINT_INFRASTRUCTURE_EXIT_CODE
54
+ completion = subprocess.run(
55
+ [sys.executable, str(script_path.resolve(strict=True)), POLICY_LINT_STAGED_ARGUMENT],
56
+ check=False,
57
+ timeout=POLICY_LINT_TIMEOUT_SECONDS,
58
+ )
59
+ except (OSError, subprocess.TimeoutExpired) as launch_error:
60
+ sys.stderr.write(POLICY_LINT_FAILED_MESSAGE.format(error=launch_error) + "\n")
61
+ return POLICY_LINT_INFRASTRUCTURE_EXIT_CODE
62
+ return completion.returncode
30
63
 
31
64
 
32
65
  def invoke_gate(gate_script_path: Path) -> int:
@@ -50,6 +83,14 @@ def invoke_gate(gate_script_path: Path) -> int:
50
83
 
51
84
 
52
85
  def main() -> int:
86
+ """Run local staged policy lint before the existing native commit checks.
87
+
88
+ Returns:
89
+ The first failing check's status, or zero when the checks pass.
90
+ """
91
+ lint_exit_code = run_staged_policy_lint()
92
+ if lint_exit_code != 0:
93
+ return lint_exit_code
53
94
  gate_script_not_found_message = GATE_SCRIPT_NOT_FOUND_MESSAGE
54
95
  gate_script_path, exact_allowed_path = resolve_gate_script_path()
55
96
  if not is_safe_regular_file(gate_script_path, exact_allowed_path):
@@ -0,0 +1,159 @@
1
+ """Prove retirement and local enforcement through an actual disposable install."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import subprocess
9
+ import sys
10
+ from pathlib import Path
11
+
12
+ import pytest
13
+ from test_pre_commit import ALL_RULE_FIXTURES, _git, _stage
14
+ from test_pre_commit import repository_root as repository_root
15
+
16
+ ALL_RETIRED_HOOK_PATHS = (
17
+ "blocking/code_rules_enforcer.py",
18
+ "blocking/tdd_enforcer.py",
19
+ "blocking/windows_rmtree_blocker.py",
20
+ "blocking/state_description_blocker.py",
21
+ "blocking/subprocess_budget_completeness.py",
22
+ "blocking/hook_prose_detector_consistency.py",
23
+ "blocking/workflow_substitution_slot_blocker.py",
24
+ )
25
+ PACKAGE_ROOT = Path(__file__).resolve().parents[2]
26
+
27
+
28
+ def _legacy_settings_bytes(managed_root: Path, foreign_command: str) -> bytes:
29
+ original_settings = {
30
+ "permissions": {"allow": ["Read"]},
31
+ "hooks": {"PreToolUse": [{
32
+ "matcher": "Write|Edit|MultiEdit|apply_patch",
33
+ "hooks": [
34
+ {"type": "command", "command": foreign_command, "timeout": 17},
35
+ *({
36
+ "type": "command",
37
+ "command": f"python {managed_root.as_posix()}/hooks/{each_path}",
38
+ "timeout": 10,
39
+ } for each_path in ALL_RETIRED_HOOK_PATHS),
40
+ ],
41
+ }]},
42
+ }
43
+ return json.dumps(original_settings, indent=2).encode("utf-8")
44
+
45
+
46
+ def _install_environment(home: Path) -> dict[str, str]:
47
+ environment = {
48
+ each_name: each_value for each_name, each_value in os.environ.items()
49
+ if not each_name.startswith("GIT_")
50
+ }
51
+ environment.update({
52
+ "HOME": str(home),
53
+ "USERPROFILE": str(home),
54
+ "CLAUDE_CONFIG_DIR": str(home / ".claude"),
55
+ "CODEX_HOME": str(home / ".codex"),
56
+ "GIT_CONFIG_GLOBAL": str(home / "gitconfig"),
57
+ "GIT_CONFIG_NOSYSTEM": "1",
58
+ })
59
+ return environment
60
+
61
+
62
+ def _install_twice(home: Path, settings_path: Path) -> bytes:
63
+ all_settings_bytes: list[bytes] = []
64
+ for each_run in range(2):
65
+ completed = subprocess.run(
66
+ ["node", str(PACKAGE_ROOT / "bin/install.mjs"), "--only", "core"],
67
+ cwd=PACKAGE_ROOT, env=_install_environment(home),
68
+ capture_output=True, text=True, check=False, timeout=180,
69
+ )
70
+ (home / f"install-{each_run}.log").write_text(
71
+ completed.stdout + completed.stderr, encoding="utf-8"
72
+ )
73
+ assert completed.returncode == 0, completed.stdout + completed.stderr
74
+ all_settings_bytes.append(settings_path.read_bytes())
75
+ assert all_settings_bytes[0] == all_settings_bytes[1]
76
+ return all_settings_bytes[-1]
77
+
78
+
79
+ def _assert_retired_registrations(managed_root: Path, settings_bytes: bytes, foreign_command: str) -> None:
80
+ settings = json.loads(settings_bytes)
81
+ all_commands = [
82
+ each_hook["command"]
83
+ for each_groups in settings["hooks"].values()
84
+ for each_group in each_groups
85
+ for each_hook in each_group["hooks"]
86
+ ]
87
+ assert all_commands.count(foreign_command) == 1
88
+ assert "Read" in settings["permissions"]["allow"]
89
+ for each_relative_path in ALL_RETIRED_HOOK_PATHS:
90
+ assert not any(each_relative_path in each_command for each_command in all_commands)
91
+ source = PACKAGE_ROOT / "hooks" / each_relative_path
92
+ installed = managed_root / "hooks" / each_relative_path
93
+ assert hashlib.sha256(installed.read_bytes()).digest() == hashlib.sha256(source.read_bytes()).digest()
94
+
95
+
96
+ @pytest.fixture(scope="module")
97
+ def managed_installation(tmp_path_factory: pytest.TempPathFactory) -> Path:
98
+ """Install twice into a private home while retaining exact prior settings."""
99
+ home = tmp_path_factory.mktemp("installed-policy")
100
+ managed_root = home / ".claude"
101
+ managed_root.mkdir()
102
+ foreign_command = "python /unmanaged/fixture_notice.py"
103
+ original_bytes = _legacy_settings_bytes(managed_root, foreign_command)
104
+ settings_path = managed_root / "settings.json"
105
+ settings_path.write_bytes(original_bytes)
106
+ backup_directory = home / "before-cutover"
107
+ backup_directory.mkdir()
108
+ (backup_directory / "settings.json").write_bytes(original_bytes)
109
+ installed_settings = _install_twice(home, settings_path)
110
+ _assert_retired_registrations(managed_root, installed_settings, foreign_command)
111
+ assert (backup_directory / "settings.json").read_bytes() == original_bytes
112
+ hook_path = _git(home, "config", "--file", str(home / "gitconfig"), "--get", "core.hooksPath")
113
+ assert Path(hook_path).resolve() == (managed_root / "hooks/git-hooks").resolve()
114
+ return managed_root
115
+
116
+
117
+ @pytest.mark.parametrize(("rule_id", "relative_path", "content"), ALL_RULE_FIXTURES)
118
+ def test_installed_native_commit_catches_each_retired_file_rule(
119
+ managed_installation: Path,
120
+ repository_root: Path,
121
+ monkeypatch: pytest.MonkeyPatch,
122
+ rule_id: str,
123
+ relative_path: str,
124
+ content: str,
125
+ ) -> None:
126
+ _git(repository_root, "config", "core.hooksPath", str(managed_installation / "hooks/git-hooks"))
127
+ monkeypatch.delenv("CLAUDE_CONFIG_DIR", raising=False)
128
+ monkeypatch.setenv("CODE_RULES_GATE_PATH", str(repository_root / "missing-old-gate.py"))
129
+ before = _git(repository_root, "rev-parse", "HEAD")
130
+ _stage(repository_root, relative_path, content)
131
+ completed = subprocess.run(
132
+ ["git", "commit", "-m", "invalid fixture"],
133
+ cwd=repository_root, capture_output=True, text=True, check=False, timeout=240,
134
+ )
135
+ assert completed.returncode != 0
136
+ assert rule_id in completed.stdout + completed.stderr
137
+ assert _git(repository_root, "rev-parse", "HEAD") == before
138
+
139
+
140
+ def test_installed_linter_and_native_commit_allow_valid_changes(
141
+ managed_installation: Path,
142
+ repository_root: Path,
143
+ monkeypatch: pytest.MonkeyPatch,
144
+ ) -> None:
145
+ _git(repository_root, "config", "core.hooksPath", str(managed_installation / "hooks/git-hooks"))
146
+ monkeypatch.delenv("CLAUDE_CONFIG_DIR", raising=False)
147
+ monkeypatch.setenv("CODE_RULES_GATE_PATH", str(repository_root / "missing-old-gate.py"))
148
+ _stage(repository_root, "docs/guide.md", "# Guide\n\nThe API uses port 8080.\n")
149
+ completed = subprocess.run(
150
+ [sys.executable, str(managed_installation / "scripts/cde_lint.py"), "--staged", "--format", "json"],
151
+ cwd=repository_root, capture_output=True, text=True, check=False, timeout=240,
152
+ )
153
+ assert completed.returncode == 0, completed.stdout + completed.stderr
154
+ report = json.loads(completed.stdout)
155
+ assert report["failed_rules"] == []
156
+ assert "terminology-sweep" in report["executed_rules"]
157
+ before = _git(repository_root, "rev-parse", "HEAD")
158
+ _git(repository_root, "commit", "-m", "valid installed fixture")
159
+ assert _git(repository_root, "rev-parse", "HEAD") != before