claude-dev-env 2.12.0 → 2.14.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 (399) hide show
  1. package/AGENTS.md +61 -0
  2. package/CLAUDE.md +1 -57
  3. package/_shared/AGENTS.md +15 -0
  4. package/_shared/CLAUDE.md +1 -15
  5. package/_shared/advisor/AGENTS.md +22 -0
  6. package/_shared/advisor/CLAUDE.md +1 -22
  7. package/_shared/pr-loop/AGENTS.md +27 -0
  8. package/_shared/pr-loop/CLAUDE.md +1 -27
  9. package/_shared/pr-loop/scripts/AGENTS.md +35 -0
  10. package/_shared/pr-loop/scripts/CLAUDE.md +1 -35
  11. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
  12. package/_shared/pr-loop/scripts/code_rules_gate_parts/AGENTS.md +42 -0
  13. package/_shared/pr-loop/scripts/code_rules_gate_parts/CLAUDE.md +1 -42
  14. package/_shared/pr-loop/scripts/pr_loop_shared_constants/AGENTS.md +26 -0
  15. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -26
  16. package/_shared/pr-loop/scripts/test_code_rules_gate.py +0 -26
  17. package/_shared/pr-loop/scripts/tests/AGENTS.md +44 -0
  18. package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -44
  19. package/_shared/process-tree/AGENTS.md +41 -0
  20. package/_shared/process-tree/CLAUDE.md +1 -41
  21. package/agents/AGENTS.md +31 -0
  22. package/agents/CLAUDE.md +1 -32
  23. package/agents/clean-coder.md +6 -6
  24. package/agents/pr-description-writer.md +11 -81
  25. package/agents/test_agent_frontmatter.py +4 -4
  26. package/audit-rubrics/AGENTS.md +43 -0
  27. package/audit-rubrics/CLAUDE.md +1 -43
  28. package/audit-rubrics/category_rubrics/AGENTS.md +37 -0
  29. package/audit-rubrics/category_rubrics/CLAUDE.md +1 -37
  30. package/audit-rubrics/prompts/AGENTS.md +37 -0
  31. package/audit-rubrics/prompts/CLAUDE.md +1 -37
  32. package/bin/AGENTS.md +112 -0
  33. package/bin/CLAUDE.md +1 -112
  34. package/bin/ever-shipped-skills.mjs +1 -0
  35. package/bin/install-constants.mjs +6 -7
  36. package/bin/install.mjs +71 -13
  37. package/bin/install.prune.test.mjs +26 -0
  38. package/bin/install.settings-defaults.test.mjs +60 -33
  39. package/bin/install.test.mjs +36 -4
  40. package/commands/AGENTS.md +23 -0
  41. package/commands/CLAUDE.md +1 -23
  42. package/commands/hook-log-extract.md +3 -3
  43. package/commands/sr-loop.md +9 -5
  44. package/docs/AGENTS.md +32 -0
  45. package/docs/CLAUDE.md +1 -33
  46. package/docs/CODE_RULES.md +1 -1
  47. package/docs/imagegen.md +22 -0
  48. package/docs/references/AGENTS.md +17 -0
  49. package/docs/references/CLAUDE.md +1 -18
  50. package/hooks/AGENTS.md +31 -0
  51. package/hooks/CLAUDE.md +1 -31
  52. package/hooks/_gh_pr_author_swap_utils.py +14 -0
  53. package/hooks/advisory/AGENTS.md +16 -0
  54. package/hooks/advisory/CLAUDE.md +1 -16
  55. package/hooks/blocking/AGENTS.md +123 -0
  56. package/hooks/blocking/CLAUDE.md +1 -148
  57. package/hooks/blocking/claude_md_orphan_file_blocker_parts/AGENTS.md +28 -0
  58. package/hooks/blocking/claude_md_orphan_file_blocker_parts/CLAUDE.md +1 -28
  59. package/hooks/blocking/code_rules_banned_identifiers.py +1 -1
  60. package/hooks/blocking/code_rules_dead_module_constant.py +149 -37
  61. package/hooks/blocking/code_rules_probe_chains.py +6 -2
  62. package/hooks/blocking/config/AGENTS.md +10 -0
  63. package/hooks/blocking/config/CLAUDE.md +1 -22
  64. package/hooks/blocking/conventional_pr_title_gate.py +1 -2
  65. package/hooks/blocking/gh_pr_author_restore.py +7 -2
  66. package/hooks/blocking/intent_only_ending_blocker.py +11 -16
  67. package/hooks/blocking/inventory_intent_records/AGENTS.md +26 -0
  68. package/hooks/blocking/inventory_intent_records/CLAUDE.md +1 -26
  69. package/hooks/blocking/package_inventory_stale_blocker_parts/AGENTS.md +26 -0
  70. package/hooks/blocking/package_inventory_stale_blocker_parts/CLAUDE.md +1 -26
  71. package/hooks/blocking/pii_prevention_blocker_parts/AGENTS.md +24 -0
  72. package/hooks/blocking/pii_prevention_blocker_parts/CLAUDE.md +1 -24
  73. package/hooks/blocking/send_user_file_open_locally_blocker.py +1 -1
  74. package/hooks/blocking/stop_dispatcher.py +4 -4
  75. package/hooks/blocking/tdd_enforcer_parts/AGENTS.md +30 -0
  76. package/hooks/blocking/tdd_enforcer_parts/CLAUDE.md +1 -30
  77. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -3
  78. package/hooks/blocking/test_code_rules_enforcer_banned_prefixes.py +1 -1
  79. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +171 -0
  80. package/hooks/blocking/test_code_rules_enforcer_magic_allowlist.py +1 -1
  81. package/hooks/blocking/test_gh_pr_author_restore.py +2 -1
  82. package/hooks/blocking/test_intent_only_ending_blocker.py +4 -0
  83. package/hooks/blocking/test_pre_tool_use_dispatcher.py +6 -7
  84. package/hooks/blocking/test_send_user_file_open_locally_blocker.py +4 -3
  85. package/hooks/blocking/test_shared_stdin_adoption.py +0 -2
  86. package/hooks/diagnostic/AGENTS.md +43 -0
  87. package/hooks/diagnostic/CLAUDE.md +1 -43
  88. package/hooks/diagnostic/hook_log_extractor.py +4 -38
  89. package/hooks/diagnostic/hook_log_stop_wrapper.py +6 -155
  90. package/hooks/diagnostic/migrations/AGENTS.md +16 -0
  91. package/hooks/diagnostic/migrations/CLAUDE.md +1 -16
  92. package/hooks/diagnostic/queries/AGENTS.md +19 -0
  93. package/hooks/diagnostic/queries/CLAUDE.md +1 -19
  94. package/hooks/diagnostic/test_hook_log_extractor.py +10 -23
  95. package/hooks/diagnostic/test_hook_log_stop_wrapper.py +3 -331
  96. package/hooks/git-hooks/AGENTS.md +31 -0
  97. package/hooks/git-hooks/CLAUDE.md +1 -31
  98. package/hooks/git-hooks/git_hooks_constants/AGENTS.md +21 -0
  99. package/hooks/git-hooks/git_hooks_constants/CLAUDE.md +1 -21
  100. package/hooks/git-hooks/git_hooks_constants/__init__.py +4 -12
  101. package/hooks/git-hooks/pre_push.py +49 -118
  102. package/hooks/git-hooks/test_pre_push.py +111 -137
  103. package/hooks/hooks.json +0 -35
  104. package/hooks/hooks_constants/AGENTS.md +104 -0
  105. package/hooks/hooks_constants/CLAUDE.md +1 -113
  106. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -11
  107. package/hooks/hooks_constants/convergence_branch_constants.py +0 -1
  108. package/hooks/hooks_constants/fable_spawn_gate_constants.py +3 -4
  109. package/hooks/hooks_constants/hook_log_extractor_constants.py +1 -2
  110. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -8
  111. package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +1 -6
  112. package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -1
  113. package/hooks/hooks_constants/task_list_loop_starter_constants.py +4 -3
  114. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -26
  115. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +0 -5
  116. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -1
  117. package/hooks/hooks_constants/working_style_prompt_constants.py +29 -20
  118. package/hooks/lifecycle/AGENTS.md +18 -0
  119. package/hooks/lifecycle/CLAUDE.md +1 -20
  120. package/hooks/observability/AGENTS.md +20 -0
  121. package/hooks/observability/CLAUDE.md +1 -20
  122. package/hooks/session/AGENTS.md +33 -0
  123. package/hooks/session/CLAUDE.md +1 -33
  124. package/hooks/session/gh_pr_author_session_cleanup.py +7 -2
  125. package/hooks/session/task_list_loop_starter.py +3 -3
  126. package/hooks/session/test_gh_pr_author_session_cleanup.py +2 -1
  127. package/hooks/session/test_task_list_loop_starter.py +10 -2
  128. package/hooks/session/test_working_style_prompt.py +9 -4
  129. package/hooks/validation/AGENTS.md +20 -0
  130. package/hooks/validation/CLAUDE.md +1 -20
  131. package/hooks/validators/AGENTS.md +51 -0
  132. package/hooks/validators/CLAUDE.md +1 -51
  133. package/hooks/validators/health_check.py +52 -19
  134. package/hooks/validators/python_style_checks.py +1 -1
  135. package/hooks/validators/test_health_check.py +112 -27
  136. package/hooks/workflow/AGENTS.md +16 -0
  137. package/hooks/workflow/CLAUDE.md +1 -16
  138. package/installable-surfaces.manifest.json +1 -0
  139. package/output-styles/AGENTS.md +15 -0
  140. package/output-styles/CLAUDE.md +1 -15
  141. package/package.json +2 -1
  142. package/rules/AGENTS.md +61 -0
  143. package/rules/CLAUDE.md +1 -63
  144. package/rules/code-standards.md +5 -9
  145. package/rules/doc-prose-cuts.md +1 -1
  146. package/rules/gh-cli-conventions.md +0 -1
  147. package/rules/git-workflow.md +1 -3
  148. package/scripts/AGENTS.md +62 -0
  149. package/scripts/CLAUDE.md +1 -63
  150. package/scripts/_code_review_test_support.py +0 -95
  151. package/scripts/codex_compat_materializer.py +4 -0
  152. package/scripts/dev_env_scripts_constants/AGENTS.md +21 -0
  153. package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -21
  154. package/scripts/dev_env_scripts_constants/code_review_constants.py +9 -98
  155. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +1 -44
  156. package/scripts/invoke_code_review.py +8 -451
  157. package/scripts/sync_to_cursor/AGENTS.md +23 -0
  158. package/scripts/sync_to_cursor/CLAUDE.md +1 -23
  159. package/scripts/test_invoke_code_review.py +5 -61
  160. package/scripts/test_invoke_code_review_cli.py +1 -45
  161. package/scripts/test_invoke_code_review_contract.py +1 -118
  162. package/scripts/test_validate_instruction_pairs.py +90 -0
  163. package/scripts/test_verify_installable_package.py +5 -1
  164. package/scripts/tests/AGENTS.md +33 -0
  165. package/scripts/tests/CLAUDE.md +1 -33
  166. package/scripts/tests/test_codex_compat_materializer.py +36 -1
  167. package/scripts/validate_instruction_pairs.py +183 -0
  168. package/settings.json +1 -10
  169. package/skills/AGENTS.md +72 -0
  170. package/skills/CLAUDE.md +1 -64
  171. package/skills/_shared/AGENTS.md +44 -0
  172. package/skills/_shared/CLAUDE.md +1 -44
  173. package/skills/_shared/advisor/AGENTS.md +9 -0
  174. package/skills/_shared/advisor/CLAUDE.md +1 -9
  175. package/skills/_shared/pr-loop/AGENTS.md +50 -0
  176. package/skills/_shared/pr-loop/CLAUDE.md +1 -50
  177. package/skills/_shared/pr-loop/prompts/AGENTS.md +9 -0
  178. package/skills/_shared/pr-loop/prompts/CLAUDE.md +1 -9
  179. package/skills/_shared/pr-loop/scripts/AGENTS.md +35 -0
  180. package/skills/_shared/pr-loop/scripts/CLAUDE.md +1 -35
  181. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/AGENTS.md +24 -0
  182. package/skills/_shared/pr-loop/scripts/skills_pr_loop_constants/CLAUDE.md +1 -24
  183. package/skills/anthropic-plan/AGENTS.md +34 -0
  184. package/skills/anthropic-plan/CLAUDE.md +1 -34
  185. package/skills/anthropic-plan/scripts/AGENTS.md +11 -0
  186. package/skills/anthropic-plan/scripts/CLAUDE.md +1 -11
  187. package/skills/anthropic-plan/scripts/anthropic_plan_scripts_constants/AGENTS.md +16 -0
  188. package/skills/anthropic-plan/scripts/anthropic_plan_scripts_constants/CLAUDE.md +1 -16
  189. package/skills/anthropic-plan/templates/AGENTS.md +13 -0
  190. package/skills/anthropic-plan/templates/CLAUDE.md +1 -13
  191. package/skills/anthropic-plan/workflow/AGENTS.md +14 -0
  192. package/skills/anthropic-plan/workflow/CLAUDE.md +1 -14
  193. package/skills/auditing-claude-config/AGENTS.md +21 -0
  194. package/skills/auditing-claude-config/CLAUDE.md +1 -21
  195. package/skills/autoconverge/AGENTS.md +36 -0
  196. package/skills/autoconverge/CLAUDE.md +1 -36
  197. package/skills/autoconverge/reference/AGENTS.md +16 -0
  198. package/skills/autoconverge/reference/CLAUDE.md +1 -16
  199. package/skills/autoconverge/reference/convergence.md +7 -7
  200. package/skills/autoconverge/reference/gotchas.md +3 -10
  201. package/skills/autoconverge/workflow/AGENTS.md +26 -0
  202. package/skills/autoconverge/workflow/CLAUDE.md +1 -26
  203. package/skills/autoconverge/workflow/autoconverge_report_constants/AGENTS.md +16 -0
  204. package/skills/autoconverge/workflow/autoconverge_report_constants/CLAUDE.md +1 -16
  205. package/skills/autoconverge/workflow/converge.contract.test.mjs +53 -1388
  206. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +4 -3
  207. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +33 -493
  208. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +15 -97
  209. package/skills/autoconverge/workflow/converge.mjs +201 -238
  210. package/skills/autoconverge/workflow/converge.precatch.test.mjs +1 -1
  211. package/skills/bugteam/AGENTS.md +30 -0
  212. package/skills/bugteam/CLAUDE.md +1 -30
  213. package/skills/bugteam/CONSTRAINTS.md +1 -1
  214. package/skills/bugteam/SKILL.md +9 -0
  215. package/skills/bugteam/reference/AGENTS.md +20 -0
  216. package/skills/bugteam/reference/CLAUDE.md +1 -20
  217. package/skills/bugteam/reference/audit-and-teammates.md +10 -6
  218. package/skills/bugteam/reference/obstacles/AGENTS.md +24 -0
  219. package/skills/bugteam/reference/obstacles/CLAUDE.md +1 -24
  220. package/skills/bugteam/scripts/AGENTS.md +30 -0
  221. package/skills/bugteam/scripts/CLAUDE.md +1 -30
  222. package/skills/bugteam/scripts/bugteam_scripts_constants/AGENTS.md +18 -0
  223. package/skills/bugteam/scripts/bugteam_scripts_constants/CLAUDE.md +1 -18
  224. package/skills/bugteam/test_skill_additions.py +9 -0
  225. package/skills/closeout/reference/handoff-prompt-template.md +1 -1
  226. package/skills/codex-review/AGENTS.md +46 -0
  227. package/skills/codex-review/CLAUDE.md +1 -46
  228. package/skills/codex-review/reference/AGENTS.md +15 -0
  229. package/skills/codex-review/reference/CLAUDE.md +1 -15
  230. package/skills/codex-review/scripts/codex_review_scripts_constants/AGENTS.md +18 -0
  231. package/skills/codex-review/scripts/codex_review_scripts_constants/CLAUDE.md +1 -18
  232. package/skills/codex-review/test_skill_scaffold.py +3 -3
  233. package/skills/comments/SKILL.md +65 -0
  234. package/skills/copilot-review/AGENTS.md +18 -0
  235. package/skills/copilot-review/CLAUDE.md +1 -18
  236. package/skills/descriptions/SKILL.md +51 -0
  237. package/skills/emergencies/SKILL.md +42 -0
  238. package/skills/everything-search/AGENTS.md +17 -0
  239. package/skills/everything-search/CLAUDE.md +1 -17
  240. package/skills/fresh-branch/AGENTS.md +14 -0
  241. package/skills/fresh-branch/CLAUDE.md +1 -14
  242. package/skills/grok-spawn/AGENTS.md +28 -0
  243. package/skills/grok-spawn/CLAUDE.md +1 -28
  244. package/skills/orchestrator/SKILL.md +2 -4
  245. package/skills/plan-to-pr/SKILL.md +6 -7
  246. package/skills/plan-to-pr/reference/final-validation-tasks.md +1 -1
  247. package/skills/plan-to-pr/reference/process-inventory.md +2 -2
  248. package/skills/plan-to-pr/reference/review-loop.md +2 -2
  249. package/skills/plan-to-pr/reference/run-record.schema.json +1 -1
  250. package/skills/plan-to-pr/reference/task-seeds.md +1 -1
  251. package/skills/plan-to-pr/reference/task-ticket.md +3 -4
  252. package/skills/plan-to-pr/scripts/config/constants.py +1 -2
  253. package/skills/plan-to-pr/scripts/test_validate_protocol.py +1 -1
  254. package/skills/plan-to-pr/test_skill_contract.py +2 -2
  255. package/skills/pr-converge/AGENTS.md +32 -0
  256. package/skills/pr-converge/CLAUDE.md +1 -32
  257. package/skills/pr-converge/SKILL.md +13 -16
  258. package/skills/pr-converge/pr_converge_skill_constants/AGENTS.md +26 -0
  259. package/skills/pr-converge/pr_converge_skill_constants/CLAUDE.md +1 -26
  260. package/skills/pr-converge/reference/AGENTS.md +28 -0
  261. package/skills/pr-converge/reference/CLAUDE.md +1 -28
  262. package/skills/pr-converge/reference/obstacles/AGENTS.md +23 -0
  263. package/skills/pr-converge/reference/obstacles/CLAUDE.md +1 -23
  264. package/skills/pr-converge/reference/progress-checklist.md +6 -5
  265. package/skills/pr-converge/reference/state-schema.md +1 -27
  266. package/skills/pr-converge/scripts/AGENTS.md +46 -0
  267. package/skills/pr-converge/scripts/CLAUDE.md +1 -46
  268. package/skills/pr-converge/scripts/pr_converge_scripts_constants/AGENTS.md +18 -0
  269. package/skills/pr-converge/scripts/pr_converge_scripts_constants/CLAUDE.md +1 -18
  270. package/skills/pr-converge/workflows/AGENTS.md +16 -0
  271. package/skills/pr-converge/workflows/CLAUDE.md +1 -16
  272. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  273. package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +3 -3
  274. package/skills/pr-loop-lifecycle/SKILL.md +1 -1
  275. package/skills/pr-loop-lifecycle/reference/teardown-publish-permissions.md +1 -1
  276. package/skills/prototype/SKILL.md +4 -4
  277. package/skills/prototype/reference/honest-limitations.md +5 -5
  278. package/skills/prototype/reference/promotion-tasks.md +2 -2
  279. package/skills/prototype/workflows/promotion.md +4 -6
  280. package/skills/rebase/AGENTS.md +32 -0
  281. package/skills/rebase/CLAUDE.md +1 -32
  282. package/skills/recall/AGENTS.md +30 -0
  283. package/skills/recall/CLAUDE.md +1 -30
  284. package/skills/remember/AGENTS.md +31 -0
  285. package/skills/remember/CLAUDE.md +1 -31
  286. package/skills/reviews/SKILL.md +85 -0
  287. package/skills/session-log/AGENTS.md +32 -0
  288. package/skills/session-log/CLAUDE.md +1 -32
  289. package/skills/session-tidy/AGENTS.md +36 -0
  290. package/skills/session-tidy/CLAUDE.md +1 -36
  291. package/skills/skill-builder/AGENTS.md +49 -0
  292. package/skills/skill-builder/CLAUDE.md +1 -49
  293. package/skills/skill-builder/references/AGENTS.md +25 -0
  294. package/skills/skill-builder/references/CLAUDE.md +1 -25
  295. package/skills/skill-builder/references/deterministic-elements.md +1 -1
  296. package/skills/skill-builder/templates/AGENTS.md +14 -0
  297. package/skills/skill-builder/templates/CLAUDE.md +1 -14
  298. package/skills/skill-builder/workflows/AGENTS.md +19 -0
  299. package/skills/skill-builder/workflows/CLAUDE.md +1 -19
  300. package/skills/small-cl/SKILL.md +52 -0
  301. package/skills/task-build/AGENTS.md +29 -0
  302. package/skills/task-build/CLAUDE.md +1 -29
  303. package/skills/update/AGENTS.md +38 -0
  304. package/skills/update/CLAUDE.md +1 -38
  305. package/system-prompts/AGENTS.md +17 -0
  306. package/system-prompts/CLAUDE.md +1 -17
  307. package/system-prompts/software-engineer.xml +3 -3
  308. package/agents/code-verifier.md +0 -74
  309. package/docs/PR_DESCRIPTION_GUIDE.md +0 -157
  310. package/docs/references/code-review-enforcement.md +0 -132
  311. package/hooks/blocking/code_review_enforcement_config_bootstrap.py +0 -53
  312. package/hooks/blocking/code_review_gate_deny.py +0 -74
  313. package/hooks/blocking/code_review_pr_create_gate.py +0 -198
  314. package/hooks/blocking/code_review_push_gate.py +0 -145
  315. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +0 -348
  316. package/hooks/blocking/code_review_stamp_store.py +0 -233
  317. package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +0 -7
  318. package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +0 -15
  319. package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +0 -212
  320. package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +0 -138
  321. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +0 -49
  322. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +0 -38
  323. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +0 -483
  324. package/hooks/blocking/config/code_review_enforcement_constants.py +0 -143
  325. package/hooks/blocking/config/test_code_review_enforcement_constants.py +0 -166
  326. package/hooks/blocking/config/verified_commit_constants.py +0 -160
  327. package/hooks/blocking/config/verified_commit_context_constants.py +0 -21
  328. package/hooks/blocking/config/verified_commit_gate_output_constants.py +0 -14
  329. package/hooks/blocking/conftest.py +0 -123
  330. package/hooks/blocking/convergence_gate_blocker.py +0 -310
  331. package/hooks/blocking/pr_converge_bugteam_enforcer.py +0 -170
  332. package/hooks/blocking/pr_description_body_audit.py +0 -148
  333. package/hooks/blocking/pr_description_command_parser.py +0 -237
  334. package/hooks/blocking/pr_description_enforcer.py +0 -227
  335. package/hooks/blocking/pr_description_pr_number.py +0 -155
  336. package/hooks/blocking/pr_description_proof_of_work.py +0 -385
  337. package/hooks/blocking/pr_description_readability.py +0 -364
  338. package/hooks/blocking/reviewer_spawn_gate.py +0 -182
  339. package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +0 -62
  340. package/hooks/blocking/test_code_review_gate_deny.py +0 -54
  341. package/hooks/blocking/test_code_review_pr_create_gate.py +0 -199
  342. package/hooks/blocking/test_code_review_push_gate.py +0 -205
  343. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +0 -199
  344. package/hooks/blocking/test_code_review_stamp_store.py +0 -205
  345. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +0 -710
  346. package/hooks/blocking/test_code_verifier_tools_contract.py +0 -28
  347. package/hooks/blocking/test_convergence_gate_blocker.py +0 -428
  348. package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +0 -311
  349. package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +0 -184
  350. package/hooks/blocking/test_pr_description_enforcer.py +0 -221
  351. package/hooks/blocking/test_pr_description_enforcer_body_audit.py +0 -247
  352. package/hooks/blocking/test_pr_description_enforcer_body_rules.py +0 -493
  353. package/hooks/blocking/test_pr_description_enforcer_command_parser.py +0 -366
  354. package/hooks/blocking/test_pr_description_enforcer_pr_number.py +0 -159
  355. package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +0 -175
  356. package/hooks/blocking/test_pr_description_enforcer_readability.py +0 -443
  357. package/hooks/blocking/test_pr_description_proof_of_work.py +0 -313
  358. package/hooks/blocking/test_reviewer_spawn_gate.py +0 -230
  359. package/hooks/blocking/test_verdict_directory_write_blocker.py +0 -804
  360. package/hooks/blocking/test_verification_verdict_store.py +0 -974
  361. package/hooks/blocking/test_verified_commit_config_bootstrap.py +0 -67
  362. package/hooks/blocking/test_verified_commit_docs_delta.py +0 -176
  363. package/hooks/blocking/test_verified_commit_gate.py +0 -581
  364. package/hooks/blocking/test_verified_commit_gate_additional_context.py +0 -134
  365. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +0 -131
  366. package/hooks/blocking/test_verifier_verdict_minter.py +0 -299
  367. package/hooks/blocking/tests/test_verified_commit_gate.py +0 -41
  368. package/hooks/blocking/verdict_directory_write_blocker.py +0 -687
  369. package/hooks/blocking/verification_verdict_store.py +0 -1039
  370. package/hooks/blocking/verified_commit_config_bootstrap.py +0 -63
  371. package/hooks/blocking/verified_commit_gate.py +0 -113
  372. package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +0 -28
  373. package/hooks/blocking/verified_commit_gate_parts/__init__.py +0 -1
  374. package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +0 -174
  375. package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +0 -53
  376. package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +0 -80
  377. package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +0 -170
  378. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +0 -217
  379. package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +0 -10
  380. package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +0 -94
  381. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +0 -17
  382. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +0 -38
  383. package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +0 -71
  384. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +0 -96
  385. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +0 -167
  386. package/hooks/blocking/verifier_verdict_minter.py +0 -280
  387. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +0 -64
  388. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +0 -53
  389. package/hooks/hooks_constants/pr_converge_bugteam_enforcer_constants.py +0 -55
  390. package/hooks/hooks_constants/pr_converge_bugteam_enforcer_state.py +0 -67
  391. package/hooks/hooks_constants/pr_description_enforcer_constants.py +0 -167
  392. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -107
  393. package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +0 -41
  394. package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +0 -292
  395. package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +0 -198
  396. package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +0 -283
  397. package/rules/proof-of-work-pr-comments.md +0 -22
  398. package/rules/verified-commit-gate-skip.md +0 -4
  399. package/scripts/Show-Asset.ps1 +0 -106
package/bin/AGENTS.md ADDED
@@ -0,0 +1,112 @@
1
+ # bin
2
+
3
+ The installer and its companion modules. Running `npx claude-dev-env` (or `node bin/install.mjs`) copies package files into the managed root (`~/.claude/` by default; `CLAUDE_CONFIG_DIR` or `--target` selects another), merges hook entries into that root's `settings.json`, installs Git hooks, and writes `~/.mypy.ini` under the process home.
4
+
5
+ ## Files
6
+
7
+ | File | Purpose |
8
+ |---|---|
9
+ | `install.mjs` | Main installer: builds a read-only plan via `install-plan.mjs`, then runs mutations inside `install-transaction.mjs` recovery (copy content directories, merge hooks, install skills, prune, git hooks, mypy.ini); routes `CLAUDE_HOME`, the manifest path, and `~/.mypy.ini` through `resolve-install-root.mjs`; resolves single or multi-profile targets before mutation and writes one ownership manifest per target |
10
+ | `resolve-install-root.mjs` | Pure install-root resolver: precedence `--target` > `CLAUDE_CONFIG_DIR` > `~/.claude`, separator-boundary containment, and the declared external allowlist for `~/.mypy.ini` |
11
+ | `select-install-targets.mjs` | Pure target selection for main-default, explicit `--target`, and `--profile`/`--profiles`; rejects ambiguous or duplicate targets; builds per-target manifest records with `targetIdentity` and `managedRoot` |
12
+ | `install-plan.mjs` | Read-only install and uninstall plans: install preflight (managed root, source conflicts, Python, settings when hooks install) and uninstall preflight (settings JSON before removal, removable vs skipped manifest records), freezes plans E2/F execute |
13
+ | `install-transaction.mjs` | Install, update, and uninstall transaction journal: captures prior settings, manifest, managed files, and `core.hooksPath`, restores them on failure, and supports fault injection phases for recovery tests |
14
+ | `install.transaction.test.mjs` | Unit and sandbox installer tests for snapshot/restore and fault phases (`after_file_staging`, `after_settings_write`, `after_git_config`, `after_manifest_write`) |
15
+ | `install.uninstall-transaction.test.mjs` | Uninstall plan preflight and recovery: malformed/non-object settings fail before removal, each fault phase restores files/settings/manifest/`core.hooksPath`, retry succeeds, selected-root containment |
16
+ | `install.profile-root.test.mjs` | Contract tests for the install-root resolver: precedence, containment boundary, external allowlist, and the install.mjs import smoke check |
17
+ | `install-constants.mjs` | The named values `install.mjs` reads: `SKIPPED_SOURCE_ENTRY_NAMES` and `SKIPPED_SOURCE_FILE_EXTENSIONS` for the build artifacts the source walk leaves behind, `RUN_BACKUP_DIRECTORY_NAME_PATTERN` for the timestamp shape a run backup directory carries, `MANAGED_SKILLS_DIRECTORY_NAME` and `MANAGED_HOOKS_DIRECTORY_NAME` for the directory name each of those trees carries in a package source and under `~/.claude` — read by the copy loops, the hooks.json reads, the git-hook shims, the mypy configuration, and the prunes alike — `SETTINGS_FILE_NAME` for the settings file the merge, the retired-hook prune, and the uninstall purge share, and `MYPY_INI_FILE_NAME` for the home-directory file `install_mypy_ini.mjs` writes (also imported by `resolve-install-root.mjs`) |
18
+ | `ever-shipped-skills.mjs` | Static `EVER_SHIPPED_SKILL_NAMES` set of every top-level skill directory name the package has shipped; the installer subtracts the current skill set from it to prune retired skills left under `~/.claude/skills` |
19
+ | `expand_home_directory_tokens.mjs` | Expands residual `$HOME` / `${HOME}` / `~/` tokens in settings.json hook and statusLine commands to absolute home paths at install time (literal-safe for homes that contain `$`) |
20
+ | `git_hooks_installer.mjs` | Installs or updates the `pre-commit`, `pre-push`, and `post-commit` Git hooks in the user's git config; writes hook scripts that delegate to the installed Python hooks |
21
+ | `install_mypy_ini.mjs` | Writes `~/.mypy.ini` with settings that make mypy find the hooks package and enforce strict type checking |
22
+ | `install.test.mjs` | Unit tests for `install.mjs` — covers conflict detection, interpreter detection, settings merging, the settings shapes the installer never wrote — those every hook walk hands back untouched, and the shipped-event value the merge replaces with a warning — the source-artifact skip in `collectFiles`, the case-only rename decision and the `copyTree` copy that acts on it, the retired-hook diff and settings prune, the stale-file prune: the manifest diff, path-key case folding, emptied-parent cleanup, and the warn-and-keep paths, and backup retention: the sweep a moved-content run drives, the empty root a run whose moves failed gives up, and the populated root retention keeps |
23
+ | `install.profiles.test.mjs` | Profile target-selection and per-target ownership manifest contract tests (main-default, multi-profile, ambiguity/duplicate rejection, help text) |
24
+ | `install.plan.test.mjs` | Read-only plan and preflight tests: zero-write plan construction, source-conflict and missing-Python fail-closed, settings check only when hooks install, tolerant broken-manifest, invalid managed root, mutation-kind list for E2 |
25
+ | `install.prune.test.mjs` | End-to-end prune tests that run the real installer against a sandbox `HOME` — retired-skill, retired-hook, and stale-file moves into one timestamped backup, the settings entry a retired hook loses, the top-level paths every root's diff leaves alone, the manifest record a failed move keeps, the full-install and resolved-dependency gates, backup retention, and the uninstall: the `~/.mypy.ini` removal, the containment guard, and nested-directory cleanup |
26
+ | `git_hooks_installer.test.mjs` | Tests for `git_hooks_installer.mjs` |
27
+ | `install_mypy_ini.test.mjs` | Tests for `install_mypy_ini.mjs` |
28
+
29
+ ## Source build artifacts
30
+
31
+ `collectFiles` walks the package source and skips the artifacts a contributor's tooling writes beside it: the entry names `__pycache__`, `.ruff_cache`, `.pytest_cache`, `.mypy_cache`, `node_modules`, `.DS_Store`, and any file ending `.pyc` or `.pyo`. A skipped directory takes everything under it out of the walk. The `files` negations in `package.json` (`!**/__pycache__/**`, `!**/*.py[cod]`, the cache directories, `!**/*.log`, `!**/*.egg-info/**`) keep the same artifacts out of the published tarball, and `.npmignore` carries those patterns for tooling that reads it — keep the two in step. An `npx` install reads a clean tree; the walk covers a local `node bin/install.mjs` run against a working tree that holds the artifacts.
32
+
33
+ The skip and the cleanup of artifacts an earlier install copied are one code path. A `.pyc` a prior manifest records under any managed root sits outside the set the walk returns, so the next full install reads it as stale, moves it into that run's backup root, and drops it from the manifest the run writes.
34
+
35
+ ## Copying a file whose name changed letter case
36
+
37
+ `copyTree` renames a destination entry that differs from the shipped file name only in letter case to the shipped name, then copies. On a case-insensitive volume `copyFileSync` writes its bytes through whichever entry the filesystem resolves the path to, so a package shipping `README.md` over an installed `Readme.md` would fill the installed entry and leave the earlier spelling standing. The rename runs first because `renameSync` inside one directory is atomic: a run interrupted between the rename and the copy leaves the file present under the shipped name holding the earlier content, which the next install overwrites.
38
+
39
+ The decision reads the destination directory's entry names, cached one listing per directory for the whole copy run. On a case-sensitive volume the two names are two files, so the rename is skipped and each name keeps its own content. `caseOnlyRenameSourceName(shippedName, existingNames, options)` holds the decision, and `options.isCaseInsensitive` carries the platform answer as a value so a test drives either branch on a host of either kind.
40
+
41
+ ## Retired-skill prune
42
+
43
+ The full-install prune renames a retired skill directory into a timestamped backup rather than deleting it. Each pruned directory is renamed to `~/.claude/.claude-dev-env-pruned/<timestamp>/skills/<skill-name>/`, a backup root outside `~/.claude/skills` so a backed-up directory is never re-discovered as a skill. The `skills/` segment mirrors `~/.claude`, matching the layout the stale-file prune writes, so one recovery point reads as a copy of the tree it came from. One run shares one timestamped root, so a run leaves one recovery point. A rename that fails leaves the directory in place with a logged warning and never falls back to deletion, so a prune failure costs at most a cosmetic leftover.
44
+
45
+ Matching is by directory name alone, so a user-authored directory whose name collides with a retired skill is backed up as if it were that skill. A directory is pruned when the prior install's manifest recorded it or the ever-shipped set names it, and the current install did not just write it. A name absent from all three of those sets, together with `~/.claude/skills/_shared`, is left in place. Recovery of a wrongly-matched directory runs until the next pruning install, which keeps its own backup and retires the rest.
46
+
47
+ ## Stale-file prune
48
+
49
+ A full install also moves aside a file under a managed root that the run leaves unwritten. `copyTree` adds and overwrites but never removes, so every root the installer writes carries the same drift, and the prune covers all of them: `rules`, `docs`, `commands`, `agents`, `system-prompts`, `scripts`, `_shared`, `audit-rubrics`, `skills`, and `hooks` — the names in `MANAGED_TOP_LEVEL_DIRECTORY_NAMES`.
50
+
51
+ Nothing moves unless a prior install recorded it. That single rule is what makes covering ten roots as safe as covering one: the installer reads the file list from `~/.claude/.claude-dev-env-manifest.json`, subtracts every file the run copied across all source roots, and moves what remains.
52
+
53
+ The prune runs once per root, each call confined to its own root. Per-root iteration gives the containment guard and the emptied-parent walk the root that owns each file, and it settles `_shared`: `~/.claude/_shared` and `~/.claude/skills/_shared` are distinct absolute paths, so the `_shared` call and the `skills` call each see their own files and no path enters two diffs. Each root's content lands under `~/.claude/.claude-dev-env-pruned/<timestamp>/<root-name>/<relative>`, so the backup mirrors `~/.claude`. Every prune in a run shares that one timestamped root. A recorded path under no managed root — `~/.claude/CLAUDE.md`, `settings.json`, the manifest itself, and the `~/.mypy.ini` that sits in the home directory beside `~/.claude` — reaches no root's diff and stays where it is. The install summary reports the skills root's own count on the `skills:` line and the sum across roots on its own line.
54
+
55
+ The manifest diff limits the move to files the installer itself wrote. Runtime-generated content — a Python `__pycache__` entry, a ruff cache, a log — and any file a user authored under a managed root stay in place, because no install recorded them. Path comparison ignores letter case on Windows and macOS, so a package shipping `README.md` over an installed `Readme.md` keeps the bytes the run just wrote. A directory or a link standing where the manifest records a file is skipped with a warning, so the mover never renames a whole tree and never follows a link out of `~/.claude`. A directory emptied by a move is removed, walking up to the root the file sat under. A move that fails logs a warning and leaves the file in place, so a prune failure costs at most a stale file. The installer records each such path in the fresh manifest when the file is still on disk, so the file stays inside the next full install's diff and gets another attempt.
56
+
57
+ A missing or unreadable manifest, or one carrying no file list, holds the stale-file prune for that run: with no record of what an install wrote, the run has nothing to diff against.
58
+
59
+ A run writes both manifest keys — the file list and the skill-name list — wholesale from what it just installed only when the prunes ran that run and read the prior record all the way through, so the next diff reads as "the package stopped shipping this". Every other run unions what it wrote onto the prior lists: a scoped `--only` install, a full install holding its prunes behind an unresolved dependency group, and a run whose prune step ends early with a logged warning. The union keeps every entry a later prune needs to spot a stale file or a retired skill, and keeps `--uninstall` able to name the whole tree. The prune itself bounds the lists: a stale path leaves the record on the first full install that moves it aside.
60
+
61
+ ## Retired-hook entries in settings.json
62
+
63
+ A hook script under `~/.claude/hooks` carries a second reference: the `settings.json` entry that runs it. A full install removes that entry in the same run that moves the script aside, and removes it first — a `settings.json` naming a script that has left the hooks directory makes every session start invoke a missing file.
64
+
65
+ The retired set comes from the manifest diff alone: the hook files a prior install recorded that this run leaves unwritten, each taken relative to `~/.claude/hooks`. A script the run still writes stays out of the set, and a path no install of ours recorded never enters it, so a user-authored hook is out of reach of the prune. Each command is matched on the anchored `/.claude/hooks/<relative>` tail the merge uses to tell this installer's entries from a user's, so a command whose path is a retired tail plus a suffix (`retired_gate.py.bak`) names another file and stays.
66
+
67
+ The walk covers every event type the settings file holds rather than the ones the current `hooks.json` names, so an entry under an event type the package stopped shipping is reached too. A matcher group left empty is dropped, and an event type left empty goes with it. The file is written once, and only when an entry left it, so a run that retires no hook leaves `settings.json` byte-identical.
68
+
69
+ Every settings walk recognizes the shapes the installer writes and steps around the rest, so a hand-edited or third-party `settings.json` carries an install through rather than ending it. A matcher group carrying no `hooks` array and a hook entry whose `command` is not a string survive every walk untouched. An event type whose value is not an array of groups survives this prune and the uninstall purge; the merge replaces that value with the group list it ships for that event type, warning with the event type named so the user can recover the value from their own history. An event type the package ships no groups for keeps whatever value the file holds.
70
+
71
+ ## Backup retention
72
+
73
+ A run that moves content into `~/.claude/.claude-dev-env-pruned/<timestamp>/` then retires the other run backups, so the directory holds the one recovery point closest to what sits on disk. The retired-skill prune and the stale-file prune each report how many moves succeeded, and their sum is the signal the sweep answers to. The sweep removes a direct child whose name matches the installer's timestamp shape (`2026-07-25T18-04-11-923Z`), which leaves anything else under the pruned-backup directory in place, along with the directory itself. A removal that fails logs a warning and the sweep carries on, so retention never ends an install. The install output names the count when the sweep removes anything.
74
+
75
+ A run that moves nothing sweeps nothing, so every recovery point the user holds stays where it is. `moveIntoRunBackup` creates the directories leading to a backup path before it renames, so a run whose every move fails — the antivirus scanner or open editor case — leaves that timestamped root standing empty. Retention clears it with `rmdirSync` alone, depth first, so a directory holding anything survives every step.
76
+
77
+ ## Uninstall
78
+
79
+ `--uninstall` builds a read-only uninstall plan, captures a recovery snapshot, then removes each file the plan lists.
80
+
81
+ Settings JSON is validated before any removal. A malformed or non-object `settings.json` fails closed with the managed files still on disk. Each manifest record passes a containment guard: the path resolves under `~/.claude`, or it names the `~/.mypy.ini` the install writes in the home directory. Every other record is skipped with a warning and counted. Skipping keeps one malformed record — hand-edited, or written by an installer that ran against a different home — from stranding the user with a half-removed install. The purge removes every legitimate record, clears the manifest, and reports the skipped count.
82
+
83
+ The uninstall runs inside the same snapshot/restore journal as install: prior settings, manifest, managed files, and `core.hooksPath` restore when a later phase fails, so a retry starts from a complete ownership record. The journal is discarded only after a successful commit.
84
+
85
+ Removing a file leaves its directory a candidate for cleanup. Once the file loop ends, the purge walks up from each such directory to the managed top-level directory the file sits under (`MANAGED_TOP_LEVEL_DIRECTORY_NAMES`), removing each directory it finds empty. That reaches a nested tree such as `skills/<name>/scripts/`. A record under no managed root gets no walk, so `~/.claude` itself is never a stop root and a directory the installer never wrote stays. A separate pass drops each managed top-level directory the purge empties.
86
+
87
+ ## Prune gates
88
+
89
+ Every prune runs behind the same two gates: a full install, and every declared dependency group resolved. When any dependency group fails to resolve, all of them are skipped for the whole run with a logged notice naming the unresolved group. An unresolved dependency contributes no skills to the installed set, so a live skill that a dependency package supplies would look retired and its files would look stale; holding every prune until each dependency resolves keeps that skill's files in place and keeps their manifest records, so a run with every dependency resolved can still prune them.
90
+
91
+ ## Key exports from install.mjs
92
+
93
+ | Export | Description |
94
+ |---|---|
95
+ | `CONTENT_DIRECTORIES` | Array of package subdirectory names copied verbatim to `~/.claude/` |
96
+ | `MANAGED_TOP_LEVEL_DIRECTORY_NAMES` | The content directories plus `skills` and `hooks`; the stale-file prune walks it to give each root its own diff, and the uninstall purge reads it to find the root a recorded file belongs to |
97
+ | `collectFiles(directory)` | Lists every file under a source directory, skipping the build-artifact names and extensions in `install-constants.mjs` |
98
+ | `pythonCandidatesForPlatform(platform)` | Returns ordered Python interpreter candidates to probe; `py -3` first on Windows to avoid Microsoft Store alias issues |
99
+ | `isWindowsStorePythonStub(path)` | Returns true when the path resolves to the non-spawnable WindowsApps stub |
100
+ | `interpreterCommandFromPath(path)` | Formats an absolute interpreter path as a settings.json hook command prefix |
101
+ | `collectPackageSourceConflicts(dir)` | Returns any unmerged git conflicts in the package source; installer aborts when any exist |
102
+ | `pruneStaleInstalledFiles(priorFiles, currentFiles, destinationRoot, backupRoot, options)` | Moves each manifest-recorded file under the destination root that the run leaves unwritten into the run's backup root; returns `{ prunedCount, failedPaths }`. `options.isCaseInsensitive` drives path-key case folding, defaulting to this host's filesystem; `options.managedHomeDirectory` sets the home the containment guard tests against, defaulting to `~/.claude` |
103
+ | `copyTree(sourceBase, destBase, options)` | Copies every file under a source directory, renaming a destination entry that differs from the shipped name only in letter case to the shipped name first; returns `{ created, updated, paths }`. `options.isCaseInsensitive` drives that rename, defaulting to this host's filesystem |
104
+ | `caseOnlyRenameSourceName(shippedName, existingNames, options)` | Returns the existing directory entry a shipped file name would overwrite through a case-only spelling difference, or null; `options.isCaseInsensitive` defaults to this host's filesystem |
105
+ | `retiredManagedHookRelativePaths(priorFiles, currentFiles, hooksRoot)` | Returns the hook script paths a prior install recorded under the hooks root that this run leaves unwritten, each relative to that root |
106
+ | `pruneRetiredHookEntriesFromSettings(settingsPath, retiredPaths)` | Removes each settings.json entry running a retired managed hook script, writing the file only when an entry left it; returns the removed count |
107
+ | `retainNewestRunBackupOnly(runBackupRoot, didRunMoveContent)` | Retires every run backup sitting beside the run's own when `didRunMoveContent` holds; clears the run's empty root with `rmdirSync` when it does not |
108
+ | `comparisonKeyForPath(path, options)` | Builds the key two paths are compared through: resolved, forward-slashed, and lowercased when `options.isCaseInsensitive` holds — which defaults to true on Windows and macOS |
109
+
110
+ ## Install groups
111
+
112
+ `install.mjs` defines install groups (`core`, `journal`) plus any dependency groups discovered from `package.json` `dependencies`. The `core` group installs skills, all hooks, and the content directories. `journal` installs only its skill set.
package/bin/CLAUDE.md CHANGED
@@ -1,112 +1 @@
1
- # bin
2
-
3
- The installer and its companion modules. Running `npx claude-dev-env` (or `node bin/install.mjs`) copies package files into the managed root (`~/.claude/` by default; `CLAUDE_CONFIG_DIR` or `--target` selects another), merges hook entries into that root's `settings.json`, installs Git hooks, and writes `~/.mypy.ini` under the process home.
4
-
5
- ## Files
6
-
7
- | File | Purpose |
8
- |---|---|
9
- | `install.mjs` | Main installer: builds a read-only plan via `install-plan.mjs`, then runs mutations inside `install-transaction.mjs` recovery (copy content directories, merge hooks, install skills, prune, git hooks, mypy.ini); routes `CLAUDE_HOME`, the manifest path, and `~/.mypy.ini` through `resolve-install-root.mjs`; resolves single or multi-profile targets before mutation and writes one ownership manifest per target |
10
- | `resolve-install-root.mjs` | Pure install-root resolver: precedence `--target` > `CLAUDE_CONFIG_DIR` > `~/.claude`, separator-boundary containment, and the declared external allowlist for `~/.mypy.ini` |
11
- | `select-install-targets.mjs` | Pure target selection for main-default, explicit `--target`, and `--profile`/`--profiles`; rejects ambiguous or duplicate targets; builds per-target manifest records with `targetIdentity` and `managedRoot` |
12
- | `install-plan.mjs` | Read-only install and uninstall plans: install preflight (managed root, source conflicts, Python, settings when hooks install) and uninstall preflight (settings JSON before removal, removable vs skipped manifest records), freezes plans E2/F execute |
13
- | `install-transaction.mjs` | Install, update, and uninstall transaction journal: captures prior settings, manifest, managed files, and `core.hooksPath`, restores them on failure, and supports fault injection phases for recovery tests |
14
- | `install.transaction.test.mjs` | Unit and sandbox installer tests for snapshot/restore and fault phases (`after_file_staging`, `after_settings_write`, `after_git_config`, `after_manifest_write`) |
15
- | `install.uninstall-transaction.test.mjs` | Uninstall plan preflight and recovery: malformed/non-object settings fail before removal, each fault phase restores files/settings/manifest/`core.hooksPath`, retry succeeds, selected-root containment |
16
- | `install.profile-root.test.mjs` | Contract tests for the install-root resolver: precedence, containment boundary, external allowlist, and the install.mjs import smoke check |
17
- | `install-constants.mjs` | The named values `install.mjs` reads: `SKIPPED_SOURCE_ENTRY_NAMES` and `SKIPPED_SOURCE_FILE_EXTENSIONS` for the build artifacts the source walk leaves behind, `RUN_BACKUP_DIRECTORY_NAME_PATTERN` for the timestamp shape a run backup directory carries, `MANAGED_SKILLS_DIRECTORY_NAME` and `MANAGED_HOOKS_DIRECTORY_NAME` for the directory name each of those trees carries in a package source and under `~/.claude` — read by the copy loops, the hooks.json reads, the git-hook shims, the mypy configuration, and the prunes alike — `SETTINGS_FILE_NAME` for the settings file the merge, the retired-hook prune, and the uninstall purge share, and `MYPY_INI_FILE_NAME` for the home-directory file `install_mypy_ini.mjs` writes (also imported by `resolve-install-root.mjs`) |
18
- | `ever-shipped-skills.mjs` | Static `EVER_SHIPPED_SKILL_NAMES` set of every top-level skill directory name the package has shipped; the installer subtracts the current skill set from it to prune retired skills left under `~/.claude/skills` |
19
- | `expand_home_directory_tokens.mjs` | Expands residual `$HOME` / `${HOME}` / `~/` tokens in settings.json hook and statusLine commands to absolute home paths at install time (literal-safe for homes that contain `$`) |
20
- | `git_hooks_installer.mjs` | Installs or updates the `pre-commit`, `pre-push`, and `post-commit` Git hooks in the user's git config; writes hook scripts that delegate to the installed Python hooks |
21
- | `install_mypy_ini.mjs` | Writes `~/.mypy.ini` with settings that make mypy find the hooks package and enforce strict type checking |
22
- | `install.test.mjs` | Unit tests for `install.mjs` — covers conflict detection, interpreter detection, settings merging, the settings shapes the installer never wrote — those every hook walk hands back untouched, and the shipped-event value the merge replaces with a warning — the source-artifact skip in `collectFiles`, the case-only rename decision and the `copyTree` copy that acts on it, the retired-hook diff and settings prune, the stale-file prune: the manifest diff, path-key case folding, emptied-parent cleanup, and the warn-and-keep paths, and backup retention: the sweep a moved-content run drives, the empty root a run whose moves failed gives up, and the populated root retention keeps |
23
- | `install.profiles.test.mjs` | Profile target-selection and per-target ownership manifest contract tests (main-default, multi-profile, ambiguity/duplicate rejection, help text) |
24
- | `install.plan.test.mjs` | Read-only plan and preflight tests: zero-write plan construction, source-conflict and missing-Python fail-closed, settings check only when hooks install, tolerant broken-manifest, invalid managed root, mutation-kind list for E2 |
25
- | `install.prune.test.mjs` | End-to-end prune tests that run the real installer against a sandbox `HOME` — retired-skill, retired-hook, and stale-file moves into one timestamped backup, the settings entry a retired hook loses, the top-level paths every root's diff leaves alone, the manifest record a failed move keeps, the full-install and resolved-dependency gates, backup retention, and the uninstall: the `~/.mypy.ini` removal, the containment guard, and nested-directory cleanup |
26
- | `git_hooks_installer.test.mjs` | Tests for `git_hooks_installer.mjs` |
27
- | `install_mypy_ini.test.mjs` | Tests for `install_mypy_ini.mjs` |
28
-
29
- ## Source build artifacts
30
-
31
- `collectFiles` walks the package source and skips the artifacts a contributor's tooling writes beside it: the entry names `__pycache__`, `.ruff_cache`, `.pytest_cache`, `.mypy_cache`, `node_modules`, `.DS_Store`, and any file ending `.pyc` or `.pyo`. A skipped directory takes everything under it out of the walk. The `files` negations in `package.json` (`!**/__pycache__/**`, `!**/*.py[cod]`, the cache directories, `!**/*.log`, `!**/*.egg-info/**`) keep the same artifacts out of the published tarball, and `.npmignore` carries those patterns for tooling that reads it — keep the two in step. An `npx` install reads a clean tree; the walk covers a local `node bin/install.mjs` run against a working tree that holds the artifacts.
32
-
33
- The skip and the cleanup of artifacts an earlier install copied are one code path. A `.pyc` a prior manifest records under any managed root sits outside the set the walk returns, so the next full install reads it as stale, moves it into that run's backup root, and drops it from the manifest the run writes.
34
-
35
- ## Copying a file whose name changed letter case
36
-
37
- `copyTree` renames a destination entry that differs from the shipped file name only in letter case to the shipped name, then copies. On a case-insensitive volume `copyFileSync` writes its bytes through whichever entry the filesystem resolves the path to, so a package shipping `README.md` over an installed `Readme.md` would fill the installed entry and leave the earlier spelling standing. The rename runs first because `renameSync` inside one directory is atomic: a run interrupted between the rename and the copy leaves the file present under the shipped name holding the earlier content, which the next install overwrites.
38
-
39
- The decision reads the destination directory's entry names, cached one listing per directory for the whole copy run. On a case-sensitive volume the two names are two files, so the rename is skipped and each name keeps its own content. `caseOnlyRenameSourceName(shippedName, existingNames, options)` holds the decision, and `options.isCaseInsensitive` carries the platform answer as a value so a test drives either branch on a host of either kind.
40
-
41
- ## Retired-skill prune
42
-
43
- The full-install prune renames a retired skill directory into a timestamped backup rather than deleting it. Each pruned directory is renamed to `~/.claude/.claude-dev-env-pruned/<timestamp>/skills/<skill-name>/`, a backup root outside `~/.claude/skills` so a backed-up directory is never re-discovered as a skill. The `skills/` segment mirrors `~/.claude`, matching the layout the stale-file prune writes, so one recovery point reads as a copy of the tree it came from. One run shares one timestamped root, so a run leaves one recovery point. A rename that fails leaves the directory in place with a logged warning and never falls back to deletion, so a prune failure costs at most a cosmetic leftover.
44
-
45
- Matching is by directory name alone, so a user-authored directory whose name collides with a retired skill is backed up as if it were that skill. A directory is pruned when the prior install's manifest recorded it or the ever-shipped set names it, and the current install did not just write it. A name absent from all three of those sets, together with `~/.claude/skills/_shared`, is left in place. Recovery of a wrongly-matched directory runs until the next pruning install, which keeps its own backup and retires the rest.
46
-
47
- ## Stale-file prune
48
-
49
- A full install also moves aside a file under a managed root that the run leaves unwritten. `copyTree` adds and overwrites but never removes, so every root the installer writes carries the same drift, and the prune covers all of them: `rules`, `docs`, `commands`, `agents`, `system-prompts`, `scripts`, `_shared`, `audit-rubrics`, `skills`, and `hooks` — the names in `MANAGED_TOP_LEVEL_DIRECTORY_NAMES`.
50
-
51
- Nothing moves unless a prior install recorded it. That single rule is what makes covering ten roots as safe as covering one: the installer reads the file list from `~/.claude/.claude-dev-env-manifest.json`, subtracts every file the run copied across all source roots, and moves what remains.
52
-
53
- The prune runs once per root, each call confined to its own root. Per-root iteration gives the containment guard and the emptied-parent walk the root that owns each file, and it settles `_shared`: `~/.claude/_shared` and `~/.claude/skills/_shared` are distinct absolute paths, so the `_shared` call and the `skills` call each see their own files and no path enters two diffs. Each root's content lands under `~/.claude/.claude-dev-env-pruned/<timestamp>/<root-name>/<relative>`, so the backup mirrors `~/.claude`. Every prune in a run shares that one timestamped root. A recorded path under no managed root — `~/.claude/CLAUDE.md`, `settings.json`, the manifest itself, and the `~/.mypy.ini` that sits in the home directory beside `~/.claude` — reaches no root's diff and stays where it is. The install summary reports the skills root's own count on the `skills:` line and the sum across roots on its own line.
54
-
55
- The manifest diff limits the move to files the installer itself wrote. Runtime-generated content — a Python `__pycache__` entry, a ruff cache, a log — and any file a user authored under a managed root stay in place, because no install recorded them. Path comparison ignores letter case on Windows and macOS, so a package shipping `README.md` over an installed `Readme.md` keeps the bytes the run just wrote. A directory or a link standing where the manifest records a file is skipped with a warning, so the mover never renames a whole tree and never follows a link out of `~/.claude`. A directory emptied by a move is removed, walking up to the root the file sat under. A move that fails logs a warning and leaves the file in place, so a prune failure costs at most a stale file. The installer records each such path in the fresh manifest when the file is still on disk, so the file stays inside the next full install's diff and gets another attempt.
56
-
57
- A missing or unreadable manifest, or one carrying no file list, holds the stale-file prune for that run: with no record of what an install wrote, the run has nothing to diff against.
58
-
59
- A run writes both manifest keys — the file list and the skill-name list — wholesale from what it just installed only when the prunes ran that run and read the prior record all the way through, so the next diff reads as "the package stopped shipping this". Every other run unions what it wrote onto the prior lists: a scoped `--only` install, a full install holding its prunes behind an unresolved dependency group, and a run whose prune step ends early with a logged warning. The union keeps every entry a later prune needs to spot a stale file or a retired skill, and keeps `--uninstall` able to name the whole tree. The prune itself bounds the lists: a stale path leaves the record on the first full install that moves it aside.
60
-
61
- ## Retired-hook entries in settings.json
62
-
63
- A hook script under `~/.claude/hooks` carries a second reference: the `settings.json` entry that runs it. A full install removes that entry in the same run that moves the script aside, and removes it first — a `settings.json` naming a script that has left the hooks directory makes every session start invoke a missing file.
64
-
65
- The retired set comes from the manifest diff alone: the hook files a prior install recorded that this run leaves unwritten, each taken relative to `~/.claude/hooks`. A script the run still writes stays out of the set, and a path no install of ours recorded never enters it, so a user-authored hook is out of reach of the prune. Each command is matched on the anchored `/.claude/hooks/<relative>` tail the merge uses to tell this installer's entries from a user's, so a command whose path is a retired tail plus a suffix (`retired_gate.py.bak`) names another file and stays.
66
-
67
- The walk covers every event type the settings file holds rather than the ones the current `hooks.json` names, so an entry under an event type the package stopped shipping is reached too. A matcher group left empty is dropped, and an event type left empty goes with it. The file is written once, and only when an entry left it, so a run that retires no hook leaves `settings.json` byte-identical.
68
-
69
- Every settings walk recognizes the shapes the installer writes and steps around the rest, so a hand-edited or third-party `settings.json` carries an install through rather than ending it. A matcher group carrying no `hooks` array and a hook entry whose `command` is not a string survive every walk untouched. An event type whose value is not an array of groups survives this prune and the uninstall purge; the merge replaces that value with the group list it ships for that event type, warning with the event type named so the user can recover the value from their own history. An event type the package ships no groups for keeps whatever value the file holds.
70
-
71
- ## Backup retention
72
-
73
- A run that moves content into `~/.claude/.claude-dev-env-pruned/<timestamp>/` then retires the other run backups, so the directory holds the one recovery point closest to what sits on disk. The retired-skill prune and the stale-file prune each report how many moves succeeded, and their sum is the signal the sweep answers to. The sweep removes a direct child whose name matches the installer's timestamp shape (`2026-07-25T18-04-11-923Z`), which leaves anything else under the pruned-backup directory in place, along with the directory itself. A removal that fails logs a warning and the sweep carries on, so retention never ends an install. The install output names the count when the sweep removes anything.
74
-
75
- A run that moves nothing sweeps nothing, so every recovery point the user holds stays where it is. `moveIntoRunBackup` creates the directories leading to a backup path before it renames, so a run whose every move fails — the antivirus scanner or open editor case — leaves that timestamped root standing empty. Retention clears it with `rmdirSync` alone, depth first, so a directory holding anything survives every step.
76
-
77
- ## Uninstall
78
-
79
- `--uninstall` builds a read-only uninstall plan, captures a recovery snapshot, then removes each file the plan lists.
80
-
81
- Settings JSON is validated before any removal. A malformed or non-object `settings.json` fails closed with the managed files still on disk. Each manifest record passes a containment guard: the path resolves under `~/.claude`, or it names the `~/.mypy.ini` the install writes in the home directory. Every other record is skipped with a warning and counted. Skipping keeps one malformed record — hand-edited, or written by an installer that ran against a different home — from stranding the user with a half-removed install. The purge removes every legitimate record, clears the manifest, and reports the skipped count.
82
-
83
- The uninstall runs inside the same snapshot/restore journal as install: prior settings, manifest, managed files, and `core.hooksPath` restore when a later phase fails, so a retry starts from a complete ownership record. The journal is discarded only after a successful commit.
84
-
85
- Removing a file leaves its directory a candidate for cleanup. Once the file loop ends, the purge walks up from each such directory to the managed top-level directory the file sits under (`MANAGED_TOP_LEVEL_DIRECTORY_NAMES`), removing each directory it finds empty. That reaches a nested tree such as `skills/<name>/scripts/`. A record under no managed root gets no walk, so `~/.claude` itself is never a stop root and a directory the installer never wrote stays. A separate pass drops each managed top-level directory the purge empties.
86
-
87
- ## Prune gates
88
-
89
- Every prune runs behind the same two gates: a full install, and every declared dependency group resolved. When any dependency group fails to resolve, all of them are skipped for the whole run with a logged notice naming the unresolved group. An unresolved dependency contributes no skills to the installed set, so a live skill that a dependency package supplies would look retired and its files would look stale; holding every prune until each dependency resolves keeps that skill's files in place and keeps their manifest records, so a run with every dependency resolved can still prune them.
90
-
91
- ## Key exports from install.mjs
92
-
93
- | Export | Description |
94
- |---|---|
95
- | `CONTENT_DIRECTORIES` | Array of package subdirectory names copied verbatim to `~/.claude/` |
96
- | `MANAGED_TOP_LEVEL_DIRECTORY_NAMES` | The content directories plus `skills` and `hooks`; the stale-file prune walks it to give each root its own diff, and the uninstall purge reads it to find the root a recorded file belongs to |
97
- | `collectFiles(directory)` | Lists every file under a source directory, skipping the build-artifact names and extensions in `install-constants.mjs` |
98
- | `pythonCandidatesForPlatform(platform)` | Returns ordered Python interpreter candidates to probe; `py -3` first on Windows to avoid Microsoft Store alias issues |
99
- | `isWindowsStorePythonStub(path)` | Returns true when the path resolves to the non-spawnable WindowsApps stub |
100
- | `interpreterCommandFromPath(path)` | Formats an absolute interpreter path as a settings.json hook command prefix |
101
- | `collectPackageSourceConflicts(dir)` | Returns any unmerged git conflicts in the package source; installer aborts when any exist |
102
- | `pruneStaleInstalledFiles(priorFiles, currentFiles, destinationRoot, backupRoot, options)` | Moves each manifest-recorded file under the destination root that the run leaves unwritten into the run's backup root; returns `{ prunedCount, failedPaths }`. `options.isCaseInsensitive` drives path-key case folding, defaulting to this host's filesystem; `options.managedHomeDirectory` sets the home the containment guard tests against, defaulting to `~/.claude` |
103
- | `copyTree(sourceBase, destBase, options)` | Copies every file under a source directory, renaming a destination entry that differs from the shipped name only in letter case to the shipped name first; returns `{ created, updated, paths }`. `options.isCaseInsensitive` drives that rename, defaulting to this host's filesystem |
104
- | `caseOnlyRenameSourceName(shippedName, existingNames, options)` | Returns the existing directory entry a shipped file name would overwrite through a case-only spelling difference, or null; `options.isCaseInsensitive` defaults to this host's filesystem |
105
- | `retiredManagedHookRelativePaths(priorFiles, currentFiles, hooksRoot)` | Returns the hook script paths a prior install recorded under the hooks root that this run leaves unwritten, each relative to that root |
106
- | `pruneRetiredHookEntriesFromSettings(settingsPath, retiredPaths)` | Removes each settings.json entry running a retired managed hook script, writing the file only when an entry left it; returns the removed count |
107
- | `retainNewestRunBackupOnly(runBackupRoot, didRunMoveContent)` | Retires every run backup sitting beside the run's own when `didRunMoveContent` holds; clears the run's empty root with `rmdirSync` when it does not |
108
- | `comparisonKeyForPath(path, options)` | Builds the key two paths are compared through: resolved, forward-slashed, and lowercased when `options.isCaseInsensitive` holds — which defaults to true on Windows and macOS |
109
-
110
- ## Install groups
111
-
112
- `install.mjs` defines install groups (`core`, `journal`) plus any dependency groups discovered from `package.json` `dependencies`. The `core` group installs skills, all hooks, and the content directories. `journal` installs only its skill set.
1
+ @AGENTS.md
@@ -39,6 +39,7 @@ export const EVER_SHIPPED_SKILL_NAMES = new Set([
39
39
  'gotcha',
40
40
  'grok-spawn',
41
41
  'grokify',
42
+ 'imagegen',
42
43
  'implement',
43
44
  'issue-tracker',
44
45
  'log-audit',
@@ -6,14 +6,13 @@
6
6
  */
7
7
 
8
8
  /**
9
- * Entry names the source walk leaves behind, whatever directory they sit in.
9
+ * Entry names the source walk keeps out of installed trees.
10
10
  *
11
- * Each name belongs to a tool that writes beside the source it reads: Python
12
- * bytecode caches, the ruff, pytest, and mypy caches, an installed
13
- * `node_modules` tree, and the macOS Finder's `.DS_Store` marker. A contributor
14
- * who runs the test suites and then runs `node bin/install.mjs` copies from a
15
- * source tree carrying all of them, so skipping the names at the walk keeps them
16
- * out of `~/.claude` and out of the install manifest.
11
+ * Each name represents local metadata or build output.
12
+ * A contributor who runs the test suites and then runs `node bin/install.mjs`
13
+ * copies from a source tree carrying these entries, so skipping the names at
14
+ * the walk keeps installed trees and the install manifest focused on runtime
15
+ * files.
17
16
  */
18
17
  export const SKIPPED_SOURCE_ENTRY_NAMES = new Set([
19
18
  '__pycache__',
package/bin/install.mjs CHANGED
@@ -117,6 +117,7 @@ export const CORE_INCLUDE_DIRECTORIES = [
117
117
  export const CORE_SKILLS = [
118
118
  'orchestrator', 'orchestrator-refresh', 'team-advisor', 'grokify',
119
119
  'grok-spawn',
120
+ 'small-cl', 'comments', 'reviews', 'descriptions', 'emergencies',
120
121
  'anthropic-plan', 'everything-search',
121
122
  'privacy-hygiene',
122
123
  'issue-tracker',
@@ -894,9 +895,14 @@ export function copyTree(sourceBase, destBase, options = {}) {
894
895
 
895
896
  /**
896
897
  * If destPath exists and differs from incomingPath, copy the existing file to
897
- * ~/.claude/backups/CLAUDE.md.<timestamp>.bak before the installer overwrites it.
898
+ * ~/.claude/backups/<backupName>.<timestamp>.bak before the installer overwrites it.
899
+ *
900
+ * @param {string} destPath The managed file the installer writes.
901
+ * @param {string} incomingPath The package file that replaces it.
902
+ * @param {string} backupName The managed file name used in the backup path.
903
+ * @returns {string|null} The backup path when existing content differs.
898
904
  */
899
- function backupClaudeHubBeforeOverwrite(destPath, incomingPath) {
905
+ function backupHubBeforeOverwrite(destPath, incomingPath, backupName) {
900
906
  if (!existsSync(destPath)) return null;
901
907
  const existingBytes = readFileSync(destPath);
902
908
  const incomingBytes = readFileSync(incomingPath);
@@ -904,7 +910,7 @@ function backupClaudeHubBeforeOverwrite(destPath, incomingPath) {
904
910
  const backupsDir = join(CLAUDE_HOME, 'backups');
905
911
  mkdirSync(backupsDir, { recursive: true });
906
912
  const stamp = new Date().toISOString().replace(/[:.]/g, '-');
907
- const backupPath = join(backupsDir, `CLAUDE.md.${stamp}.bak`);
913
+ const backupPath = join(backupsDir, `${backupName}.${stamp}.bak`);
908
914
  copyFileSync(destPath, backupPath);
909
915
  return backupPath;
910
916
  }
@@ -928,7 +934,6 @@ export const FOLDED_HOOK_RELATIVE_PATHS = new Set([
928
934
  'blocking/state_description_blocker.py',
929
935
  'blocking/subprocess_budget_completeness.py',
930
936
  'blocking/hook_prose_detector_consistency.py',
931
- 'blocking/verified_commit_message_accuracy_blocker.py',
932
937
  'blocking/workflow_substitution_slot_blocker.py',
933
938
  'blocking/claude_md_orphan_file_blocker.py',
934
939
  'blocking/env_var_table_code_drift_blocker.py',
@@ -1491,23 +1496,58 @@ function loadPackageManagedDenyEntries() {
1491
1496
  }
1492
1497
 
1493
1498
  /**
1494
- * Merge package-owned permission defaults into ~/.claude/settings.json.
1499
+ * Load deny entries an earlier manifest records as package-managed.
1500
+ *
1501
+ * @returns {string[]}
1502
+ */
1503
+ function loadPriorManagedDenyEntries() {
1504
+ if (!existsSync(MANIFEST_FILE)) return [];
1505
+ try {
1506
+ const priorManifest = JSON.parse(readFileSync(MANIFEST_FILE, 'utf8'));
1507
+ return managedDenyEntriesFromPackageSettings({
1508
+ permissions: priorManifest?.[MANIFEST_MANAGED_PERMISSIONS_KEY],
1509
+ });
1510
+ } catch {
1511
+ return [];
1512
+ }
1513
+ }
1514
+
1515
+ /**
1516
+ * Reconcile package-managed permission defaults with ~/.claude/settings.json.
1495
1517
  *
1496
- * @returns {{addedCount: number, alreadyPresentCount: number, managedDenyEntries: string[]}}
1518
+ * @returns {{addedCount: number, alreadyPresentCount: number, removedCount: number, managedDenyEntries: string[]}}
1497
1519
  */
1498
1520
  function mergeManagedPermissions() {
1499
- const managedDenyEntries = loadPackageManagedDenyEntries();
1500
- if (managedDenyEntries.length === 0) {
1501
- return { addedCount: 0, alreadyPresentCount: 0, managedDenyEntries: [] };
1521
+ const allCurrentManagedDenyEntries = loadPackageManagedDenyEntries();
1522
+ const allPriorManagedDenyEntries = loadPriorManagedDenyEntries();
1523
+ const allCurrentManagedDenyEntrySet = new Set(allCurrentManagedDenyEntries);
1524
+ const allRetiredManagedDenyEntries = allPriorManagedDenyEntries.filter(
1525
+ eachEntry => !allCurrentManagedDenyEntrySet.has(eachEntry),
1526
+ );
1527
+ const hasCurrentManagedDenyEntries = allCurrentManagedDenyEntries.length > 0;
1528
+ const hasRetiredManagedDenyEntries = allRetiredManagedDenyEntries.length > 0;
1529
+ if (!hasCurrentManagedDenyEntries && !hasRetiredManagedDenyEntries) {
1530
+ return {
1531
+ addedCount: 0,
1532
+ alreadyPresentCount: 0,
1533
+ removedCount: 0,
1534
+ managedDenyEntries: [],
1535
+ };
1502
1536
  }
1503
1537
  const settingsPath = join(CLAUDE_HOME, SETTINGS_FILE_NAME);
1504
1538
  const settingsExisted = existsSync(settingsPath);
1505
1539
  const settings = loadClaudeSettingsObjectOrExit(settingsPath);
1506
- const mergeOutcome = mergeManagedPermissionsIntoSettings(settings, managedDenyEntries);
1507
- if (mergeOutcome.addedCount > 0 || !settingsExisted) {
1540
+ const pruneOutcome = pruneManagedPermissionsFromSettings(settings, allRetiredManagedDenyEntries);
1541
+ const mergeOutcome = hasCurrentManagedDenyEntries
1542
+ ? mergeManagedPermissionsIntoSettings(settings, allCurrentManagedDenyEntries)
1543
+ : { addedCount: 0, alreadyPresentCount: 0, managedDenyEntries: [] };
1544
+ const shouldWriteSettings = pruneOutcome.removedCount > 0
1545
+ || mergeOutcome.addedCount > 0
1546
+ || (!settingsExisted && hasCurrentManagedDenyEntries);
1547
+ if (shouldWriteSettings) {
1508
1548
  writeFileSync(settingsPath, JSON.stringify(settings, null, 4) + '\n');
1509
1549
  }
1510
- return mergeOutcome;
1550
+ return { ...mergeOutcome, removedCount: pruneOutcome.removedCount };
1511
1551
  }
1512
1552
 
1513
1553
  /**
@@ -2010,6 +2050,9 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2010
2050
 
2011
2051
  const permissionMerge = mergeManagedPermissions();
2012
2052
  summary.managedPermissions = permissionMerge;
2053
+ if (permissionMerge.removedCount > 0) {
2054
+ console.log(` Permissions: ${permissionMerge.removedCount} retired managed deny(s) removed`);
2055
+ }
2013
2056
  if (permissionMerge.managedDenyEntries.length > 0) {
2014
2057
  console.log(
2015
2058
  ` Permissions: ${permissionMerge.addedCount} managed deny(s) added, `
@@ -2021,7 +2064,7 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2021
2064
  const claudeHubSource = join(PACKAGE_ROOT, 'CLAUDE.md');
2022
2065
  if (existsSync(claudeHubSource)) {
2023
2066
  const claudeHubDest = join(CLAUDE_HOME, 'CLAUDE.md');
2024
- const backupPath = backupClaudeHubBeforeOverwrite(claudeHubDest, claudeHubSource);
2067
+ const backupPath = backupHubBeforeOverwrite(claudeHubDest, claudeHubSource, 'CLAUDE.md');
2025
2068
  if (backupPath) {
2026
2069
  console.log(
2027
2070
  ` \u21bb ${relative(CLAUDE_HOME, backupPath)} (previous CLAUDE.md hub preserved)`
@@ -2031,6 +2074,19 @@ function executeInstallPlanMutations(plan, transactionHelpers) {
2031
2074
  allInstalledFiles.push(claudeHubDest);
2032
2075
  console.log(` \u2713 ${relative(CLAUDE_HOME, claudeHubDest)} (hub)`);
2033
2076
  }
2077
+ const agentsHubSource = join(PACKAGE_ROOT, 'AGENTS.md');
2078
+ if (existsSync(agentsHubSource)) {
2079
+ const agentsHubDest = join(CLAUDE_HOME, 'AGENTS.md');
2080
+ const backupPath = backupHubBeforeOverwrite(agentsHubDest, agentsHubSource, 'AGENTS.md');
2081
+ if (backupPath) {
2082
+ console.log(
2083
+ ` \u21bb ${relative(CLAUDE_HOME, backupPath)} (previous AGENTS.md guidance preserved)`
2084
+ );
2085
+ }
2086
+ copyFileSync(agentsHubSource, agentsHubDest);
2087
+ allInstalledFiles.push(agentsHubDest);
2088
+ console.log(` \u2713 ${relative(CLAUDE_HOME, agentsHubDest)} (canonical guidance)`);
2089
+ }
2034
2090
  const isFullInstall = !selectedGroups;
2035
2091
  const didPruneRun = isFullInstall && UNRESOLVED_DEPENDENCY_NAMES.length === 0;
2036
2092
  let failedPrunePaths = [];
@@ -2385,6 +2441,8 @@ with package, version, targetIdentity, managedRoot, files, and skills.
2385
2441
 
2386
2442
  If ~/.claude/CLAUDE.md already exists and differs from the package copy, the installer
2387
2443
  writes the previous contents to ~/.claude/backups/CLAUDE.md.<timestamp>.bak first.
2444
+ If ~/.claude/AGENTS.md already exists and differs from the package copy, the installer
2445
+ writes the previous contents to ~/.claude/backups/AGENTS.md.<timestamp>.bak first.
2388
2446
  `);
2389
2447
  }
2390
2448
 
@@ -37,6 +37,13 @@ const RUNTIME_ARTIFACT_RELATIVE_SEGMENTS = ['scripts', '__pycache__', 'helper.cp
37
37
  const DEPENDENCY_STUB_PACKAGE_SEGMENTS = ['@jl-cmd', 'prompt-generator'];
38
38
  const DEPENDENCY_SUPPLIED_SKILL_DIRECTORY = 'dependency-supplied-skill';
39
39
  const SCOPED_GROUP_SKILL_DIRECTORY = 'orchestrator';
40
+ const CORE_REVIEW_GUIDE_SKILL_DIRECTORIES = [
41
+ 'small-cl',
42
+ 'comments',
43
+ 'reviews',
44
+ 'descriptions',
45
+ 'emergencies',
46
+ ];
40
47
  const PRIOR_RUN_BACKUP_DIRECTORY_NAMES = [
41
48
  '2020-01-01T00-00-00-000Z',
42
49
  '2021-06-15T12-30-45-123Z',
@@ -829,6 +836,25 @@ test('a scoped --only install records the skill names it installed', () => {
829
836
  }
830
837
  });
831
838
 
839
+ test('a scoped core install ships the canonical review guide skills', () => {
840
+ const sandbox = createSandbox();
841
+ try {
842
+ runInstaller(sandbox.homeDirectory, ['--only', 'core']);
843
+
844
+ const missingGuideSkillNames = CORE_REVIEW_GUIDE_SKILL_DIRECTORIES.filter(
845
+ eachSkillName => !existsSync(join(sandbox.skillsDirectory, eachSkillName, 'SKILL.md')),
846
+ );
847
+ assert.deepEqual(missingGuideSkillNames, []);
848
+
849
+ const recordedSkillNames = new Set(readManifest(sandbox.manifestPath).skills);
850
+ for (const eachSkillName of CORE_REVIEW_GUIDE_SKILL_DIRECTORIES) {
851
+ assert.equal(recordedSkillNames.has(eachSkillName), true);
852
+ }
853
+ } finally {
854
+ rmSync(sandbox.homeDirectory, { recursive: true, force: true });
855
+ }
856
+ });
857
+
832
858
  test('the prune skip is scoped: once the dependency resolves a later full install prunes normally', () => {
833
859
  const sandbox = createSandbox();
834
860
  try {